8、Paging3:Paging3基础概念、PagingSource与RemoteMediator、PagingData与UI层
各位同学,今天我们来聊聊Paging3。说实话,这个库刚出来的时候,我内心是拒绝的——又学新东西?但真正用上之后,嗯,真香。Paging3解决了一个很实际的问题:当你的数据多到一屏装不下时,怎么优雅地加载?
说白了,分页加载就是「用多少,拿多少」。但Android里实现方式五花八门,有人用LinearLayout手动加Footer,有人用RecyclerView的OnScrollListener自己算。Paging3把这些脏活累活都封装好了,你只需要告诉它「数据从哪来」和「怎么展示」就行。
8.1 核心概念:Paging3到底在做什么?
先看一张图,帮你快速建立整体认知:
这张图我画了好几次才满意。你看,Paging3把整个流程分成了三层:数据源、数据流、UI层。每一层各司其职,互不干扰。我在项目中经常看到有人把网络请求直接写在Adapter里——千万别这么干,Paging3就是来帮你解耦的。
8.2 PagingSource:数据从哪来?
PagingSource是Paging3的数据源接口。你需要实现它,告诉Paging3「怎么加载每一页的数据」。我习惯把它理解成一个「数据水龙头」——拧一下(调用一次load方法),就流出一批数据。
来看一个最简单的例子,从网络加载用户列表:
class UserPagingSource(
private val api: UserApi,
private val query: String
) : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
return try {
// params.key 就是页码,第一次为null时用1
val page = params.key ?: 1
val pageSize = params.loadSize // Paging3自动计算的最佳大小
val response = api.searchUsers(query, page, pageSize)
LoadResult.Page(
data = response.users,
prevKey = if (page > 1) page - 1 else null,
nextKey = if (response.hasMore) page + 1 else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
// 这个方法用于支持下拉刷新时重新加载
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
return state.anchorPosition?.let { anchorPos ->
state.closestPageToPosition(anchorPos)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPos)?.nextKey?.minus(1)
}
}
}
关键点:
LoadParams包含页码和加载大小,Paging3会自动管理LoadResult.Page返回数据和前后页码getRefreshKey用于刷新后定位到之前的位置
我曾经犯过一个低级错误:在load方法里直接返回了LoadResult.Page(prevKey = page - 1, nextKey = page + 1),没有判断边界。结果用户翻到最后一页时,nextKey还是非null,Paging3继续请求,返回空数据,然后崩溃。嗯,边界判断一定要做。
8.3 RemoteMediator:网络+数据库的黄金搭档
如果你的App只用网络分页,PagingSource就够了。但大多数实际项目都是「网络+本地缓存」的模式——先展示缓存数据,后台再刷新。这时候就需要RemoteMediator了。
RemoteMediator的作用是:当本地数据不够时,自动去网络拉取,然后存到数据库。我把它比作「数据搬运工」——从网络搬到本地,再让PagingSource从本地读取。
@OptIn(ExperimentalPagingApi::class)
class UserRemoteMediator(
private val api: UserApi,
private val db: UserDatabase
) : RemoteMediator<Int, User>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, User>
): MediatorResult {
return try {
// 1. 确定要加载的页码
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.page + 1)
}
}
// 2. 请求网络
val response = api.getUsers(page)
// 3. 存入数据库
db.userDao().insertAll(response.users.map { it.toEntity(page) })
MediatorResult.Success(
endOfPaginationReached = response.users.isEmpty()
)
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
}
注意:RemoteMediator需要配合@OptIn(ExperimentalPagingApi::class)注解,因为它在Paging3.0还是实验性API。另外,数据库表里最好加一个page字段,方便定位。
我记得第一次用RemoteMediator时,遇到了一个诡异的问题:下拉刷新后,列表数据重复了。排查了半天,发现是数据库的insertAll用了REPLACE策略,但主键没设对。后来我把userId设为主键,问题就解决了。细节决定成败啊。
8.4 PagingData:连接数据与UI的桥梁
PagingData是一个不可变的数据容器,它持有分页数据的快照。你不需要直接操作它,而是通过Pager来生成:
// 在ViewModel中
val userPagingData: Flow<PagingData<User>> = Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
initialLoadSize = 40
),
pagingSourceFactory = { UserPagingSource(api, query) }
).flow
.cachedIn(viewModelScope) // 缓存数据,避免重复加载
我的建议:
pageSize设20-30比较合适,太小会导致频繁请求,太大则失去分页意义enablePlaceholders设为false,避免出现空白占位符,用户体验更好cachedIn()一定要调用,否则旋转屏幕时会重新加载
8.5 UI层:PagingDataAdapter与LoadState
UI层就简单多了。PagingDataAdapter继承自RecyclerView.Adapter,但它是专门为PagingData设计的。你只需要实现onBindViewHolder和onCreateViewHolder,然后调用submitData()即可。
class UserAdapter : PagingDataAdapter<User, UserViewHolder>(UserComparator) {
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bind(getItem(position))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
return UserViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_user, parent, false)
)
}
companion object {
// DiffUtil比较器,用于高效更新列表
private val UserComparator = object : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
}
}
在Fragment或Activity中,这样使用:
val adapter = UserAdapter()
binding.recyclerView.adapter = adapter
// 收集PagingData流
lifecycleScope.launch {
viewModel.userPagingData.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
// 监听加载状态
adapter.addLoadStateListener { loadState ->
when (loadState.refresh) {
is LoadState.Loading -> showLoading()
is LoadState.Error -> showError((loadState.refresh as LoadState.Error).error)
is LoadState.NotLoading -> hideLoading()
}
}
避坑指南:我曾经在collectLatest里直接调用了adapter.submitData(),但忘了用lifecycleScope。结果Activity销毁后,协程还在跑,导致内存泄漏。记住:collectLatest一定要绑定生命周期。
8.6 总结一下
Paging3的核心就三件事:
- PagingSource:定义数据怎么从源头加载
- RemoteMediator:处理网络+数据库的混合分页
- PagingData + PagingDataAdapter:把数据优雅地展示到UI上
说实话,Paging3的学习曲线不算陡,但坑确实不少。我建议你先从最简单的PagingSource开始,跑通一个Demo,再逐步引入RemoteMediator。别一上来就搞「网络+数据库+分页」的大全套,容易把自己绕晕。
好了,这一章就到这里。记住:分页不是功能,是体验。用户滑动时卡顿一下,可能就流失了。Paging3帮你把体验做好了,你只需要把业务逻辑写对就行。