参考文章:
Paging | Android 开发者 | Android Developers
Android Jetpack组件(七)Paging_八归少年-CSDN博客
Android Jetpack组件之 Paging使用-源码_Jason_Lee155的博客-CSDN博客
一、简介
应用开发过程中分页加载时很普遍的需求,它能节省数据流量,提升应用的性能。 Google为了方便开发者完成分页加载而推出了分页组件—Paging。为几种常见的分页机制提供了统一的解决方案。
- 优势
- 分页数据的内存中缓存。该功能可确保应用在处理分页数据时高效利用系统资源。
- 内置的请求重复信息删除功能,可确保应用高效利用网络带宽和系统资源。
- 可配置的RecyclerView适配器,会在用户滚动到已加载数据的末尾时自动请求数据。
- 对Kotlin协程和Flow以及LiveData和RxJava的一流支持。
- 内置对错误处理功能的支持,包括刷新和重试功能。
- 数据来源:Paging支持三种数据架构类型
- 网络:对网络数据进行分页加载是最常见的需求。API接口通常不太一样,Paging提供了三种不同的方案,应对不同的分页机制。Paging不提供任务错误处理功能,发生错误后可重试网络请求。
- 数据库:数据库进行分页加载和网络类似,推荐使用Room数据库修改和插入数据。
- 网络+数据库:通常只采用单一数据源作为解决方案,从网络获取数据,直接缓存进数据库,列表直接从数据库中获取数据。
二、核心
2.1 核心类
Paging的工作原理主要涉及三个类:
- PagedListAdapter:
RecyclerView.Adapter
基类,用于在RecyclerView
显示来自PagedList
的分页数据。 - PagedList:
PagedList
负责通知DataSource
何时获取数据,如加载第一页、最后一页及加载数量等。从DataSource获取的数据将存储在PagedList
中。 - DataSource:执行具体的数据载入工作,数据载入需要在工作线程中进行
以上三个类的关系及数据加载流程如下图:
当一条新的item插入到数据库,DataSource会被初始化,LiveData后台线程就会创建一个新的PagedList。这个新的PagedList会被发送到UI线程的PagedListAdapter中,PagedListAdapter使用DiffUtil在对比现在的Item和新建Item的差异。当对比结束,PagedListAdapter通过调用RecycleView.Adapter.notifyItemInserted()将新的item插入到适当的位置
2.2 DataSource
根据分页机制的不同,Paing为我们提供了三种DataSource。
-
PositionalDataSource 适用于可通过任意位置加载数据,且目标数据源数量固定的情况。
-
PageKeyedDataSource 适合数据源以“页”的方式进行请求的情况。如获取数据携带
page
和pageSize
时。本文代码使用此DataSource -
ItemKeyedDataSource 适用于当目标数据的下一页需要依赖上一页数据中的最后一个对象中的某个字段作为key的情况,如评论数据的接口携带参数
since
和pageSize
。
三、使用
3.1 构建自己的DataSource
DataSource控制数据加载,包括初始化加载,加载上页数据,加载下页数据。此处我们以PageKeyedDataSource为例
//泛型参数未Key Value,Key就是每页的标志,此处为Long,Value为数据类型
class ListDataSource : PageKeyedDataSource<Long, Item>() {
//重试加载时的参数
private var lastLoadParam: Pair<LoadParams<Long>, LoadCallback<Long, Item>>? = null
//初始化加载数据
override fun loadInitial(
params: LoadInitialParams<Long>,
callback: LoadInitialCallback<Long, Item>
) {
CLog.i(TAG, "loadInitial!!")
lastLoadParam = null
refreshListLiveData.postValue(true)
loadInitListState.postValue(LoadListState.STATE_LOADING)
dispose()
fetchList(cId, Id, 0, { data, nextKey ->
CLog.i(TAG, "loadInitial success callback!!")
val key = if (nextKey != -1L) nextKey else null
//成功后的回调,data是处理后的数据,Id是加载上页数据的Key,key是加载下页的Key,cId与业务相关
callback.onResult(data, Id, key)
//第一次加载了最后一页数据的特殊处理
loadAfterListState.value = LoadListState.STATE_LOAD_END
refreshListLiveData.value = false
loadInitListState.value = LoadListState.STATE_LOAD_END
isListEmpty.value = data.isEmpty()
}, {
CLog.i(TAG, "loadInitial failed callback!!")
refreshListLiveData.value = false
networkError.value = true
//失败的回调,两个Key填null
callback.onResult(listOf(), null, null)
})
}
//加载上页数据
override fun loadBefore(
params: LoadParams<Long>,
callback: LoadCallback<Long, Item>
) {
CLog.i(TAG, "loadBefore start")
loadBeforeListState.postValue(changeLoadState(loadBeforeListState.value!!))
fetchList(cId, params.key, 1, { data, nextKey ->
CLog.i(TAG, "loadBefore success callback")
lastLoadParam = null
val key = if (nextKey != -1L) nextKey else null
//在回调中填入数据和Key
callback.onResult(data.drop(1).reversed(), key)
loadBeforeListState.value = LoadListState.STATE_LOAD_END
}, {
CLog.i(TAG, "loadBefore failed callback")
lastLoadParam = params to callback
loadBeforeListState.value = LoadListState.STATE_LOAD_ERROR
})
}
//加载下页数据
override fun loadAfter(params: LoadParams<Long>, callback: LoadCallback<Long, Item>) {
CLog.i(TAG, "loadAfter start")
loadAfterListState.postValue(changeLoadState(loadAfterListState.value!!))
fetchList(categoryId, params.key, 0, { data, nextKey ->
CLog.i(TAG, "loadAfter success callback")
lastLoadParam = null
val key = if (nextKey != -1L) nextKey else null
//在回调中填入数据和Key
callback.onResult(data.drop(1), key)
loadAfterListState.value = LoadListState.STATE_LOAD_END
}, {
CLog.i(TAG, "loadAfter failed callback")
lastLoadParam = params to callback
loadAfterListState.value = LoadListState.STATE_LOAD_ERROR
})
}
//头部重试函数
fun retryLoadHead() {
val param = lastLoadParam
if (param != null)
loadBefore(param.first, param.second)
}
//尾部重试函数
fun retryLoadFoot() {
val param = lastLoadParam
if (param != null) {
loadAfter(param.first, param.second)
}
}
//type:为0加载下方数据,为1加载上方数据
fun fetchList(
cId: Int,
cursor: Long,
type: Int,
onComplete: (List<Item>, Long) -> Unit,
onFail: () -> Unit
) {
//封装好的网络请求方法
val req = ScanContentListReq().apply {
this.cId = cId
scanReq = ScanReq().apply {
this.cursor = cursor
limit = PAGE_SIZE
scanType = type
}
}
scanListRxJava(req)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<ScanListResp> {
override fun onSubscribe(d: Disposable) {
}
override fun onNext(resp: ScanListResp) {
if (resp.baseResp.error.code != 0) {
onFail()
return
}
if (resp.contentList == null) {
onFail()
return
}
val key = if (resp.hasMore) resp.newCursor else -1
val data = resp.List
.map {
val ListItem = Item(it)
ListItem
}
onComplete(data, key)
}
override fun onError(e: Throwable) {
networkError.value = true
}
override fun onComplete() {
}
})
}
}
其中的关键点在于,每次Key的选定以及loadInitial
、loadBefore
和loadAfter
三个函数的重写。PageKeyedDataSource的Key一般依赖与服务端返回的数据。
3.2 构建PagedList
companion object{
private const val TAG = "List"
const val PAGE_SIZE = 5
const val FETCH_DIS = 1
}
val ListData: LiveData<PagedList<Item>> = LivePagedListBuilder(
dataSourceFactory,
Config(
PAGE_SIZE,
FETCH_DIS,
true
)
).build()
其中PAGE_SIZE
是每页的数量,FETCH_DIS
是距离最后一个数据item还有多少距离就触发加载动作。
此处ListData
是LiveData类型,因此可以在Activity中进行监听,当发生数据变化时,则刷新adapter:
ListViewModel.ListData.observe(this) {
adapter.submitList(it)
}
3.3 构建自己的PagedListAdapter
一定要继承PagedListAdapter<Item, RecyclerView.ViewHolder>(``POST_COMPARATOR``)
,POST_COMPARATOR
就是DiffUtil
,PagedListAdapter使用DiffUtil在对比现在的Item和新建Item的差异。
typealias ItemClickListener = (Item) -> Unit
typealias onClickListener = () -> Unit
class ListAdapter(
private val context: Context,
private val onItemClickListener: ItemClickListener,
private val retryHeadClickListener: onClickListener,
private val retryFootClickListener: onClickListener
) : PagedListAdapter<Item, RecyclerView.ViewHolder>(POST_COMPARATOR) {
companion object {
private const val TAG = "ListAdapter"
val POST_COMPARATOR = object : DiffUtil.ItemCallback<Item>() {
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean =
oldItem.id == newItem.id
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean =
oldItem.id == newItem.id
override fun getChangePayload(oldItem: Item, newItem: Item): Any? {
return null
}
}
const val TYPE_ITEM = 0
const val TYPE_LOAD_HEAD = 1
const val TYPE_LOAD_FOOT = 2
}
private var loadAfterState = LoadListState.STATE_NONE
private var loadBeforeState = LoadListState.STATE_NONE
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (getItemViewType(position)) {
TYPE_LOAD_HEAD -> (holder as LoadingHeadViewHolder).bind(loadBeforeState)
TYPE_LOAD_FOOT -> (holder as LoadingFootViewHolder).bind(loadAfterState)
else -> {
(holder as ItemViewHolder).bind(
getItem(position - 1)!!,
position,
Id
)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
TYPE_LOAD_FOOT -> LoadingFootViewHolder.create(parent, this, retryFootClickListener)
TYPE_LOAD_HEAD -> LoadingHeadViewHolder.create(parent, this, retryHeadClickListener)
else -> ItemViewHolder.create(parent, onItemClickListener)
}
}
private fun isFirstItem(position: Int) = position == 0
private fun isLastItem(position: Int) = position == (itemCount - 1)
override fun getItemCount() = super.getItemCount() + 2
override fun getItemViewType(position: Int): Int {
return when {
isLastItem(position) -> TYPE_LOAD_FOOT
isFirstItem(position) -> TYPE_LOAD_HEAD
else -> TYPE_ITEM
}
}
}
可以看到基本写法和普通的RecyclerView.Adapter
是差不多的,只是多了DiffUtil
,使用起来也是一样:
adapter = ListAdapter(
this,
onItemClickListener,
headRetryClickListener,
footRetryClickListener
)
list_rv.adapter = adapter
四、Paging 3.0
Paging3与旧版Paging存在很大区别。Paging2.x运行起来的效果无限滑动还不错,不过代码写起来有点麻烦,功能也不是太完善,比如下拉刷新的方法都没有提供,我们还得自己去调用DataSource#invalidate()
方法重置数据来实现。Paging3.0功能更加强大,用起来更简单。
4.1 区别
- DataSource
Paing2中的DataSource有三种,Paging3中将它们合并到了PagingSource中,实现load()和getRefreshKey(),在Paging3中,所有加载方法参数被一个LoadParams密封类替代,该类中包含了每个加载类型所对应的子类。如果需要区分load()中的加载类型,需要检查传入了LoadParams的哪个子类
- PagedListAdapter
Adapter不在继承PagedListAdapter
,而是由PagingDataAdapter
替代,其它不变。
class ArticleAdapter : PagingDataAdapter<Article,ArticleViewHolder>(POST_COMPARATOR){
companion object{
val POST_COMPARATOR = object : DiffUtil.ItemCallback<Article>() {
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean =
oldItem == newItem
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean =
oldItem.id == newItem.id
}
}
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
holder.tvName.text = getItem(position)?.title
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
return ArticleViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_item,parent,false))
}
}
class ArticleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val tvName: TextView = itemView.findViewById(R.id.tvname)
}
4.2 获取数据并设置给Adapter
google提倡我使用三层架构来完成数据到Adapter的设置,如下图
代码库层
代码库层中的主要 Paging 库组件是 PagingSource
。每个 PagingSource
对象都定义了数据源,以及如何从该数据源检索数据。PagingSource
对象可以从任何单个数据源(包括网络来源和本地数据库)加载数据。可使用的另一个 Paging 库组件是 RemoteMediator
。RemoteMediator
对象会处理来自分层数据源(例如具有本地数据库缓存的网络数据源)的分页。
ViewModel 层
Pager
组件提供了一个公共 API,基于 PagingSource
对象和 PagingConfig
配置对象来构造在响应式流中公开的 PagingData
实例。将 ViewModel
层连接到界面的组件是 PagingData
。
PagingData
对象是用于存放分页数据快照的容器。它会查询 PagingSource
对象并存储结果。
界面层
界面层中的主要 Paging 库组件是 PagingDataAdapter
。