'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