为什么要分页
一个长列表可能有几千条数据。一次性全拉下来,流量大、加载慢、列表还卡。分页就是用户滚到哪、加载到哪,一次一页,体验丝滑。
滚到哪加载到哪,不一次性拉全。
Jetpack Paging 3 把这套逻辑封装好了,直接用。
手写分页不难但容易出 bug:滚到底没触发、重复加载、刷新后页码错位、加加载中状态忘了关。Paging 3 把这些脏活全包了,只管告诉它"下一页在哪一页、接口长什么样",剩下的翻页、去重、预取、错误重试它都自己来。
Paging 3 三件套
PagingSource 告诉数据从哪来、怎么翻页;Pager 把它变成一个 Flow;列表把这个 Flow 渲染出来。三者配合。
分页最难的其实是"下拉刷新"和"局部更新"。Paging 3 的刷新就是重新跑一遍 load,你只要在 ViewModel 里调一下 refresh,列表会从头开始重新加载。点赞、收藏这类单条更新,用 LazyPagingItems 的 peek 或者监听远端数据变化,让 Paging 自动 diff,不用整列表重刷。
- PagingSource定义数据源。
- Pager产出 Flow。
- collectAsLazyPagingItems喂给列表。
pageSize 别贪大,20 到 30 条是舒服区间;太小翻页次数多、太大首次加载慢。配 prefetchDistance 让列表提前在距底部几屏时就预取下一页,滚动才不卡。配合 cachedIn 把流放在 viewModelScope,旋转屏幕不重新加载。
写 PagingSource
class NewsSource : PagingSource<Int, News>() {
override suspend fun load(p: LoadParams<Int>): LoadResult<Int, News> {
val page = p.key ?: 1
val data = api.fetchNews(page)
return LoadResult.Page(data, prevKey = null, nextKey = page + 1)
}
}
LoadResult.Page 里的 prevKey 和 nextKey 是翻页的关键:最后一页就把 nextKey 设成 null,Paging 就知道到底了、不再请求。数据源出错时返回 LoadResult.Error(e),界面上会自动显示加载失败的状态,可以配合重试按钮。
页面上收集流
用 Pager 构造流,ViewModel 暴露出去。
val flow = Pager(PagingConfig(pageSize = 20)) { NewsSource() }.flow.cachedIn(viewModelScope)
Compose 渲染列表
val items = newsFlow.collectAsLazyPagingItems()
LazyColumn { items(items) { NewsRow(it) } }
实战:新闻流分页
把上面三块拼起来:数据源拉一页、Pager 产出流、列表滚到底自动拉下一页。整个分页链路就通了。
调试时先用 Log 打一下 load 被调了几次、key 是多少,确认翻页逻辑没问题。真机上滚到底看一下加载圈是不是只出现一次、会不会重复请求同一页。做到这步,分页就稳了。
小结
Paging 3 让长列表分页几乎不用手写:写个 PagingSource、Pager 出流、列表收集渲染,翻页逻辑它全包。
从协程 Flow 切过来的项目基本无痛,Paging 本身就是 Flow 的一层包装。