分享

JetPack系列 Paging 3.0学习

 codingSmart 2021-10-22

code小生 一个专注大前端领域的技术平台公众号回复Android加入安卓技术群

作者:柠檬茶就是力量
链接:https:///post/5ee74d216fb9a047f012853e
声明:本文来自柠檬茶就是力量投稿,转发等请联系原作者授权

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


介绍

官方文档:https://developer./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 == 0null 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(dataT)Boolean

    abstract fun areContentsTheSame(dataT)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(dataItemArticle)Boolean {
        return entity.id == data.entity.id
    }

    override fun areContentsTheSame(dataItemArticle)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地址作为参考

Github:https://github.com/Colaman0/PagingDemo

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多