安卓分页加载器——Paging使用指南

3,325 阅读7分钟

参考文章:

Paging  |  Android 开发者  |  Android Developers

Android Jetpack组件(七)Paging_八归少年-CSDN博客

Android Jetpack组件之 Paging使用-源码_Jason_Lee155的博客-CSDN博客

Android Jetpack架构-Paging自定义上拉加载更多_One X的博客-CSDN博客

Android官方架构组件Paging-Ex:为分页列表添加Header和Footer - 掘金

一、简介

应用开发过程中分页加载时很普遍的需求,它能节省数据流量,提升应用的性能。 Google为了方便开发者完成分页加载而推出了分页组件—Paging。为几种常见的分页机制提供了统一的解决方案。

  • 优势
    • 分页数据的内存中缓存。该功能可确保应用在处理分页数据时高效利用系统资源。
    • 内置的请求重复信息删除功能,可确保应用高效利用网络带宽和系统资源。
    • 可配置的RecyclerView适配器,会在用户滚动到已加载数据的末尾时自动请求数据。
    • 对Kotlin协程和Flow以及LiveData和RxJava的一流支持。
    • 内置对错误处理功能的支持,包括刷新和重试功能。
  • 数据来源:Paging支持三种数据架构类型
    • 网络:对网络数据进行分页加载是最常见的需求。API接口通常不太一样,Paging提供了三种不同的方案,应对不同的分页机制。Paging不提供任务错误处理功能,发生错误后可重试网络请求。
    • 数据库:数据库进行分页加载和网络类似,推荐使用Room数据库修改和插入数据。
    • 网络+数据库:通常只采用单一数据源作为解决方案,从网络获取数据,直接缓存进数据库,列表直接从数据库中获取数据。

二、核心

2.1 核心类

Paging的工作原理主要涉及三个类:

  1. PagedListAdapter:RecyclerView.Adapter基类,用于在RecyclerView显示来自PagedList的分页数据。
  2. PagedList:PagedList负责通知DataSource何时获取数据,如加载第一页、最后一页及加载数量等。从DataSource获取的数据将存储在PagedList中。
  3. DataSource:执行具体的数据载入工作,数据载入需要在工作线程中进行

以上三个类的关系及数据加载流程如下图:

20181021221030916.gif

当一条新的item插入到数据库,DataSource会被初始化,LiveData后台线程就会创建一个新的PagedList。这个新的PagedList会被发送到UI线程的PagedListAdapter中,PagedListAdapter使用DiffUtil在对比现在的Item和新建Item的差异。当对比结束,PagedListAdapter通过调用RecycleView.Adapter.notifyItemInserted()将新的item插入到适当的位置

2.2 DataSource

根据分页机制的不同,Paing为我们提供了三种DataSource。

  1. PositionalDataSource 适用于可通过任意位置加载数据,且目标数据源数量固定的情况。

  2. PageKeyedDataSource 适合数据源以“页”的方式进行请求的情况。如获取数据携带pagepageSize时。本文代码使用此DataSource

  3. ItemKeyedDataSource 适用于当目标数据的下一页需要依赖上一页数据中的最后一个对象中的某个字段作为key的情况,如评论数据的接口携带参数sincepageSize

三、使用

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的选定以及loadInitialloadBeforeloadAfter三个函数的重写。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的设置,如下图

image.png

代码库层

代码库层中的主要 Paging 库组件是 PagingSource。每个 PagingSource 对象都定义了数据源,以及如何从该数据源检索数据。PagingSource 对象可以从任何单个数据源(包括网络来源和本地数据库)加载数据。可使用的另一个 Paging 库组件是 RemoteMediatorRemoteMediator 对象会处理来自分层数据源(例如具有本地数据库缓存的网络数据源)的分页。

ViewModel 层

Pager 组件提供了一个公共 API,基于 PagingSource 对象和 PagingConfig 配置对象来构造在响应式流中公开的 PagingData 实例。将 ViewModel 层连接到界面的组件是 PagingDataPagingData 对象是用于存放分页数据快照的容器。它会查询 PagingSource 对象并存储结果。

界面层

界面层中的主要 Paging 库组件是 PagingDataAdapter