前言
Paging库是Google新推出的jetPack组件,主要方便封装分页逻辑。
使用Paging库后,我们不需要再考虑加载下一页的逻辑,可以做到自动加载,同时可以方便的观察加载下一页的状态,是成功还是失败
本文主要包括Paging3的基本使用与部分源码解析,具体如下:
1.Paging3的基本使用
2.Paging3自动加载更多原理
Paging3的基本使用
主要优点
Paging 库包含以下功能:
- 分页数据的内存中缓存。这可确保您的应用在处理分页数据时高效利用系统资源。
- 内置的请求重复信息删除功能,可确保您的应用高效地利用网络带宽和系统资源。
- 可配置的 RecyclerView 适配器,它们会在用户滚动到已加载数据的末尾时自动请求数据。
- 对 Kotlin 协程和流程以及 LiveData 和 RxJava 的一流支持。
- 内置对错误处理功能的支持,包括刷新和重试功能。
基本结构
里面几个类的作用:
PagingSource
:单一的数据源。RemoteMediator
:其实RemoteMediator
也是单一的数据源,它会在PagingSource
没有数据的时候,再使用RemoteMediator
提供的数据,如果既存在数据库请求,又存在网络请求,通常PagingSource
用于进行数据库请求,RemoteMediator
进行网络请求。PagingData
:单次分页数据的容器。Pager
:用来构建Flow<PagingData>
的类,实现数据加载完成的回调。PagingDataAdapter
:分页加载数据的RecyclerView
的适配器。
简述一下就是 PagingSource
和 RemoteMediator
充当数据源的角色,ViewModel
使用 Pager
中提供的 Flow<PagingData>
监听数据刷新.
每当 RecyclerView
即将滚动到底部的时候,就会有新的数据的到来,最后,PagingAdapter
展示数据。
基本使用
1.配置数据源
首先需要生成数据层,配置数据源
private const val SHOE_START_INDEX = 0;
class CustomPageDataSource(private val shoeRepository: ShoeRepository) : PagingSource<Int, Shoe>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Shoe> {
val pos = params.key ?: SHOE_START_INDEX
val startIndex = pos * params.loadSize + 1
val endIndex = (pos + 1) * params.loadSize
return try {
// 从数据库拉去数据
val shoes = shoeRepository.getPageShoes(startIndex.toLong(), endIndex.toLong())
// 返回你的分页结果,并填入前一页的 key 和后一页的 key
LoadResult.Page(
shoes,
if (pos <= SHOE_START_INDEX) null else pos - 1,
if (shoes.isNullOrEmpty()) null else pos + 1
)
}catch (e:Exception){
LoadResult.Error(e)
}
}
}
复制代码
2. 生成可观察的数据集
第二步则是在viewModel中生成可观察的数据集
这里可观察数据集包括 LiveData
、Flow
以及 RxJava
中的 Observable
和 Flowable
,其中,RxJava 需要单独引入扩展库去支持的。
class ShoeModel constructor(private val shoeRepository: ShoeRepository) : ViewModel() {
/**
* @param config 分页的参数
* @param pagingSourceFactory 单一数据源的工厂,在闭包中提供一个PageSource即可
* @param remoteMediator 同时支持网络请求和数据库请求的数据源
* @param initialKey 初始化使用的key
*/
var shoes = Pager(config = PagingConfig(
pageSize = 20
, enablePlaceholders = false
, initialLoadSize = 20
), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).flow
// ... 省略
}
复制代码
3.创建Adapter
和普通的 Adapter 没有特别大的区别,主要是:
- 提供
DiffUtil.ItemCallback<Shoe>
- 继承
PagingDataAdapter
使用PagingAdapter
需要实现DiffUtil.ItemCallback
接口,因为后续提交数据时,会根据DiffUtil
接口判断数据是否相同,从而做插入与删除操作
同时使用Paging需要继承PagingDataAdapter
,这通常需要我们修改已有的基类,这也是Paging库的一个主要缺点
4.在UI中使用
如果只显示数据,我们要做的是:
- 创建和设置适配器。
- 开启一个协程
- 在协程中接收 Flow 提供的数据。
val adapter = ShoeAdapter(context!!)
binding.recyclerView.adapter = adapter
job = viewModel.viewModelScope.launch(Dispatchers.IO) {
viewModel.shoes.collect() {
adapter.submitData(it)
}
}
复制代码
5.监听数据加载状态
Paging可以监听数据的加载状态,状态对应的类是 LoadState,它有三种状态:
- Loading:数据加载中。
- NotLoading:内存中有已经获取的数据,即使往下滑,Paging 也不需要请求更多的数据。
- Error:请求数据时返回了一个错误。
监听数据状态的代码:
adapter.addLoadStateListener {state:CombinedLoadStates->
//... 状态监听
}
复制代码
监听方法就是这么简单,可以看到这个 state
并不是 LoadState
,而是一个 CombinedLoadStates
,顾名思义,就是多个 LoadState
组合而成的状态类,它里面有:
refresh:LoadState
:刷新时的状态,因为可以调用PagingDataAdapter#refresh()
方法进行数据刷新。append:LoadState
:可以理解为 RecyclerView 向下滑时数据的请求状态。prepend:LoadState
:可以理解为RecyclerView 向上滑时数据的请求状态。
Paging3如何实现自动加载更多?
当RecyclerView即将滚动到底部时,Paging库会自动加载更多,我们可以看下是怎样实现的
实际上,Paging的加载更多逻辑是通过PagingDataAdapter
的getItem()
方法触发的
protected fun getItem(@IntRange(from = 0) position: Int) = differ.getItem(position)
复制代码
这里的differ
是一个AsyncPagingDataDiffer
对象:
fun getItem(@IntRange(from = 0) index: Int): T? {
try {
inGetItem = true
return differBase[index]
} finally {
inGetItem = false
}
}
复制代码
接着看differBase
的get()
方法,differBase
是一个PagingDataDiffer
对象:
operator fun get(index: Int): T? {
lastAccessedIndex = index
receiver?.addHint(presenter.loadAround(index))
return presenter.get(index)
}
复制代码
这里的receiver
是一个UiReceiver
对象,在初始化时会初始化为PagerUiReceiver
,下面看一下addHint()
方法
inner class PagerUiReceiver<Key : Any, Value : Any> constructor(
private val pageFetcherSnapshot: PageFetcherSnapshot<Key, Value>,
private val retryChannel: SendChannel<Unit>
) : UiReceiver {
override fun addHint(hint: ViewportHint) = pageFetcherSnapshot.addHint(hint)
override fun retry() {
retryChannel.offer(Unit)
}
override fun refresh() = this@PageFetcher.refresh()
}
复制代码
这里的pageFetcherSnapshot
是一个PageFetcherSnapshot
对象
fun addHint(hint: ViewportHint) {
lastHint = hint
@OptIn(ExperimentalCoroutinesApi::class)
hintChannel.offer(hint)
}
复制代码
这里的hintChannel
是一个BroadcastChannel
对象,只要该channel
中有新值,它会广播给所有的订阅者,下面看一下订阅hintChannel
的地方
hintChannel.asFlow()
// Prevent infinite loop when competing PREPEND / APPEND cancel each other
.drop(if (generationId == 0) 0 else 1)
.map { hint -> GenerationalViewportHint(generationId, hint) }
}
// Prioritize new hints that would load the maximum number of items.
.runningReduce { previous, next ->
if (next.shouldPrioritizeOver(previous, loadType)) next else previous
}
.conflate()
.collect { generationalHint ->
doLoad(loadType, generationalHint)
}
复制代码
可以看到上游hintChannel有值时,会构造一个GenerationalViewportHint
对象,下游会调用doLoad()
方法:
private suspend fun doLoad(
loadType: LoadType,
generationalHint: GenerationalViewportHint
) {
.....
var loadKey: Key? = stateHolder.withLock { state ->
state.nextLoadKeyOrNull(
loadType,
generationalHint.generationId,
generationalHint.presentedItemsBeyondAnchor(loadType) + itemsLoaded,
)?.also { state.setLoading(loadType) }
}
...
loop@ while (loadKey != null) {
val params = loadParams(loadType, loadKey)
val result: LoadResult<Key, Value> = pagingSource.load(params)
....
}
}
复制代码
可以看出在loadKey
不为null
时,会调用pagingSource
的load
方法从而加载下一页,我们看看nextLoadKeyOrNull
方法
private fun PageFetcherSnapshotState<Key, Value>.nextLoadKeyOrNull(
loadType: LoadType,
generationId: Int,
presentedItemsBeyondAnchor: Int
): Key? {
if (generationId != generationId(loadType)) return null
// Skip load if in error state, unless retrying.
if (sourceLoadStates.get(loadType) is Error) return null
// Skip loading if prefetchDistance has been fulfilled.
if (presentedItemsBeyondAnchor >= config.prefetchDistance) return null
return if (loadType == PREPEND) {
pages.first().prevKey
} else {
pages.last().nextKey
}
}
复制代码
从上可以看出,只有当加载状态为成功,且最后一个的距离小于预加载距离时,才会返回nextKey
,即开始加载下一页
以上就是Paing3自动加载下一页的源码分析,总结为时序图如下: