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

使用Remote Mediator时自定义Paging Source无法加载更多数据的解决方法

自定义Paging Source无法加载更多数据的问题

Room默认提供的Paging Source不符合需求,自行实现后出现以下问题:仅能加载前1-2页数据(取决于Remote Mediator的Refresh操作是否删除数据库数据),向下滚动时无法加载更多。推测核心原因是自定义Paging Source未感知到Remote Mediator从API下载并插入数据库的新数据,求解决办法。


自定义Paging Source代码

class TopFilmsLocalPagingSource(
    private val filmLocalStorage: FilmLocalStorage,
    private val type: TopFilmCategories): PagingSource<Int, Film>() {

    override fun getRefreshKey(state: PagingState<Int, Film>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            val anchorPage = state.closestPageToPosition(anchorPosition)
            anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Film> {
        val currentPage = params.key ?: 1
        val offset = (currentPage - 1) * params.loadSize
        val count = params.loadSize

        return try {
            val films = filmLocalStorage.getFilmsByType(offset, count, type)
            val prevKey = if (currentPage == 1) null else currentPage - 1
            val nextKey = if (films.count() < count) null else currentPage + 1

            LoadResult.Page(films, prevKey, nextKey)
        } catch (ex: Exception){
            LoadResult.Error(ex)
        }
    }
}

Remote Mediator代码(companion object中last字段用于测试)

@OptIn(ExperimentalPagingApi::class)
class FilmRemoteMediator(
    private val filmLocalStorage: FilmLocalStorage,
    private val filmRemoteStorage: FilmRemoteStorage,
    private val type: TopFilmCategories): RemoteMediator<Int, Film>() {

    override suspend fun load(loadType: LoadType, state: PagingState<Int, Film>): MediatorResult {
        return try{
            val loadKey = when (loadType) {
                LoadType.REFRESH -> 1
                LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
                LoadType.APPEND -> {
                    last += 1
                    last
                }
            }
            val films = when(type){
                TopFilmCategories.TOP_100_POPULAR_FILMS -> filmRemoteStorage.getPopularFilms(loadKey)
                TopFilmCategories.TOP_250_BEST_FILMS -> filmRemoteStorage.getBestFilms(loadKey)
                TopFilmCategories.TOP_AWAIT_FILMS -> filmRemoteStorage.getTopAwaitFilms(loadKey)
            }
            if (loadType == LoadType.REFRESH) {
                filmLocalStorage.refreshFilmsByType(films, type)
                MediatorResult.Success(endOfPaginationReached = films.isEmpty())
            } else {
                filmLocalStorage.insertAllFilms(films, type)
                MediatorResult.Success(endOfPaginationReached = films.isEmpty())
            }
        } catch (e: IOException) {
            MediatorResult.Error(e)
        } catch (e: HttpException) {
            MediatorResult.Error(e)
        }
    }

    companion object{
        var last = 1
    }
}

Repository代码

class FilmRepositoryImpl @Inject constructor(
    private val filmRemoteStorage: FilmRemoteStorage,
    private val filmLocalStorage: FilmLocalStorage): FilmRepository {

    @OptIn(ExperimentalPagingApi::class)
    override fun getBestFilmsPaged(): Flow<PagingData<DomainFilm>> {
        return Pager(
            PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 20),
            remoteMediator = FilmRemoteMediator(
                filmLocalStorage,
                filmRemoteStorage,
                TopFilmCategories.TOP_250_BEST_FILMS
            )
        ) {
            TopFilmsLocalPagingSource(filmLocalStorage, TopFilmCategories.TOP_250_BEST_FILMS)
        }.flow.toDomain()
    }
}

fun Flow<PagingData<com.gramzin.cinescope.data.model.Film>>.toDomain(): Flow<PagingData<DomainFilm>> {
    return transform { value ->
        emit(value.map { it.toDomain() })
    }
}

操作日志

  • paging source: load page 1
  • remote mediator: refresh operation (load page 1)
  • paging source: load page 2
  • remote mediator: load success
  • remote mediator: prepend operation
  • remote mediator: append operation (load page 2)
  • remote mediator: load success

问题分析与解决办法

1. 自定义Paging Source未监听数据库更新

Room自带的Paging Source会自动监听数据库变化并触发刷新,但自定义的TopFilmsLocalPagingSource无此机制。Remote Mediator插入新数据后,Paging Source无法感知,不会尝试加载新页码。

解决:

  • 在FilmLocalStorage中暴露监听对应类型电影数据变化的Flow,比如getFilmsByTypeFlow(type: TopFilmCategories): Flow<List<Film>>。
  • 在TopFilmsLocalPagingSource中收集该Flow,当数据变化时调用invalidate(),触发Paging Source重新加载。

2. Remote Mediator页码管理逻辑错误

用companion object的last字段管理页码是线程不安全的,且配置变化或重启APP会导致页码重置,同时未结合数据库实际数据量计算下一页。

解决:

  • 移除全局last变量,改为从数据库查询已加载的该类型电影总数,计算下一页:
LoadType.APPEND -> {
    val totalLoaded = filmLocalStorage.getTotalFilmsCount(type)
    (totalLoaded / state.config.pageSize) + 1
}

3. Paging Source的nextKey计算逻辑有误

当Remote Mediator插入数据前,Paging Source加载第2页时若数据量不足,会返回nextKey = null,后续即使数据更新也不会再尝试加载。

解决:

  • 不要仅根据当前加载的数量判断是否有下一页,可结合API返回的总页数,或在Remote Mediator完成插入后主动调用PagingSource.invalidate(),让Paging Source重新计算nextKey。

4. 验证Room分页查询正确性

检查filmLocalStorage.getFilmsByType(offset, count, type)是否正确实现了LIMIT count OFFSET offset,且排序逻辑与API一致,避免数据重复或遗漏。


内容的提问来源于stack exchange,提问作者ramzes1_1

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 13:48:21