You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 18:06:25