Paging3搭配RemoteMediator卡在第3页 空闲时持续调用APPEND
Paging3 接入RemoteMediator离线缓存异常
问题现象
- 列表未滚动、处于空闲状态时,组件持续调用
LoadType.APPEND触发加载逻辑 - 分页加载卡在第3页,无法加载初始3页之外的其他分页数据
相关实现代码
RemoteMediator实现
@ExperimentalPagingApi class ProductRemoteMediator( private val remoteDataSource: ProductRemoteDataSource, private val localDataSource: ProductLocalDataSource, ) : RemoteMediator<Int, ProductEntity>() { override suspend fun load( loadType: LoadType, state: PagingState<Int, ProductEntity> ): MediatorResult { val page = when (loadType) { REFRESH -> { val remoteKeys = getRemoteKeyClosestToCurrentPosition(state) remoteKeys?.nextKey?.minus(1) ?: STARTING_PAGE_INDEX } PREPEND -> { val remoteKeys = getRemoteKeyForFirstItem(state) val prevKey = remoteKeys?.prevKey ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null) prevKey } APPEND -> { val remoteKeys = getRemoteKeyForLastItem(state) val nextKey = remoteKeys?.nextKey ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null) nextKey } } return try { Timber.d("loadType=$loadType") Timber.d("page=$page, size=${state.config.pageSize}") val products = remoteDataSource.getProductsAsBuyer( page = page, size = state.config.pageSize, ).map { it.mapToEntityModel() } val endOfPaginationReached = products.isEmpty() Timber.d("endOfPaginationReached=$endOfPaginationReached") localDataSource.cacheProductTransaction { if (loadType == REFRESH) { localDataSource.clearRemoteKeys() localDataSource.clearCachedProducts() } val prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1 val nextKey = if (endOfPaginationReached) null else page + 1 val keys = products.map { RemoteKeys(productId = it.id, prevKey = prevKey, nextKey = nextKey) } localDataSource.insertRemoteKeys(keys) localDataSource.cacheAllProducts(products) } MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) } catch (exception: IOException) { MediatorResult.Error(exception) } catch (exception: HttpException) { MediatorResult.Error(exception) } catch (exception: Exception) { MediatorResult.Error(exception) } } private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, ProductEntity>): RemoteKeys? { return state.pages .lastOrNull { it.data.isNotEmpty() } ?.data ?.lastOrNull() ?.let { repo -> localDataSource.getRemoteKeysId(repo.id) } } private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, ProductEntity>): RemoteKeys? { return state.pages .firstOrNull { it.data.isNotEmpty() } ?.data ?.firstOrNull() ?.let { repo -> localDataSource.getRemoteKeysId(repo.id) } } private suspend fun getRemoteKeyClosestToCurrentPosition( state: PagingState<Int, ProductEntity> ): RemoteKeys? { return state.anchorPosition?.let { position -> state.closestItemToPosition(position)?.id?.let { repoId -> localDataSource.getRemoteKeysId(repoId) } } } }
Repository层Pager实现
override fun getProductsAsBuyer(): Flow<PagingData<ProductEntity>> { return Pager( config = PagingConfig(pageSize = NETWORK_PAGE_SIZE), remoteMediator = ProductRemoteMediator( remoteDataSource = remoteDataSource, localDataSource = localDataSource, ), pagingSourceFactory = { localDataSource.getCachedProducts() } ).flow }
补充信息:技术栈为Jetpack Compose,通过
collectAsLazyPagingItems()实现列表数据渲染。
排查与修复方案
1. 空闲状态无限触发APPEND问题修复
两个核心原因:
PagingConfig默认开启占位符enablePlaceholders = true,但本地数据源的PagingSource没有返回数据总数支撑占位符计算,Paging判定当前已加载条目不足以填满视口,就会持续触发APPEND请求PagingConfig默认初始加载大小为3 * pageSize,如果首屏实际展示的条目数超过这个初始加载量,也会触发连续加载
直接修改PagingConfig配置即可:
PagingConfig( pageSize = NETWORK_PAGE_SIZE, initialLoadSize = NETWORK_PAGE_SIZE * 2, // 按首屏可展示的最大条目数调整,一般设pageSize的2-3倍即可 enablePlaceholders = false // 未做占位符适配直接关闭 )
2. 分页卡在第3页问题修复
根因是REFRESH场景页码计算逻辑错误,搭配默认3倍pageSize的初始加载规则触发:
首次加载时Paging默认拉3页数据(对应page1、page2、page3),原代码里REFRESH分支的remoteKeys?.nextKey?.minus(1)在后续触发刷新时会算错页码,加上原逻辑用products.isEmpty()判断分页结束存在边界问题——如果后端最后一页返回数据量小于pageSize但不为空,会提前判定分页结束,不再加载更多。
修复两个点:
- 给RemoteKeys表新增
currentKey字段,存储条目所在的当前页码,修正REFRESH分支的页码取值逻辑:
REFRESH -> { val remoteKeys = getRemoteKeyClosestToCurrentPosition(state) remoteKeys?.currentKey ?: STARTING_PAGE_INDEX }
对应修改RemoteKeys生成逻辑,事务内写入key时赋值currentKey:
val keys = products.map { RemoteKeys( productId = it.id, currentKey = page, prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1, nextKey = if (endOfPaginationReached) null else page + 1 ) }
- 修正分页结束判断逻辑,不要用返回列表是否为空判断,改为判断返回数据长度小于请求的pageSize:
val endOfPaginationReached = products.size < state.config.pageSize
额外检查项
- 确认本地RemoteKeys表的插入方法使用
OnConflictStrategy.REPLACE冲突策略,避免重复key插入失败导致加载中断 - 确认清缓存、清RemoteKeys、插入新数据、插入新key的逻辑全部包裹在同一个数据库事务内,避免数据和key不匹配导致页码计算错误
内容的提问来源于stack exchange,提问作者wiryadev
相关产品推荐
相关产品推荐

