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
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.