JetPack系列 Paging 3.0学习

作者: Colaman丶 | 来源:发表于2020-06-16 17:12 被阅读0次

    上个周末晚上看到了鸿洋大神的公众号推送文章<<Jetpack重磅更新>>,于是乎点开文章看了一下具体内容,在翻阅的过程中发现Paging 3.0的信息,因为以前写过旧版Paging的demo,但是当时觉得Paging并不是很好用就放弃了,所以这次更新了Paging 3.0所以第一时间到官网看一下介绍然后写了个简单的小Demo来熟悉一下这个新的Paging


    介绍

    官方文档:https://developer.android.com/topic/libraries/architecture/paging/v3-overview

    作为一个RecyclerView相关的库,Paging 3.0处理了关于数据加载,loadmore,refresh等功能,具体有以下这些功能和优点,可以减少代码中的逻辑处理和封装

    • 记录上一页和下一页的key,也就是我们常用的分页加载中的page
    • 滚动到底部的时候自动开启请求下一页的数据
    • 确保不会同时触发多个请求
    • 允许缓存数据:如果使用的是Kotlin,则可以通过CoroutineScope 来完成;如果使用的是Java,则可以用LiveData
    • 跟踪加载数据的状态,比如重试 刷新
    • 可以用map filter等操作符处理数据
    • 提供方法实现简单的分割线

    实现步骤

    1.创建数据源PagingSource

    首先我们需要创建一个数据源PagingSource来给Paging提供源数据,这里用wanAndroid的接口来做测试,网络请求用Retrofit和协程来处理,具体相关代码就不贴在下面了

    class MainSource : PagingSource<Int, ArticleEntity>() {
        override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ArticleEntity> {
            // 如果key是null,那就加载第0页的数据
            val page = params.key ?: 0
            return try {
                // 这里获取文章列表的response
                val response = Api.getHomeArticles(page)
                LogUtils.d("加载第$page 页")
                // 如果成功加载,那么返回一个LoadResult.Page,如果失败就返回一个Error
                // Page里传进列表数据,以及上一页和下一页的页数,具体的是否最后一页或者其他逻辑就自行判断
                // 需要注意的是,如果是第一页,prevKey就传null,如果是最后一页那么nextKey也传null
                // 其他情况prevKey就是page-1,nextKey就是page+1
                LoadResult.Page(
                    data = response.pageData(),
                    prevKey = if (page == 0) null else page - 1,
                    nextKey = if (response.isLastPage()) null else page + 1
                )
            } catch (e: Exception) {
                // 捕获异常,返回一个Error
                LoadResult.Error(e.toKError())
            }
        }
    }
    

    2. PagingDataPager

    PagingData是数据源和RecyclerView Adapter之间的一个桥梁,每一页数据就是一个PagingData
    首先创建Flow<PagingData<XXX>>,可以通过Pager().flow()去实现,可以传入PagingConfigPager中去设置一些分页的参数,比如:

    • pageSize : 每一页的数据量
    • prefetchDistance : 预取数据的距离,也就是距离最后一个item多远时开始加载下一页数据,默认是一页的数据量ppagesize,也就是说你获取到N页数据之后会自动开始获取第N+1页的数据,如果设置为0那么loadmore的效果就会消失
    • initialLoadSize : 初始化加载的数量,默认为pagesize * 3
     Pager(
        config = PagingConfig(pageSize = 20, prefetchDistance = 1),
        // 这里的source就是上面的MainSource对象
        pagingSourceFactory = { source }).flow.cachedIn(viewModelScope)
    

    Flow<PagingData>有一个方便的cachedIn()方法,使我们可以将内容缓存Flow<PagingData>CoroutineScope中,如果我们放在viewmodel中,这样我们就可以在配置被更改之后,activity能够接收到原有的数据而不用重新开始获取,如果你要在flow上用map等操作符的时候,要确保cachedIn()是在最后面的,如果没有缓存,那么在切换横竖屏的时候就会重新开始请求,具体效果可以修改demo里的代码测试

    3. 接收数据流

        // 在activity中订阅pager的数据流,并且通过submitData方法把数据传给adapter
        // 这里的PagingItemView是经过封装的recyclerview item,是自己封装的,下面会说到
        // 正常的逻辑下可以在这里做一些过滤转换的操作,然后把数据传给adapter就可以了
        lifecycleScope.launch {
            viewmodel.pager.collect {
            // 把数据转化成itemview然后让adapter刷新ui
            adapter.submitData(it.map { ItemArticleView(it) as PagingItemView<Any> })
            }
        }
    

    4. PagingDataAdapter

    Paging库时需要recyclerview的adapter继承PagingDataAdapter,代码和原本的自定义Adapter差别不大

    这一部分加入了一点自己封装的代码,每个人写法不同,也可以直接跳过一些部分

    • PagingAdapter 这里我简单的封装了一下,配合PagingItemView去使用,这样项目里就只需要一个Adapter,每个列表不同的Item逻辑可以放到PagingItmeView里去写,也方便复用一些一样的item
    class PagingAdapter(context: Context) :
        PagingDataAdapter<PagingItemView<Any>, RecyclerView.ViewHolder>(
         // 首先构造方法里需要传入diffutil的callback,这是判断item是否需要更新的部分
        // 相信用过diffutil的都能明白这部分代码,就不多讲了
            object : DiffUtil.ItemCallback<PagingItemView<Any>>() {
                override fun areItemsTheSame(
                    oldItem: PagingItemView<Any>,
                    newItem: PagingItemView<Any>
                ): Boolean {
                    return oldItem.areItemsTheSame(newItem)
                }
    
                override fun areContentsTheSame(
                    oldItem: PagingItemView<Any>,
                    newItem: PagingItemView<Any>
                ): Boolean {
                    return oldItem.areContentsTheSame(newItem)
                }
            }
        ) {
    
        val layoutInflater by lazy {
            LayoutInflater.from(context)
        }
        
        // 获取到对应的itemview去调用onBindView方法设置UI
        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            if (position != RecyclerView.NO_POSITION) {
                getItem(position)?.onBindView(holder = holder as PagingVHolder, position = position)
            }
        }
    
        // 这里用itemView的layoutRes去作为viewtype,这样不同布局的itemview就可以区分开来
        override fun getItemViewType(position: Int): Int {
            return getItem(position)!!.layoutRes
        }
    
        // 因为上面是用layoutRes来作为itemType,所以创建viewholder的时候直接用viewType来创建
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
            val holder = PagingVHolder(layoutInflater.inflate(viewType, parent, false))
            return holder
        }
    }
    
    • 然后是PagingItemView 这里是把recyclerview的每一个item 都作为一个PagingItemView,这样对应item的逻辑可以写在各自的PagingItemView,adapter里就不需要写什么逻辑了,我一般会配合databinding一起使用,
    abstract class PagingItemView<T : Any>(@LayoutRes val layoutRes: Int) {
        lateinit var holder: PagingVHolder
        
        // adapter执行到bindView的时候会调用,可以通过holder去获取view更新UI,也可以直接用databinding
        open fun onBindView(holder: PagingVHolder, position: Int) {
            this.holder = holder
        }
    
        abstract fun areItemsTheSame(data: T): Boolean
    
        abstract fun areContentsTheSame(data: T): Boolean
    }
    
    // 把每一篇文章每一个item作为一个PagingItemView,传入对应的文章entity,然后比较旧数据和新数据是否相同,这里简单比较一下id,具体项目中自己去决定
    // 配合databinding用的时候,如果只是绑定一些简单的数据显示,代码量可以减少很多
    class ItemArticle(val entity: ArticleEntity) :
        PagingItemView<ItemArticle>(R.layout.item_article) {
    
        val binding by lazy {
            DataBindingUtil.bind<ItemArticleBinding>(holder.itemView)
        }
    
        override fun areItemsTheSame(data: ItemArticle): Boolean {
            return entity.id == data.entity.id
        }
    
        override fun areContentsTheSame(data: ItemArticle): Boolean {
            return entity.id == data.entity.id
        }
    
        override fun onBindView(holder: PagingVHolder, position: Int) {
            super.onBindView(holder, position)
            // 设置文章标题
            binding?.tvTitle?.text = entity.title
        }
    }
    

    实际上在Paging 3.0Adapter的部分跟常规的Adapter几乎是一样的,只是改成继承PagingDataAdapter并且要传入一个DiffUtil的比较逻辑,在这里贴出了我自己简单封装的Adapter,大家可以参考一下,或者自己按照喜欢的方式去写都可以。

    5. Loadmore Refresh Retry

    用到RecyclerView的地方基本上都会有loadmore/refresh功能存在,Paging 3.0很好的支持了这两个功能

    • Loadmore只需要创建一个LoadStateAdapter 然后把loadmore样式的viewholdercreate和bind,最后调用adapter.withLoadStateFooter把footer这个adapter加到PagingDataAdapter里去,这样就完成了对原有recyclerview添加loadmore页脚的功能,具体代码如下
    • refresh自由发挥,demo里用了官方的下拉控件,直接调用PagingDataAdapterrefresh()就可以完成刷新了
    • retry 在demo里用在loadmore失败的时候,把进度条变成一个重试按钮,点击后尝试重新加载,只要调用PagingDataAdapterretry方法就可以,非常方便

    Paging 3.0在这里还帮我们做了一些优化处理,比如上面说到的避免重复触发不同的请求,比如刷新和loadmore的交替请求,Paging最后只会处理后一个操作,这样可以避免列表的数据错乱

    
    // 这部分只是创建一个简单的loadmoreadapter,并且传入一个重试的回调
    class LoadmoreAdapter(val retrycallback: () -> Unit) : LoadStateAdapter<LoadmoreView>() {
        override fun onBindViewHolder(holder: LoadmoreView, loadState: LoadState) {
            holder.bindState(loadState)
        }
    
        override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadmoreView {
            return LoadmoreView(parent, loadState,retrycallback)
        }
    
    }
    
    // 这部分是一个底部loadmore的item,包含一个进度条以及重试按钮
    class LoadmoreView(
        parent: ViewGroup,
        loadState: LoadState,
        val retrycallback: () -> Unit
    ) :
        RecyclerView.ViewHolder(
            LayoutInflater.from(parent.context)!!.inflate(R.layout.item_loadmore, parent, false)
        ) {
    
        val loading = ObservableBoolean()
    
    
        // 通过databinding来控制UI,具体可以查看demo的xml
        init {
            DataBindingUtil.bind<ItemLoadmoreBinding>(itemView)?.itemview = this
            bindState(loadState)
        }
    
        fun bindState(loadState: LoadState) {
            loading.set(loadState is LoadState.Loading)
        }
    }
    
    

    PagingDataAdapter中有三个添加页头页脚的方法

    • withLoadStateHeader 添加页脚,可以用于loadmore
    • withLoadStateHeaderAndFooter 可以添加页头/页脚
    • withLoadStateFooter 添加页头

    这里需要注意的是,具体页头页脚的实现方式也是创建一个ViewHolder然后放到LoadStateAdapter中去,我们常见的底部loadmore就是添加页脚,但是这里的Header不是我们项目中在列表最顶部添加一个item的意思,而是和loadmore类似的概念。
    也就是说如果我们添加了一个页头,那么只有在PagingSource中返回LoadResult.Page的时候prevKey不为null才会显示出来,所以如果我们从第一页开始加载是看不到这个Header的,如果我们一开始加载的页数是第5页,那么我们在往上滑动的时候,才能看到我们的Header

    6.监听状态

    想要监听数据获取的状态在PagingDataAdapter里有两个方法

    • addDataRefreshListener 这个方法是当新的PagingData被提交并且显示的回调
    • addLoadStateListener这个相较于上面那个比较复杂,listener中的回调会返回一个CombinedLoadStates对象
    data class CombinedLoadStates(
        /**
         * [LoadStates] corresponding to loads from a [PagingSource].
         */
        val source: LoadStates,
    
        /**
         * [LoadStates] corresponding to loads from a [RemoteMediator], or `null` if RemoteMediator
         * not present.
         */
        val mediator: LoadStates? = null
    ) {
        val refresh: LoadState = (mediator ?: source).refresh
        val prepend: LoadState = (mediator ?: source).prepend
        val append: LoadState = (mediator ?: source).append
    ...
    }
    
    • refresh: 对应LoadType.REFRESH也就是我们初始化/刷新数据的类型
    • prepend: 对应LoadType.PREPEND也就是上面提到的要往当前列表的头部添加数据
    • append: 对应LoadType.APPEND 往列表底部添加数据,也就是loadmore

    上面三种加载类型对应的加载状态用LoadState来表示,分别有 NotLoading Loading Error 看类名就很好理解,然后我们需要结合起来去判断当前的加载类型以及状态,这里我做一些简单的判断处理,大家具体在项目中可以自由发挥

    adapter.addLoadStateListener { loadState ->
        // 这里的逻辑可以自由发挥,我这里用了自己写的statuslayout库去管理页面状态
        // 错误和loading的时候对应去切换状态的UI,还可以做全局设置,这样不同页面也只需要设置一次就够了
        when (loadState.refresh) {
            is LoadState.Error -> statuslayout.switchLayout(StatusLayout.STATUS_ERROR)
            is LoadState.Loading -> {
                statuslayout.showDefaultContent()
                refreshlayout.isRefreshing = true
            }
            is LoadState.NotLoading -> {
                statuslayout.showDefaultContent()
                refreshlayout.isRefreshing = false
            }
        }
    }
    

    这里一般用来做RecyclerView的初始化loading,失败,成功这几种状态的页面切换。在项目里我会用自己之前写的StatusLayout来处理,这样项目里不同页面的列表状态UI都可以很方便的统一处理

    这是StatusLayout的github地址,https://github.com/Colaman0/StatusLayout
    大家可以试试看配合使用,如果觉得有用的话可以给个star

    7.总结

    • PagingSource负责提供源数据,一般是网络请求或者数据库查询,
    • Flow<PagingData<Value>> 或者说是 Pager负责一些分页的参数设定和订阅源数据流
    • PagingDataAdapter 跟常规的RecyclerivewAdapter一样把数据转换成UI
    • LoadStateAdapter 可以添加页头/页脚,方便实现loadmore的样式

    Paging 3.0的基本用法介绍到这里就完成了,在学习的过程中能明显感觉到Paging对于列表处理是很友好的,为我们处理了很多业务上的逻辑,代码实现起来也相对优雅友好很多。还有一些实验性的Api这次就先不写了,等以后有机会再写一篇文章来介绍。最后附上自己的Demo地址作为参考

    https://github.com/Colaman0/PagingDemo

    相关文章

      网友评论

        本文标题:JetPack系列 Paging 3.0学习

        本文链接:https://www.haomeiwen.com/subject/qzurxktx.html