code小生 一个专注大前端领域的技术平台公众号回复Android
加入安卓技术群
作者:柠檬茶就是力量
链接:https://juejin.im/post/5ee74d216fb9a047f012853e
声明:本文来自柠檬茶就是力量
投稿,转发等请联系原作者授权
上个周末晚上看到了鸿洋大神的公众号推送文章<
>,于是乎点开文章看了一下具体内容,在翻阅的过程中发现 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
等操作符处理数据
提供方法实现简单的分割线
PagingSource
首先我们需要创建一个数据源PagingSource
来给Paging
提供源数据,这里用wanAndroid的接口来做测试,网络请求用Retrofit
和协程来处理,具体相关代码就不贴在下面了
class MainSource : PagingSource() {
override suspend fun load(params: LoadParams): LoadResult {
// 如果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())
}
}
}
PagingData
和Pager
PagingData
是数据源和RecyclerView Adapter
之间的一个桥梁,每一页数据就是一个PagingData
首先创建Flow
,可以通过Pager().flow()
去实现,可以传入PagingConfig
到Pager
中去设置一些分页的参数,比如:
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
有一个方便的cachedIn()
方法,使我们可以将内容缓存Flow
在CoroutineScope
中,如果我们放在viewmodel
中,这样我们就可以在配置被更改之后,activity能够接收到原有的数据而不用重新开始获取,如果你要在flow
上用map
等操作符的时候,要确保cachedIn()
是在最后面的,如果没有缓存,那么在切换横竖屏的时候就会重新开始请求,具体效果可以修改demo里的代码测试
// 在activity中订阅pager的数据流,并且通过submitData方法把数据传给adapter
// 这里的PagingItemView是经过封装的recyclerview item,是自己封装的,下面会说到
// 正常的逻辑下可以在这里做一些过滤转换的操作,然后把数据传给adapter就可以了
lifecycleScope.launch {
viewmodel.pager.collect {
// 把数据转化成itemview然后让adapter刷新ui
adapter.submitData(it.map { ItemArticleView(it) as PagingItemView })
}
}
用Paging
库时需要recyclerview的adapter继承PagingDataAdapter
,代码和原本的自定义Adapter差别不大
这一部分加入了一点自己封装的代码,每个人写法不同,也可以直接跳过一些部分
PagingAdapter
这里我简单的封装了一下,配合PagingItemView
去使用,这样项目里就只需要一个Adapter,每个列表不同的Item逻辑可以放到PagingItmeView
里去写,也方便复用一些一样的item
class PagingAdapter(context: Context) :
PagingDataAdapter, RecyclerView.ViewHolder>(
// 首先构造方法里需要传入diffutil的callback,这是判断item是否需要更新的部分
// 相信用过diffutil的都能明白这部分代码,就不多讲了
object : DiffUtil.ItemCallback>() {
override fun areItemsTheSame(
oldItem: PagingItemView,
newItem: PagingItemView
): Boolean {
return oldItem.areItemsTheSame(newItem)
}
override fun areContentsTheSame(
oldItem: PagingItemView,
newItem: PagingItemView
): 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(@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(R.layout.item_article) {
val binding by lazy {
DataBindingUtil.bind(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.0
中Adapter
的部分跟常规的Adapter
几乎是一样的,只是改成继承PagingDataAdapter
并且要传入一个DiffUtil
的比较逻辑,在这里贴出了我自己简单封装的Adapter
,大家可以参考一下,或者自己按照喜欢的方式去写都可以。
Loadmore
Refresh
Retry
用到RecyclerView
的地方基本上都会有loadmore/refresh功能存在,Paging 3.0
很好的支持了这两个功能
Loadmore
只需要创建一个LoadStateAdapter
然后把loadmore样式的viewholder
create和bind,最后调用adapter.withLoadStateFooter
把footer这个adapter加到PagingDataAdapter
里去,这样就完成了对原有recyclerview添加loadmore页脚的功能,具体代码如下
refresh
自由发挥,demo里用了官方的下拉控件,直接调用PagingDataAdapter
的refresh()
就可以完成刷新了
retry
在demo里用在loadmore失败的时候,把进度条变成一个重试按钮,点击后尝试重新加载,只要调用PagingDataAdapter
的retry
方法就可以,非常方便
Paging 3.0
在这里还帮我们做了一些优化处理,比如上面说到的避免重复触发不同的请求,比如刷新和loadmore的交替请求,Paging
最后只会处理后一个操作,这样可以避免列表的数据错乱
// 这部分只是创建一个简单的loadmoreadapter,并且传入一个重试的回调
class LoadmoreAdapter(val retrycallback: () -> Unit) : LoadStateAdapter() {
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(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
想要监听数据获取的状态在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
PagingSource
负责提供源数据,一般是网络请求或者数据库查询,
Flow
或者说是 Pager
负责一些分页的参数设定和订阅源数据流
PagingDataAdapter
跟常规的RecyclerivewAdapter一样把数据转换成UI
LoadStateAdapter
可以添加页头/页脚,方便实现loadmore的样式
Paging 3.0
的基本用法介绍到这里就完成了,在学习的过程中能明显感觉到Paging
对于列表处理是很友好的,为我们处理了很多业务上的逻辑,代码实现起来也相对优雅友好很多。还有一些实验性的Api这次就先不写了,等以后有机会再写一篇文章来介绍。最后附上自己的Demo地址作为参考
Github:https://github.com/Colaman0/PagingDemo
1 Jetpack---Paging你知道怎样上拉加载吗?
2 JetPack + 单 Activity 的一些思路
3 JetpackNote---基于Jetpack的学习笔记APP
4 Jetpack 源码解析—LiveData的使用及工作原理
5 Android 项目中 Loading 对话框的优化
如果你有写博客的好习惯
欢迎投稿
点个在看,小生感恩❤️