前言
paging是Jetpack推出基于 RecyclerView 的 1 个分页库,之前在项目中使用过paging2,使用起来十分复杂,paging3已经出来快一年了,最新版已经到RC版,相信不久后稳定版就会到来。相比paging2,它使用起来简洁了很多,下面就看下如何使用。(本文使用版本为 3.0.0-rc01 ,基于kotlin开发,项目地址 https://github.com/Tommys-code/Jetpack-samples/tree/master/Paging3Sample)
Paging的使用
1.依赖:
implementation 'androidx.paging:paging-runtime-ktx:3.0.0-rc01'
2.数据源:
2.1Room
paging3如果使用Room来加载数据,使用非常方便,定义Dao的查询数据时,直接返回 PagingSource
类型。同时也不需要定义页数和每页数量,全部交由Paging控制。
@Dao
interface PersonDao {
@Query("SELECT * FROM PersonData ORDER BY id desc")
fun allPerson(): PagingSource<Int, PersonData>
@Insert
suspend fun insert(person: List<PersonData>)
@Delete
suspend fun delete(cheese: PersonData)
}
如果使用Room加载数据,对于插入删除操作,无需额外控制,列表会自动更新。
2.2自定义数据源
对于来自网络或其它地方的数据,需要自定义 PagingSource
class OtherPagingSource : PagingSource<Int, PersonData>() {
override fun getRefreshKey(state: PagingState<Int, PersonData>): Int? {
return null
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PersonData> {
return try {
//页码未定义置为0
val currentPage = params.key ?: 0
//仓库层请求数据
val demoReqData = OtherRepository.loadData(currentPage)
//当前页码 小于 总页码 页面加1
val nextPage = if (!demoReqData.isNullOrEmpty()) {
currentPage + 1
} else {
//没有更多数据
null
}
if (demoReqData != null) {
LoadResult.Page(data = demoReqData, prevKey = null, nextKey = nextPage)
} else {
LoadResult.Error(throwable = Throwable())
}
} catch (e: Exception) {
LoadResult.Error(throwable = e)
}
}
}
需要重写2个方法,getRefreshKey 属于比较高级用法,这里使用不到直接返回null就可以了,在 load 方法中写加载数据逻辑就可以了,我在仓库层使用本地数据模拟网络请求,在正式项目中可以替换为网络请求。
3.构建PagingData
val data: Flow<PagingData<PersonData>> = Pager(
PagingConfig(
// 每页显示的数据的大小
pageSize = 60,
// 开启占位符
enablePlaceholders = true,
// 预刷新的距离,距离最后一个 item 多远时加载数据
prefetchDistance = 3,
//初始化加载数量,默认为 pageSize * 3
initialLoadSize = 60,
//一次应在内存中保存的最大数据,这个数字将会触发,滑动加载更多的数据
maxSize = 200
)
) {
//PagingSource
// OtherPagingSource()
dao.allPerson()
}
.flow
//cachedIn 绑定协程生命周期
.cachedIn(viewModelScope)
//转换成liveData
//.asLiveData(viewModelScope.coroutineContext)
根据数据来源,使用数据库或自定义来源。
4.构建自定义PagingDataAdapter
class PersonAdapter : PagingDataAdapter<PersonData, RecyclerCompatVH<ItemPersonBinding>>(object :
DiffUtil.ItemCallback<PersonData>() {
override fun areItemsTheSame(oldItem: PersonData, newItem: PersonData): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: PersonData, newItem: PersonData): Boolean =
oldItem == newItem
})
直接继承 PagingDataAdapter 传入 DiffUtil.ItemCallback 对象,同时和普通RecycleView.Adapter一样实现2个方法即可。DiffUtil.ItemCallback 是用来对比数据的,一般都是固定写法。
5.绑定数据
构建的数据是 Flow 类型,可以通过 asLiveData 转换为liveData 给adapter绑定,也可以直接用如下方法绑定
lifecycleScope.launchWhenCreated {
viewModel.data.collect { adapter.submitData(it) }
}
adapter.submitData 是一个协程挂起(suspend)操作,所以要放入协程赋值。到这里数据就已经完成了分页展示,向下滑动会自动加载下一页。
UI状态处理和操作
1.下拉刷新
第一次请求不需要任何操作,订阅数据直接请求;手动刷新直接调用:
adapter.refresh()
2.失败重试
- 如果加载中间出错需要重试,只需调用:
adapter.retry()
刷新和重试,adapter内部已经封装好了方法,直接调用即可,比paging2方便很多
3.UI状态处理
对于其它状态,需要使用 adapter.addLoadStateListener 添加状态监听,它的参数 CombinedLoadStates 包含 refresh(刷新),prepend(向前加载更多),append(向后加载更多),source(PagingSource加载),mediator(RemoteMediator加载,没有则为null)。
每个行为分为3中状态:
- LoadState.Loading 加载中 (加载数据时候回调)
- LoadState.NotLoading 没有加载中 (加载数据前和加载数据完成后回调)
- LoadState.Error 加载失败 (加载数据失败回调)
下拉刷新处理:
adapter.addLoadStateListener {
when (it.refresh) {
is LoadState.Error -> {
//取消SwipeRefreshLayout刷新
binding.refresh.isRefreshing = false
//加载出错,显示错误页面
binding.multiStateView.viewState = MultiStateView.ViewState.ERROR
}
is LoadState.NotLoading -> {
binding.refresh.isRefreshing = false
//无数据,显示空页面
if (it.append is LoadState.NotLoading && it.append.endOfPaginationReached && adapter.itemCount == 0) {
binding.multiStateView.viewState = MultiStateView.ViewState.EMPTY
} else if (adapter.itemCount > 0){
binding.multiStateView.viewState = MultiStateView.ViewState.CONTENT
}
}
}
}
这里使用了 MultiStateView 对初始不同状态(空,错误,第一次进入加载)显示不同视图。具体方式可以自行去查看,也可以依据项目要求自行处理逻辑。
上拉加载更多处理:
paging会自动处理加载更多,如果想添加加载更多UI状态显示,PagingDataAdapter 为我们提供了 withLoadStateFooter、withLoadStateHeader以及同时添加头部和尾部方法withLoadStateHeaderAndFooter。对于加载更多,只需自定义 LoadStateAdapter 通过判断 loadstate 来显示不同状态 。
class LoadMoreAdapter(private val retryCallBack: () -> Unit) :
LoadStateAdapter<RecyclerCompatVH<ItemLoadMoreBinding>>() {
override fun onBindViewHolder(
holder: RecyclerCompatVH<ItemLoadMoreBinding>,
loadState: LoadState
) {
//没有下一页
holder.binding.noData.visibility =
if (loadState is LoadState.NotLoading && loadState.endOfPaginationReached) View.VISIBLE else View.GONE
//加载中
holder.binding.loding.visibility =
if (loadState is LoadState.Loading) View.VISIBLE else View.GONE
//出错,重试按钮
holder.binding.btnRetry.visibility =
if (loadState is LoadState.Error) View.VISIBLE else View.GONE
holder.binding.btnRetry.setOnClickListener {
retryCallBack.invoke()
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
loadState: LoadState
): RecyclerCompatVH<ItemLoadMoreBinding> {
return RecyclerCompatVH(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context), R.layout.item_load_more, parent, false
)
)
}
override fun displayLoadStateAsItem(loadState: LoadState): Boolean {
return super.displayLoadStateAsItem(loadState) || (loadState is LoadState.NotLoading && loadState.endOfPaginationReached)
}
}
这里需要注意,如果想要没有下一页状态,需要重写 displayLoadStateAsItem 方法,因为默认加载完以后会移除当前 item。同时需要将 RecycleView 绑定的 adapter 改为添加header和footer后的adapter:
binding.recycle.adapter = adapter.withLoadStateFooter(LoadMoreAdapter { adapter.retry() })
这是一个 ConcatAdapter(之前叫MergeAdapter) ,它可以按顺序连接其他 Adapter,如果感兴趣可以自己去了解。
对数据的删除、新增
对于来源Room的数据,只需要直接删除或修改Room里的数据源,无需其它操作,UI会自动进行更新。
对于自定义数据源,我们都知道,在之前,我们给adapter设置一个List,如果需要删除或者新增,我们只要改变List即可,但是在Paging3中好像没有办法,因为数据源是 PagingSource ,如果需要在不重新请求情况下进行删除和新增可以 自定义 RemoteMediator 实现 network + db 的混合使用,把网络数据缓存到本地数据库,在进行加载,这样只需要删除和修改数据库的数据源就可以直接更新UI。RemoteMediator 具体使用方式可以查看谷歌的 Sample 。但如果有多个列表需要操作,每个都需要建表,而且对于何时清空数据库处理也比较麻烦。
第二种方法,可以通过 adapter.snapshot() 获取当前数据快照,对数据进行增加或删除,然后调用刷新方法,PagingSource 构建时获取修改后的数据源,就能实现。不过这种方法需要动态去判断当前获取本地数据还是网络数据,同时刷新后当前页数会重置,需要记住前一次的页数,比较麻烦。
viewModel.remove(adapter.snapshot().toMutableList().apply{ removeAt(viewHolder.layoutPosition) }.filterNotNull())
adapter.refresh()
private var loadCache = false
private var newData: List<PersonData>? = null
private var pageNum = 0
private suspend fun getData(page: Int): List<PersonData>? {
return if (loadCache) {
loadCache = false
newData
} else {
OtherRepository.loadData(page).apply {
//数据加载完后才能赋值当前页数
pageNum = page
}
}
}
fun remove(data: List<PersonData>) {
loadCache = true
newData = data
}
对于删除,还有一种假删除的方法,直接把删除的item在adapter中改为高度为0的控件,数据没有变,只是把删除的项隐藏起来。
paging3可以很方便的实现列表的分页加载,但对于网络数据源列表删除,新增没有很好的办法,以上办法虽然都能实现,但都不优雅,如果有好的实现方案,欢迎分享!