50、Paging3分页加载:PagingSource、PagingData、PagingDataAdapter、RemoteMediator
分页加载,说白了就是「数据太多,一次拿不完」。
我刚开始做列表页时,总觉得一次性把所有数据塞进 RecyclerView 就完事了。直到有一次,用户反馈说「打开页面卡了 3 秒」,我一看日志——好家伙,一次性拉了 2000 条数据。嗯,从那以后,我再也不敢这么干了。
Paging3 是 Google 官方提供的分页解决方案。它帮你处理了「什么时候加载下一页」「加载过程中 UI 怎么展示」「数据源切换」这些脏活累活。今天我们就把它拆开看看。
核心组件一览
Paging3 有四个核心角色,我习惯把它们比作一个「数据流水线」:
| 组件 | 角色 | 一句话说明 |
|---|---|---|
| PagingSource | 数据工人 | 负责从网络或数据库里搬数据 |
| PagingData | 数据容器 | 装着一批一批的数据,按页切好 |
| PagingDataAdapter | 搬运工 | 把 PagingData 里的数据展示到 RecyclerView |
| RemoteMediator | 调度员 | 协调网络和本地数据库,实现「先看缓存,再拉新」 |
核心流程: PagingSource 产出数据 → 包装成 PagingData → PagingDataAdapter 消费展示 → RemoteMediator 在需要时触发网络加载。
PagingSource:数据从哪里来
PagingSource 是你需要自己实现的类。它告诉 Paging3:「数据长什么样」「怎么按页取」。我个人习惯把它放在 repository 层,和网络请求或数据库查询放在一起。
class ArticlePagingSource(
private val api: ArticleApi,
private val query: String
) : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
return try {
val page = params.key ?: 1
val response = api.searchArticles(query, page, params.loadSize)
LoadResult.Page(
data = response.articles,
prevKey = if (page > 1) page - 1 else null,
nextKey = if (response.articles.isNotEmpty()) page + 1 else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
小技巧: getRefreshKey 决定了刷新后回到哪一页。我一般用 anchorPosition 来定位,这样用户下拉刷新后还能回到之前看的位置,体验好很多。
PagingData:数据怎么流动
PagingData 是一个不可变的「数据流」。你不需要手动创建它,而是通过 Pager 来生成。它内部维护了加载状态、重试机制、数据合并等逻辑。
val pagingDataFlow: Flow<PagingData<Article>> = Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
initialLoadSize = 40
),
pagingSourceFactory = { ArticlePagingSource(api, query) }
).flow.cachedIn(viewModelScope)
这里有个坑——cachedIn 必须调用。我曾经漏掉它,结果每次屏幕旋转都重新加载数据,用户骂声一片。cachedIn 会把 PagingData 缓存到 ViewModel 的作用域里,配置变更时不会丢失。
PagingDataAdapter:数据怎么展示
PagingDataAdapter 继承自 RecyclerView.Adapter,但它知道怎么「增量更新」。你只需要像普通 Adapter 一样实现 DiffUtil 即可。
class ArticleAdapter : PagingDataAdapter<Article, ArticleAdapter.ViewHolder>(ArticleDiffCallback()) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) ?: return
holder.bind(item)
}
// ... ViewHolder 实现略
}
// 在 Fragment 或 Activity 中
val adapter = ArticleAdapter()
binding.recyclerView.adapter = adapter
viewLifecycleOwner.lifecycleScope.launch {
viewModel.pagingDataFlow.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
注意: submitData 必须在 lifecycleScope 中调用,并且使用 collectLatest。否则会出现「数据还没加载完,协程就被取消」的问题。我踩过这个坑,调试了一下午才发现是 collect 和 collectLatest 的区别。
RemoteMediator:网络+数据库的终极方案
如果你的 App 有本地数据库(比如 Room),RemoteMediator 就是你的最佳搭档。它的工作流程是:
- 先查本地数据库,有数据就直接展示
- 用户往下翻,本地数据不够了,触发 RemoteMediator
- RemoteMediator 去网络拉新数据,存到数据库
- 数据库更新后,PagingSource 自动感知变化,刷新列表
说白了,就是「先看缓存,再拉新」。用户体验上,用户永远看到的是「秒开」,因为数据来自本地。
class ArticleRemoteMediator(
private val api: ArticleApi,
private val db: ArticleDatabase,
private val query: String
) : RemoteMediator<Int, Article>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Article>
): MediatorResult {
return try {
val page = when (loadType) {
LoadType.REFRESH -> 1
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
if (lastItem == null) 1 else (lastItem.pageIndex + 1)
}
}
val response = api.searchArticles(query, page, state.config.pageSize)
db.withTransaction {
if (loadType == LoadType.REFRESH) {
db.articleDao().clearAll()
}
db.articleDao().insertAll(response.articles)
}
MediatorResult.Success(endOfPaginationReached = response.articles.isEmpty())
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
}
关键点: PREPEND 一般直接返回 Success,因为往上翻页的场景很少。REFRESH 时记得清空旧数据,否则会出现「新旧数据混在一起」的 bug。我曾在生产环境遇到过这个问题,用户反馈「列表里出现了两条一模一样的新闻」,就是因为没清空旧数据。
避坑指南
- 不要用 List 接收 PagingData: PagingData 是流式的,不能直接转成 List。如果你需要操作数据,请在 PagingSource 里处理。
- 页面大小别设太小: pageSize 设成 10 以下,会导致频繁触发加载,网络请求太多。我一般设 20 或 30。
- enablePlaceholders 看情况: 如果设为 true,列表会预留空位,但需要你知道总数据量。不知道总条数时,设为 false 更安全。
- RemoteMediator 的事务: 数据库写入一定要用事务,否则可能出现「写了一半,App 被杀」的情况。
总结
Paging3 不是那种「学完就忘」的库。它解决的是真实世界里的真实问题——数据太多、网络太慢、用户没耐心。你想想看,如果你的列表页能做到「秒开 + 无限滚动 + 不卡顿」,用户怎么会不喜欢?
我个人建议,新项目直接上 Paging3 + Room + RemoteMediator 的组合。虽然初期配置多一点,但后期维护起来真的很省心。
公众号:蓝海资料掘金营,微信deep3321