技术不止,文章有料,加
JiuXinDev
入群,Android 搬砖路上不孤单
前言
又到了学习 Android Jetpack 的时间了,之前我已经写过了一篇《即学即用Android Jetpack - Paging》,不过Android Jetpack 一直都在更新,这不,Paging 2 已经升级到了 Paging 3,那么,大家可能有很多要关注的问题,比如:
- Paging 3 到底升级了那些东西?
- 如何使用最新的 Paging 3?
这是我本期要和大家讨论的东西。本期的最终效果:
如果想学习 Android Jetpack,可以参考我之前的文章《学习Android Jetpack? 实战和教程这里全都有!》,这已经是 《即学即用Android Jetpack》系列的第八篇啦~
本篇的源码地址:【Hoo】
目录
一、Paging 2 和 Paging 3 有什么不同?
如果你没有使用过 Paging 2,那么你可以跳过本章节(友情提醒~)。
如果你使用过 Paging 2,你会发现 Paging 3 简直是大刀阔斧,很多 API 的使用方式都变了,简单说一下主要改变的东西:
- 支持 Kotlin 中的 Flow。
- 简化数据源
PagingSource
的实现。 - 增加请求数据时状态的回调,支持设置 Header 和 Footer。
- 支持多种方式请求数据,比如网络请求和数据库请求。
二、介绍
友情提示
官方文档:Paging 3
谷歌实验室:官方教程
官方Demo:Github仓库查询Demo
1. 定义
看一下官方的定义:
The Paging library helps you load and display pages of data from a larger dataset from local storage or over network.
就是帮助你采用分页的方式解决显示包含本地数据库和网络的大数据集的问题。
2. 优点
Paging 3 的优点:
- 使用内存帮你缓存数据。
- 内置请求去重,帮助你更有效率的显示数据。
- 自动发起请求当
RecyclerView
滑到底部的时候。 - 支持 Kotlin 中的
协程
和Flow
,还有LiveData
和RxJava2
。 - 内置状态处理,包括刷新、错误、加载等状态。
3. 几个重要的类
先看一下结构:
里面几个类的作用:
-
PagingSource
:单一的数据源。 -
RemoteMediator
:其实RemoteMediator
也是单一的数据源,它会在PagingSource
没有数据的时候,再使用RemoteMediator
提供的数据,如果既存在数据库请求,又存在网络请求,通常PagingSource
用于进行数据库请求,RemoteMediator
进行网络请求。 -
PagingData
:单次分页数据的容器。 -
Pager
:用来构建Flow<PagingData>
的类,实现数据加载完成的回调。 -
PagingDataAdapter
:分页加载数据的RecyclerView
的适配器。
简述一下就是 PagingSource
和 RemoteMediator
充当数据源的角色,ViewModel
使用 Pager
中提供的 Flow<PagingData>
监听数据刷新,每当 RecyclerView
即将滚动到底部的时候,就会有新的数据的到来,最后,PagingAdapter
复杂展示数据。
三、实战
为了简单起见,先从单一的数据源开始。
第一步 引入依赖
dependencies {
def paging_version = "3.0.0-alpha08"
implementation "androidx.paging:paging-runtime:$paging_version"
// 用于测试
testImplementation "androidx.paging:paging-common:$paging_version"
// [可选] RxJava 支持
implementation "androidx.paging:paging-rxjava2:$paging_version"
// ... 其他配置不重要,具体可以查看官方文档
}
第二步 配置数据源
在 Paging 2
中,里面有三种 Page Source
,开发者使用的时候,还得去思考对应的场景,想的脑瓜子疼。
在 Paging 3
中,如果只有单一的数据源,那么你要做的很简单,继承 PagingSource
即可。
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)
}
}
}
正常用 LoadResult.Page()
返回结果,出现错误的情况用 LoadResult.Error
返回错误。
第三步 生成可观察的数据集
这里可观察数据集包括 LiveData
、Flow
以及 RxJava 中的 Observable
和 Flowable
,其中,RxJava 需要单独引入扩展库去支持的。
这里的可观察数据集 shoes
使用的是 Flow
:
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
// ... 省略
}
可以看到,我们中了的 shoes: Flow<PagingData<Shoe>>
是由 Pager().flow
提供的。Pager
中的参数稍微解释一下:
-
PagerConfig
可以提供分页的参数,比如每一页的加载数、初始加载数和最大数等。 -
pagingSourceFactory
和remoteMediator
都是数据源,我们使用其中的一个即可。
第四步 创建Adapter
和普通的 Adapter
没有特别大的区别,主要是:
- 继承
PagingDataAdapter
- 提供
DiffUtil.ItemCallback<Shoe>
实际代码:
/**
* 鞋子的适配器 配合Data Binding使用
*/
class ShoeAdapter constructor(val context: Context) :
PagingDataAdapter<Shoe, ShoeAdapter.ViewHolder>(ShoeDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ShoeRecyclerItemBinding.inflate(
LayoutInflater.from(parent.context)
, parent
, false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val shoe = getItem(position)
holder.apply {
bind(onCreateListener(shoe!!.id), shoe)
itemView.tag = shoe
}
}
/**
* Holder的点击事件
*/
private fun onCreateListener(id: Long): View.OnClickListener {
return View.OnClickListener {
val intent = Intent(context, DetailActivity::class.java)
intent.putExtra(BaseConstant.DETAIL_SHOE_ID, id)
context.startActivity(intent)
}
}
class ViewHolder(private val binding: ShoeRecyclerItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(listener: View.OnClickListener, item: Shoe) {
binding.apply {
this.listener = listener
this.shoe = item
executePendingBindings()
}
}
}
}
class ShoeDiffCallback: DiffUtil.ItemCallback<Shoe>() {
override fun areItemsTheSame(oldItem: Shoe, newItem: Shoe): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Shoe, newItem: Shoe): Boolean {
return oldItem == newItem
}
}
第五步 在UI中使用
先来简单一点的,如果只显示数据,我们要做的是:
- 创建和设置适配器。
- 开启一个协程
- 在协程中接收
Flow
提供的数据。
我的代码:
private fun onSubscribeUi(binding: ShoeFragmentBinding) {
binding.lifecycleOwner = this
// 初始化RecyclerView部分
val adapter = ShoeAdapter(context!!)
binding.recyclerView.adapter = adapter
job = viewModel.viewModelScope.launch(Dispatchers.IO) {
viewModel.shoes.collect() {
adapter.submitData(it)
}
}
// ... 省略
}
第六步 设置Header和Footer
显然 Paging 3
还有更多的功能,对!它还支持添加 Header
和 Footer
,官方示例是把它们用作上拉刷新和下拉加载更多的控件。
不过 Hoo 中并没有使用 Header
和 Footer
,我们来看看官方怎么使用的:
- 创建一个
HeaderAdapter or FooterAdapter
继承自LoadStateAdapter
,跟普通Adapter
不一样的地方在于它在onBindViewHolder
方法中提供了LoadState
参数,它可以提供当前Paging
是 Loading、NotLoading 和 Error 的状态。 - 跟普通
Adapter
一样创建自己需要的ViewHolder
。 - 稍微修改一下第五步中设置
ShoeAdapter
,调用它的withLoadStateHeaderAndFooter
方法绑定Header
和Footer
的适配器:
private fun onSubscribeUi(binding: ShoeFragmentBinding) {
binding.lifecycleOwner = this
// 初始化RecyclerView部分
val adapter = ShoeAdapter(context!!).withLoadStateHeaderAndFooter(
header = ReposLoadStateAdapter { adapter.retry() },
footer = ReposLoadStateAdapter { adapter.retry() }
)
binding.recyclerView.adapter = adapter
// ... 省略
}
需要说明一下,我这里使用的伪代码。想要看详细的教程,可以官网的这一步教程。
第七步 监听数据加载的状态
PagingDataAdapter
除了添加 Header
和 Footer
,还可以监听数据的加载状态,状态对应的类是 LoadState
,它有三种状态:
-
Loading
:数据加载中。 -
NotLoading
:内存中有已经获取的数据,即使往下滑,Paging 也不需要请求更多的数据。 -
Error
:请求数据时返回了一个错误。
监听数据状态的代码:
adapter.addLoadStateListener {state:CombinedLoadStates->
//... 状态监听
}
监听方法就是这么简单,可以看到这个 state
并不是 LoadState
,而是一个 CombinedLoadStates
,顾名思义,就是多个 LoadState
组合而成的状态类,它里面有:
-
refresh:LoadState
:刷新时的状态,因为可以调用PagingDataAdapter#refresh()
方法进行数据刷新。 -
append:LoadState
:可以理解为RecyclerView
向下滑时数据的请求状态。 -
prepend:LoadState
:可以理解为RecyclerView
向上滑时数据的请求状态。 -
source
和mediator
分别包含上面123的属性,source
代表单一的数据源,mediator
代表多数据源的场景,source
和mediator
二选一。
解释了这么多,说一下我的玩法,下拉刷新我使用了第三方的刷新控件 SmartRefreshLayout,这就意味着我要自己处理 SmartRefreshLayout
的加载状态。布局文件不贴了,就是 SmartRefreshLayout
+ RecyclerView
+ FloatingActionButton
。
成果图(Gif有一点点问题):
使用代码就是:
private fun onSubscribeUi(binding: ShoeFragmentBinding) {
binding.lifecycleOwner = this
// 初始化RecyclerView部分
val adapter = ShoeAdapter(context!!)
// 数据加载状态的回调
adapter.addLoadStateListener { state:CombinedLoadStates ->
currentStates = state.source
// 如果append没有处于加载状态,但是refreshLayout出于加载状态,refreshLayout停止加载状态
if (state.append is LoadState.NotLoading && binding.refreshLayout.isLoading) {
refreshLayout.finishLoadMore()
}
// 如果refresh没有出于加载状态,但是refreshLayout出于刷新状态,refreshLayout停止刷新
if (state.source.refresh is LoadState.NotLoading && binding.refreshLayout.isRefreshing) {
refreshLayout.finishRefresh()
}
}
binding.recyclerView.adapter = adapter
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastPos = getLastVisiblePosition(binding.recyclerView)
if (!(lastPos == adapter.itemCount - 1 && currentStates?.append is LoadState.Loading)) {
binding.refreshLayout.finishLoadMore()
}
}
})
job = viewModel.viewModelScope.launch(Dispatchers.IO) {
viewModel.shoes.collect {
adapter.submitData(it)
}
}
binding.refreshLayout.setRefreshHeader(DropBoxHeader(context))
binding.refreshLayout.setRefreshFooter(ClassicsFooter(context))
binding.refreshLayout.setOnLoadMoreListener {
// 如果当前数据已经全部加载完,就不再加载
if(currentStates?.append?.endOfPaginationReached == true)
binding.refreshLayout.finishLoadMoreWithNoMoreData()
}
//... 省略无关代码
}
到这儿,正常的流程就走通了。
四、更多
1. 使用RemoteMediator
RemoteMediator
的使用机制跟 PageSource
没什么区别,默认是先走 PageSource
的数据,如果 PageSource
提供的数据返回为空的情况,才会走 RemoteMediator
的数据,所以你可以用:
-
PageSource
:进行数据库请求提供数据。 -
RemoteMediator
:进行网络请求,然后对请求成功的数据进行数据库的存储。
鉴于使用 RemoteMediator
跟普通的方式没什么本质区别,我这里就不再做更多介绍,感兴趣的同学可以查看文档。
2. 谈谈我遇到的一些坑
之前点击品牌按钮, Paing 2
对应的版本使用的 LiveData
,这是我之前的写法:
class ShoeModel constructor(shoeRepository: ShoeRepository) : ViewModel() {
// 品牌的观察对象 默认观察所有的品牌
private val brand = MutableLiveData<String>().apply {
value = ALL
}
// 鞋子集合的观察类
val shoes: LiveData<PagedList<Shoe>> = brand.switchMap {
// Room数据库查询,只要知道返回的是LiveData<List<Shoe>>即可
if (it == ALL) {
// LivePagedListBuilder<Int,Shoe>( shoeRepository.getAllShoes(),PagedList.Config.Builder()
LivePagedListBuilder<Int, Shoe>(
CustomPageDataSourceFactory(shoeRepository) // DataSourceFactory
, PagedList.Config.Builder()
.setPageSize(10) // 分页加载的数量
.setEnablePlaceholders(false) // 当item为null是否使用PlaceHolder展示
.setInitialLoadSizeHint(10) // 预加载的数量
.build()
)
.build()
//shoeRepository.getAllShoes()
} else {
val array: Array<String> =
when (it) {
NIKE -> arrayOf("Nike", "Air Jordan")
ADIDAS -> arrayOf("Adidas")
else -> arrayOf(
"Converse", "UA"
, "ANTA"
)
}
shoeRepository.getShoesByBrand(array)
.createPagerList(6, 6)
}
}
fun setBrand(brand: String) {
this.brand.value = brand
}
companion object {
public const val ALL = "所有"
public const val NIKE = "Nike"
public const val ADIDAS = "Adidas"
public const val OTHER = "other"
}
}
切换到 Paging 3
的时候遇到一些坑:
先把可观察的数据集从 Flow
转到 LiveData
,也就是从 Pager(...).flow
到 Pager(...).livedata
:
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) }).liveData
//....
}
紧接着,我在 ShoeFragment
进行数据的监听:
job = viewModel.viewModelScope.launch() {
viewModel.shoes.observe(viewLifecycleOwner, Observer<PagingData<Shoe>> {
adapter.submitData(viewLifecycleOwner.lifecycle,it)
})
}
第一个坑出现了,在 Paging 2
中,PagingSouce
获取数据是处在子线程的,而 Paging 3
中,获取数据处在当前线程,不会做线程切换,如果直接使用数据库查询,会报错,因为 Room 中规定,数据库查询不能在主线程。
好家伙,既然主线程不可以请求数据库,那么我就把它放在IO线程,在 launch
方法中加上 Dispatchers.IO
:
job = viewModel.viewModelScope.launch(Dispatchers.IO) {
viewModel.shoes.observe(viewLifecycleOwner, Observer<PagingData<Shoe>> {
adapter.submitData(viewLifecycleOwner.lifecycle,it)
})
}
第二个坑安排上,LiveData#observe(...)
方法不能发生在后台线程。
这就没办法了,只能使用 Flow
去实现,不过 Flow
也有坑,当数据源发生变化的时候,数据需要重新监听,代码也没有了之前的优雅:
class ShoeModel constructor(private val shoeRepository: ShoeRepository) : ViewModel() {
var shoes = Pager(config = PagingConfig(
pageSize = 20
, enablePlaceholders = false
, initialLoadSize = 20
), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).flow
fun setBrand(br: String) {
if (br == ALL) {
shoes = Pager(config = PagingConfig(
pageSize = 20
, enablePlaceholders = false
, initialLoadSize = 20
), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).flow
} else {
val array: Array<String> =
when (br) {
NIKE -> arrayOf("Nike", "Air Jordan")
ADIDAS -> arrayOf("Adidas")
else -> arrayOf(
"Converse", "UA"
, "ANTA"
)
}
shoes = shoeRepository.getShoesByBrand(array).createPager(20, 20).flow
}
}
//....
}
class ShoeFragment : Fragment() {
// ... 省略
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// ... 省略
onSubscribeUi(binding)
return binding.root
}
/**
* 鞋子数据更新的通知
*/
private fun onSubscribeUi(binding: ShoeFragmentBinding) {
// ... 省略
mNike.setOnClickListener {
viewModel.setBrand(ShoeModel.NIKE)
reInitSubscribe(adapter)
shoeAnimation()
}
mAdi.setOnClickListener {
viewModel.setBrand(ShoeModel.ADIDAS)
reInitSubscribe(adapter)
shoeAnimation()
}
mOther.setOnClickListener {
viewModel.setBrand(ShoeModel.OTHER)
reInitSubscribe(adapter)
shoeAnimation()
}
}
private fun reInitSubscribe(adapter: ShoeAdapter) {
job?.cancel()
job = viewModel.viewModelScope.launch(Dispatchers.IO) {
viewModel.shoes.collect() {
adapter.submitData(it)
}
}
}
private fun setViewVisible(isShow: Boolean) {
nikeGroup.visibility = if (isShow) View.VISIBLE else View.GONE
adiGroup.visibility = if (isShow) View.VISIBLE else View.GONE
otherGroup.visibility = if (isShow) View.VISIBLE else View.GONE
}
}
期间也试了 LiveData
转 Flow
的操作符,也没起作用,可能自己研究的不深,有了解的同学可以探讨一下哈~
五、总结
相比较 Paging 2
而言,Paging 3
的功能确实更加健全了,也值得去使用。
在后续的文章中,我还将和大家探讨一下 Paging 3 的源码,当然,Paging 3 现在还处于 Alpha 阶段,并不适用于我们的生产环境,所以我还打算和大家聊聊如何更好的使用 Paging 2。