0赞
赞赏
更多好文
在 Android 开发中,“分页加载”几乎是每个 App 都绕不开的需求。无论是微博的时间线、电商的商品列表,还是新闻资讯流,数据量一大,我们就不能一次性加载全部内容——内存会炸、流量会爆、用户体验也会变差。
过去,我们可能用 RecyclerView.OnScrollListener 手动监听滚动、维护页码、处理加载状态……代码冗长、逻辑复杂、容易出错。
但现在,Google 官方推出的 Paging 3.0(基于 Kotlin 协程和 Flow)彻底改变了这一切。
今天,我们就手把手教你用 Paging 3.0 + Kotlin 实现一套健壮、简洁、可测试的分页加载方案。
一、为什么选择 Paging 3.0?
Paging 3.0 是 Google Jetpack 的一部分,相比旧版(Paging 2.x),它有三大核心优势:
✅ 全面协程化:使用 Flow 和 suspend 函数,天然支持异步与取消
✅ 内存高效:自动管理页面缓存,避免重复请求
✅ 状态统一:内置加载状态(Loading / Error / Refresh)、重试机制、占位符支持
✅ 架构清晰:分离数据源、UI 层和业务逻辑,符合 MVVM 原则
💡 注意:Paging 3.0 要求最低 API 21,且强烈建议配合 Room 或 Retrofit 使用。
二、核心组件速览
Paging 3.0 的架构非常清晰,主要由三部分组成:
| 组件 | 作用 |
|---|---|
PagingSource | 数据源实现,负责从网络或数据库加载分页数据 |
Pager | 配置分页参数(如页面大小),并生成 Flow<PagingData<T>> |
PagingDataAdapter | 继承自 RecyclerView.Adapter,自动处理分页数据的 Diff 和更新 |
整个数据流如下:
PagingSource → Pager → Flow<PagingData> → collect → submitData() → PagingDataAdapter
三、实战:从零搭建一个分页列表
假设我们要实现一个“用户列表”,每次加载 20 条,支持下拉刷新和上拉加载更多。
第一步:添加依赖(build.gradle)
implementation "androidx.paging:paging-runtime:3.3.0"
// 如果用 RxJava 或 Guava,可选 paging-rxjava3 / paging-guava
第二步:定义数据模型
data class User(
val id: Int,
val name: String,
val email: String
)
第三步:实现 PagingSource
这是最关键的一步!你需要继承 PagingSource<Key, Value>。
Key:通常是页码(Int)或游标(String)Value:你的数据类型(如User)
class UserPagingSource(
private val api: UserService // 假设你有一个 Retrofit 接口
) : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
return try {
// 获取当前页码,默认第一页为 1
val page = params.key ?: 1
// 调用网络接口(必须是 suspend 函数)
val response = api.getUsers(page = page, size = params.loadSize)
LoadResult.Page(
data = response.users,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.users.isEmpty()) null else page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
// 可选:用于识别数据源(比如缓存时)
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
return state.anchorPosition?.let { anchorPos ->
state.closestItemToPosition(anchorPos)?.id?.minus(1)
}
}
}
🔍 说明:
params.loadSize由 Pager 配置决定(通常就是 pageSize)prevKey/nextKey控制是否还能向前/向后加载- 返回
LoadResult.Page表示成功,LoadResult.Error表示失败
第四步:在 Repository 中创建 Pager
class UserRepository(private val api: UserService) {
fun getUsers(): Flow<PagingData<User>> {
return Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false, // 是否显示占位符
maxSize = 200 // 缓存最大条目数
),
pagingSourceFactory = { UserPagingSource(api) }
).flow
}
}
第五步:ViewModel 中暴露数据流
@HiltViewModel
class UserListViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
val users = repository.getUsers().cachedIn(viewModelScope)
}
✅
cachedIn(viewModelScope)很重要!它会让 Flow 在 ViewModel 生命周期内复用,避免配置变更(如旋转屏幕)时重复请求。
第六步:实现 PagingDataAdapter
class UserAdapter : PagingDataAdapter<User, UserViewHolder>(UserDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
return UserViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_user, parent, false))
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
getItem(position)?.let { user -> holder.bind(user) }
}
}
// DiffUtil 回调
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean = oldItem == newItem
}
第七步:Activity / Fragment 中收集数据
class UserListFragment : Fragment() {
private lateinit var adapter: UserAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = UserAdapter()
recyclerView.adapter = adapter
lifecycleScope.launch {
viewModel.users.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
}
}
⚠️ 注意:一定要用
collectLatest!它会在新数据到来时自动取消旧的收集,避免竞态问题。
四、进阶:处理加载状态与错误重试
Paging 3.0 内置了强大的状态监听能力。
监听加载状态(如显示“正在加载更多”)
lifecycleScope.launch {
adapter.loadStateFlow.collect { loadStates ->
val refreshState = loadStates.refresh
val appendState = loadStates.append // 上拉加载更多
when {
refreshState is LoadState.Loading -> showLoading()
refreshState is LoadState.Error -> showError(refreshState.error)
appendState.endOfPaginationReached -> hideLoadMoreIndicator()
}
}
}
添加“重试”按钮
retryButton.setOnClickListener {
adapter.retry() // 自动重试失败的加载
}
下拉刷新支持
配合 SwipeRefreshLayout:
swipeRefreshLayout.setOnRefreshListener {
adapter.refresh() // 触发重新加载第一页
}
// 监听刷新完成以关闭刷新动画
lifecycleScope.launch {
adapter.loadStateFlow.collect { loadStates ->
swipeRefreshLayout.isRefreshing = loadStates.refresh is LoadState.Loading
}
}
五、常见问题 & 最佳实践
❓ Q1:如何实现“滑到底部自动加载”?
A:Paging 3.0 默认就支持!只要 nextKey != null,滑到底部就会自动触发 load()。
❓ Q2:能同时从网络和数据库加载吗?
A:可以!推荐使用 RemoteMediator(适用于 Room + 网络混合场景),但逻辑更复杂,适合离线优先应用。
❓ Q3:如何预加载(提前加载下一页)?
A:通过 PagingConfig.prefetchDistance 设置(默认是 pageSize 的一半)。
✅ 最佳实践:
- 网络请求务必用
suspend函数(Retrofit 2.6+ 支持) - 不要在
PagingSource中持有 Activity/Fragment 引用 - 使用
cachedIn(viewModelScope)避免重复请求 - 用
collectLatest而不是collect
六、结语
Paging 3.0 把复杂的分页逻辑封装得如此优雅,让我们开发者可以专注业务,而非样板代码。
它不仅是“分页工具”,更是现代 Android 架构(协程 + Flow + MVVM)的最佳实践体现。
如果你还在手写分页逻辑,是时候升级了!
🎁 附:完整示例代码已开源
GitHub 地址:github.com/yourname/paging3-demo
👉 关注【Kotlin 实战派】,回复“Paging3”获取完整项目模板!
下期预告:《Room + Paging 3.0 实现离线优先的分页列表》
让代码更 Kotlin,让开发更高效。
