'Android Paging 3 RemoteMediator does not go to the next page
I use RemoteMediator (Paging 3) to cache files. The problem is that when I scroll to the last item, does not go to the next page ... such problems with PagingSource did not have been
interface ImagesApi {
@GET(".?safesearch=true")
suspend fun searchImages(
@Query("q") query: String,
@Query("page") page: Int,
@Query("per_page") perPage: Int
): ImagesResponse
}
@Dao
interface ImageDao {
@Query("SELECT * FROM ${ImageEntity.TABLE_IMAGES}")
fun getImage(): PagingSource<Int,ImageEntity>
@Query("SELECT * FROM ${ImageEntity.TABLE_IMAGES}")
suspend fun getImageList(): List<ImageEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(images: List<ImageEntity>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertImage(image: ImageEntity)
@Delete
suspend fun deleteImage(image: ImageEntity)
@Query("DELETE FROM ${ImageEntity.TABLE_IMAGES}")
suspend fun clearAll()
}
@Dao
interface ImageRemoteKeysDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(remoteKey: List<ImageRemoteKeys>)
@Query("SELECT * FROM ${ImageRemoteKeys.IMAGE_REMOTE_KEY_TABLE} WHERE repoId = :repoId")
suspend fun remoteKeysById(repoId: Long): ImageRemoteKeys?
@Query("DELETE FROM ${ImageRemoteKeys.IMAGE_REMOTE_KEY_TABLE}")
suspend fun clearRemoteKeys()
}
@Entity(tableName = IMAGE_REMOTE_KEY_TABLE)
data class ImageRemoteKeys(
@PrimaryKey
val repoId: Long,
val prevKey: Int?,
val nextKey: Int?,
) {
companion object {
const val IMAGE_REMOTE_KEY_TABLE = "image_remote_key_table"
}
}
class ImageRemoteMediator(
private val service: ImagesApi,
private val query: String,
private val database: PixabayDb,
) : RemoteMediator<Int, ImageEntity>() {
private val imageDao = database.imageDao()
private val imageRemoteKeyDao = database.imageRemoteKeysDao()
override suspend fun initialize(): InitializeAction {
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, ImageEntity>
): MediatorResult {
return try {
val page = when (loadType) {
LoadType.REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: IMAGE_STARTING_PAGE_INDEX
}
LoadType.PREPEND -> {
val remoteKeys = getRemoteKeyForFirstItem(state)
val prevKey = remoteKeys?.prevKey
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
prevKey
}
LoadType.APPEND -> {
val remoteKeys = getRemoteKeyForLastItem(state)
val nextKey = remoteKeys?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
nextKey
}
}
val response = service.searchImages(query, page, state.config.pageSize)
val images = response.hits
val endPaginationReached = images.isEmpty()
database.withTransaction {
if (loadType == LoadType.REFRESH) {
imageRemoteKeyDao.clearRemoteKeys()
imageDao.clearAll()
}
val prevKey = if (page == IMAGE_STARTING_PAGE_INDEX) null else page - 1
val nextKey = if (endPaginationReached) null else page + 1
val keys = images.map {
ImageRemoteKeys(
repoId = it.id,
prevKey = prevKey,
nextKey = nextKey
)
}
Log.d(
"MEDIATOR",
"${images.size}, $prevKey, $nextKey ${state.config.pageSize}, ${keys.size}"
)
imageRemoteKeyDao.insertAll(keys)
imageDao.insertAll(images.map { it.toEntity() })
}
MediatorResult.Success(endOfPaginationReached = endPaginationReached)
} catch (e: IOException) {
MediatorResult.Error(e)
} catch (e: HttpException) {
MediatorResult.Error(e)
}
}
private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, ImageEntity>): ImageRemoteKeys? {
return state.pages.lastOrNull() { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { image ->
imageRemoteKeyDao.remoteKeysById(image.remoteId)
}
}
private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, ImageEntity>): ImageRemoteKeys? {
return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
?.let { image ->
imageRemoteKeyDao.remoteKeysById(image.remoteId)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(state: PagingState<Int, ImageEntity>): ImageRemoteKeys? {
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.remoteId?.let { imageId ->
imageRemoteKeyDao.remoteKeysById(imageId)
}
}
}
}
class ImageRepository(
private val service: ImagesApi,
private val database: PixabayDb,
) {
@ExperimentalPagingApi
fun getSearchResult(query: String): Flow<PagingData<ImageEntity>> =
Pager(
config = PagingConfig(
pageSize = 200,
enablePlaceholders = false
),
remoteMediator = ImageRemoteMediator(
service,
query,
database
),
pagingSourceFactory = { database.imageDao().getImage() }
// pagingSourceFactory = { ImagePagingSource(service, query, database) }
).flow
}
class ImageViewModel(
private val repository: ImageRepository,
state: SavedStateHandle
) : ViewModel() {
private val currentQuery = state.getLiveData(LAST_SEARCH_QUERY, DEFAULT_QUERY)
var refreshInProgress = false
var pendingScrollToTopAfterRefresh = false
var newQueryInProgress = false
var pendingScrollToTopAfterNewQuery = false
@ExperimentalPagingApi
@ExperimentalCoroutinesApi
val images = currentQuery.asFlow().flatMapLatest { query ->
repository.getSearchResult(query)
}.cachedIn(viewModelScope)
fun searchImage(query: String) {
currentQuery.value = query
newQueryInProgress = true
pendingScrollToTopAfterNewQuery = true
}
private companion object {
const val LAST_SEARCH_QUERY = "current_query"
const val DEFAULT_QUERY = "dogs"
}
}
class ImagesFragment : BaseFragment(R.layout.fragment_images) {
private val binding by viewBinding(FragmentImagesBinding::bind)
private val viewModel by viewModel<ImageViewModel>()
override fun setRecyclerView(): RecyclerView = binding.recyclerViewImages
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val imageAdapter = ImagePagingAdapter(object : OnClickListener<ImageEntity> {
override fun click(item: ImageEntity) {
val direction =
ImagesFragmentDirections.actionPhotosFragmentToImageDetailFragment(item)
navController.navigate(direction)
}
})
binding.run {
searchInput.requestFocus()
searchInput.afterTextChanged(viewModel::searchImage)
imageAdapter.withLoadStateFooter(
LoadAdapter(imageAdapter::retry)
)
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
imageAdapter.loadStateFlow.collect { loadState ->
when (val refresh = loadState.mediator?.refresh) {
is LoadState.Loading -> {
textViewError.isVisible = false
buttonRetry.isVisible = false
textViewNoResults.isVisible = false
recyclerViewImages.showIfOrInvisible {
!viewModel.newQueryInProgress && imageAdapter.itemCount > 0
}
viewModel.refreshInProgress = true
viewModel.pendingScrollToTopAfterRefresh = true
}
is LoadState.NotLoading -> {
textViewError.isVisible = false
buttonRetry.isVisible = false
recyclerViewImages.isVisible = imageAdapter.itemCount > 0
val noResult =
imageAdapter.itemCount < 1 && loadState.append.endOfPaginationReached
&& loadState.source.append.endOfPaginationReached
textViewNoResults.isVisible = noResult
viewModel.refreshInProgress = false
viewModel.newQueryInProgress = false
}
is LoadState.Error -> {
textViewNoResults.isVisible = false
recyclerViewImages.isVisible = imageAdapter.itemCount > 0
val noCachedResults =
imageAdapter.itemCount < 1 && loadState.source.append.endOfPaginationReached
textViewError.isVisible = noCachedResults
buttonRetry.isVisible = noCachedResults
val errorMessage = getString(
R.string.could_not_load_search_results,
refresh.error.localizedMessage
?: getString(R.string.unknown_error_occurred)
)
textViewError.text = errorMessage
if (viewModel.refreshInProgress) {
showSnackbar(errorMessage)
}
viewModel.refreshInProgress = false
viewModel.newQueryInProgress = false
viewModel.pendingScrollToTopAfterRefresh = false
}
}
}
}
buttonRetry.setOnClickListener {
imageAdapter.retry()
}
}
setAdapter(imageAdapter)
lifecycleScope.launchWhenStarted {
viewModel.images.collectLatest { data ->
imageAdapter.submitData(data)
}
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
