阅读 1492

关于Paging3你应该知道的知识点|牛气冲天新年征文

前言

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 的适配器。

简述一下就是 PagingSourceRemoteMediator 充当数据源的角色,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中生成可观察的数据集
这里可观察数据集包括 LiveDataFlow 以及 RxJava 中的 ObservableFlowable,其中,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的加载更多逻辑是通过PagingDataAdaptergetItem()方法触发的

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
        }
    }
复制代码

接着看differBaseget()方法,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时,会调用pagingSourceload方法从而加载下一页,我们看看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自动加载下一页的源码分析,总结为时序图如下:

参考资料

即学即用Android Jetpack - Paging 3
Paging内部原理