Issue whilst paging: Appends error and items position gives problem


Whenever I run the App, it makes a Refresh, then it makes a Prepend and Finally makes a Append, but for some reason, even if I just Load 5 initial Data and the user don't reach the end of LazyColumn, it just loads again another Append (total at the beginning 10). Also, for some reason, if the user scrolls 5 items, it loads again another Append making it (15 items) with just a 5 items scroll

My code for the Pager is this one

@OptIn(ExperimentalPagingApi::class) val posts = Pager( config = PagingConfig( pageSize = 5, initialLoadSize = 5, prefetchDistance = 1, enablePlaceholders = false ), remoteMediator = postsRemoteMediator, pagingSourceFactory = { postsDAO.getAllPosts() } ).flow.map { pagingData -> pagingData.map { postsEntity -> postsEntity.toDomain() } }.cachedIn(viewModelScope)

My RemoteMediator code is

@OptIn(ExperimentalPagingApi::class)
class PostsRemoteMediator @Inject constructor(
    private val flubyApiV2: FlubyApiV2,
    private val postsDAO: PostsDAO,
    private val postsRemoteKeysDAO: PostsRemoteKeysDAO,
    private val postsDatabase: PostsDatabase
) : RemoteMediator<Int, PostsEntity>() {

    override suspend fun initialize(): InitializeAction {
        val cacheTimeout = TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES)

        return if((System.currentTimeMillis() - postsRemoteKeysDAO.lastUpdated()) <= cacheTimeout) {
            InitializeAction.SKIP_INITIAL_REFRESH
        } else {
            InitializeAction.LAUNCH_INITIAL_REFRESH
        }

    }

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, PostsEntity>
    ): MediatorResult {

        return try {

            val currentPage = when(loadType) {
                LoadType.REFRESH -> {
                    Log.d("REMOTE_MEDIATOR", "➡️ REFRESH")
                    1
                }
                LoadType.PREPEND -> return MediatorResult.Success(
                    endOfPaginationReached = true
                )
                LoadType.APPEND -> {

                    Log.d("REMOTE_MEDIATOR", "➡️ APPEND")

                    val remoteKeys = getRemoteKeysForLastItem(state)


                    Log.d("PAGING", "$remoteKeys")

                    val nextPage = remoteKeys?.nextPage
                        ?: return MediatorResult.Success(
                            endOfPaginationReached = remoteKeys != null
                        )
                    nextPage
                }
            }

            val response = flubyApiV2.getPosts(page = currentPage, limit = state.config.pageSize)
            val endOfPaginationReached = response.isEmpty()

            Log.d(
                "REMOTE_MEDIATOR",
                "ended? → $endOfPaginationReached"
            )

            val prevPage = if(currentPage == 1) null else currentPage - 1
            val nextPage = if(endOfPaginationReached) null else currentPage + 1

            postsDatabase.withTransaction {

                if(loadType == LoadType.REFRESH) {
                    postsDAO.deletePosts()
                    postsRemoteKeysDAO.deleteAllRemoteKeys()
                }

                val keys = response.map { post ->
                    PostsRemoteKeysEntity(
                        id = post._id,
                        prevPage = prevPage,
                        nextPage = nextPage,
                        lastUpdated = System.currentTimeMillis()
                    )
                }

                postsRemoteKeysDAO.addAllRemoteKeys(remoteKeys = keys)
                postsDAO.addPosts(posts = response.map { it.toEntity() })
            }

            MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)

        } catch (e: IOException) {
            return MediatorResult.Error(e)
        } catch (e: HttpException) {
            return MediatorResult.Error(e)
        } catch (e: Exception) {
            return MediatorResult.Error(e)
        }

    }

    private suspend fun getRemoteKeysForLastItem(state: PagingState<Int, PostsEntity>): PostsRemoteKeysEntity? {
        return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()?.let { post ->
            postsRemoteKeysDAO.getRemoteKeys(id = post.id)
        }
    }


}
2026-09-22 03:41:14.520 30309-30309 REMOTE_MEDIATOR D  REFRESH
2026-09-22 03:41:14.521 30309-30309 REMOTE_MEDIATOR D  ➡️ REFRESH
2026-09-22 03:41:17.171 30309-30309 REMOTE_MEDIATOR D  ended? → false
2026-09-22 03:41:17.317 30309-30309 REMOTE_MEDIATOR D  PREPEND
2026-09-22 03:41:17.317 30309-30309 REMOTE_MEDIATOR D  APPEND
2026-09-22 03:41:17.317 30309-30309 REMOTE_MEDIATOR D  ➡️ APPEND
2026-09-22 03:41:17.463 30309-30309 REMOTE_MEDIATOR D  APPEND
2026-09-22 03:41:17.463 30309-30309 REMOTE_MEDIATOR D  ➡️ APPEND
2026-09-22 03:41:18.007 30309-30309 REMOTE_MEDIATOR D  ended? → false
2026-09-22 03:41:18.097 30309-30309 REMOTE_MEDIATOR D  APPEND
2026-09-22 03:41:18.097 30309-30309 REMOTE_MEDIATOR D  ➡️ APPEND
2026-09-22 03:41:18.544 30309-30309 REMOTE_MEDIATOR D  ended? → false
2026-09-22 03:41:18.607 30309-30309 REMOTE_MEDIATOR D  APPEND
2026-09-22 03:41:18.607 30309-30309 REMOTE_MEDIATOR D  ➡️ APPEND
2026-09-22 03:41:19.025 30309-30309 REMOTE_MEDIATOR D  ended? → false
2026-09-22 03:41:29.121 30309-30309 REMOTE_MEDIATOR D  APPEND
2026-09-22 03:41:29.121 30309-30309 REMOTE_MEDIATOR D  ➡️ APPEND
2026-09-22 03:41:29.572 30309-30309 REMOTE_MEDIATOR D  ended? → true
0
Sep 22 at 7:55 AM
User AvatarFluby
#android#kotlin#android-paging-3

Accepted Answer

you don't have enough checks for actions already performed by the program, which causes parallel requests.

in load method you need to check somehow that the download is already underway. That is, it should be something like

private var isLoad = false  

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, PostsEntity>
    ): MediatorResult {

        if (isLoad) {
            return MediatorResult.Success(endOfPaginationReached = false)
        }

        isLoad = true

        return try {

} catch (e: IOException) {
            return MediatorResult.Error(e)
        } catch (e: HttpException) {
            return MediatorResult.Error(e)
        } catch (e: Exception) {
            return MediatorResult.Error(e)
        } finally {
	isLoad = false
	}
}

only check for empty response, but not check for cases where there are more or less elements than pageSize

val endOfPaginationReached = response.isEmpty() || response.size < state.config.pageSize

I'm not sure about this, but it seems to me that you always have the same value here, because postsRemoteKeysDAO.lastUpdated() will always return null. We need to test it experimentally.

return if((System.currentTimeMillis() - postsRemoteKeysDAO.lastUpdated()) <= cacheTimeout)

have a lot of cascading queries if the element is small and the mouse is spinning fast, since the prefetchDistance is only 1. Try increasing this parameter.

User Avataruser31774114
Sep 22 at 6:01 PM
1