孟老板 Paging3 (一) 入门
Paging3 (一) 入门
前言:
官方分页工具, 确实香. 但数据源不开放, 无法随意增删改操作; 只能借助 Room; 但列表数据不一定都要用 Room吧;
如果偏查询的分页数据用 Paging3 ; 其他一概用 老Adapter; 这倒也算个方案. [苦笑]
目录:
- 简单使用 - 数据源,Viewmodel,Adapter 等
- LoadResult - Error, Page. Error 用法等
- PagingConfig
- 监听列表加载状态
- LoadStateAdapter - loading, 加载失败, 没有更多等
- Map - 数据预处理
官方 Pagings 优势:
- 分页数据的内存中缓存。该功能可确保您的应用在处理分页数据时高效利用系统资源。
- 内置的请求重复信息删除功能,可确保您的应用高效利用网络带宽和系统资源。
- 可配置的
RecyclerView
适配器,会在用户滚动到已加载数据的末尾时自动请求数据。 - 对 Kotlin 协程和 Flow 以及
LiveData
和 RxJava 的一流支持。 - 内置对错误处理功能的支持,包括刷新和重试功能。
导包:
dependencies { val paging_version = "3.0.0" //唯一必导包 implementation("androidx.paging:paging-runtime:$paging_version") // 测试用 testImplementation("androidx.paging:paging-common:$paging_version") // optional - RxJava2 support implementation("androidx.paging:paging-rxjava2:$paging_version") // optional - RxJava3 support implementation("androidx.paging:paging-rxjava3:$paging_version") // 适配 Guava 库 - 高效java扩展库 implementation("androidx.paging:paging-guava:$paging_version") // 适配 Jetpack Compose - 代码构建View; 干掉 layout implementation("androidx.paging:paging-compose:1.0.0-alpha09") }
1.1 数据源 PagingSource
自定义数据源, 继承 PagingSource
它有两个泛型参数, 1. 页码key, 没有特殊需求的话一般就是 Int 类型; 2.集合实体类型
重写两个方法: 1.load() 加载数据的方法; 2.getRefreshKey 初始加载的页码; 暂且返回 1 或 null
LoadResult.Page 后面再讲;
class DynamicDataSource: PagingSource<Int, DynamicTwo>() { //模拟最大页码 private var maxPage = 2 //模拟数据 private fun fetchItems(startPosition: Int, pageSize: Int): MutableList<DynamicTwo> { Log.d("ppppppppppppppppppppp", "startPosition=${startPosition};;;pageSize=${pageSize}") val list: MutableList<DynamicTwo> = ArrayList() for (i in startPosition until startPosition + pageSize) { val concert = DynamicTwo() concert.title = "我是标题${i}" concert.newsInfo = "我是内容${i}" concert.nickName = "小王${i}" list.add(concert) } return list } override fun getRefreshKey(state: PagingState<Int, DynamicTwo>): Int? = null override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DynamicTwo> { val nextPageNumber = params.key ?: 1 val size = params.loadSize Log.d("ppppppppppppppppppppp", "nextPageNumber=${nextPageNumber};;;size=${size}") val response = fetchItems((nextPageNumber-1) * size, size) return LoadResult.Page( data = response, prevKey = null, // Only paging forward. 只向后加载就给 null //nextKey 下一页页码; 尾页给 null; 否则当前页码加1 nextKey = if(nextPageNumber >= maxPage) null else (nextPageNumber + 1) ) } }
1.2 ViewModel
代码比较简单. 内容我们一会再讲
class DynamicPagingModel(application: Application) : AndroidViewModel(application) { val flow = Pager( //配置 PagingConfig(pageSize = 10, prefetchDistance = 2,initialLoadSize = 10) ) { //我们自定义的数据源 DynamicDataSource() }.flow .cachedIn(viewModelScope) }
1.3 前台使用:
初始化 Adapter 及 RecycleView
mViewModel?.flow?.collectLatest 绑定监听, 然后通过 submitData() 刷新列表;
mAdapter = SimplePagingAdapter(R.layout.item_dynamic_img_two, null) mDataBind.rvRecycle.let { it.layoutManager = LinearLayoutManager(mActivity) it.adapter = mAdapter } //Activity 用 lifecycleScope //Fragments 用 viewLifecycleOwner.lifecycleScope viewLifecycleOwner.lifecycleScope.launchWhenCreated { mViewModel?.flow?.collectLatest { mAdapter.submitData(it) } }
1.4 Adapter
必须继承 paging 的 PagingDataAdapter
DiffCallback() 或 handler NewViewHolder 不了解的可以看我的 ListAdapter 封装系列
open class SimplePagingAdapter( private val layout: Int, protected val handler: BaseHandler? = null ) : PagingDataAdapter<DynamicTwo, RecyclerView.ViewHolder>(DiffCallback()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return NewViewHolder( DataBindingUtil.inflate( LayoutInflater.from(parent.context), layout, parent, false ), handler ) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { if(holder is NewViewHolder){ holder.bind(getItem(position)) } } }
over 简单的分页模拟数据已完成;
它是一个密封类; 它表示加载操作的结果;
2.1 LoadResult.Error
表示加载失败; 需提供 Throwable 对象.
public data class Error<Key : Any, Value : Any>( val throwable: Throwable ) : LoadResult<Key, Value>()
可用于:
- 异常时返回, HTTP, IO, 数据解析等异常;
- 服务器错误码响应
- 没有更多数据
2.1 LoadResult.Page
表示加载成功;
参数:
data 数据集合;
prevKey 前页页码 key; //向下一页加载 给null
nextKey 后页页码 key; //向上一页加载 给null
public data class Page<Key : Any, Value : Any> constructor(
/**
* Loaded data
*/
val data: List<Value>,
/**
* [Key] for previous page if more data can be loaded in that direction, `null`
* otherwise.
*/
val prevKey: Key?,
/**
* [Key] for next page if more data can be loaded in that direction, `null` otherwise.
*/
val nextKey: Key?,
/**
* Optional count of items before the loaded data.
*/
@IntRange(from = COUNT_UNDEFINED.toLong())
val itemsBefore: Int = COUNT_UNDEFINED,
/**
* Optional count of items after the loaded data.
*/
@IntRange(from = COUNT_UNDEFINED.toLong())
val itemsAfter: Int = COUNT_UNDEFINED
) : LoadResult<Key, Value>() {
分页配置
参数:
pageSize: 每页容量
prefetchDistance: 当RecycleView 滑动到底部时, 会自动加载下一页. 如果能提前预加载, 可以省去部分等待加载的时间.
prefetchDistance 就是距离底部提前加载的距离. 默认 = pageSize; = 0 时将不会加载更多
enablePlaceholders: 允许使用占位符. 想了解的点这里
initialLoadSize: 初始加载数量, 默认 = pageSize * 3
maxSize: 似乎意义没有那么简单. 还没看源码,不清楚; 不能 < pageSize + prefetchDistance * 2
jumpThreshold: 某阈值! 好吧我摊牌了, 我不知道. [奸笑]
LoadState: 表示加载状态密封类;
LoadState.NotLoading: 加载完毕, 并且界面也已相应更新
LoadState.Error: 加载失败.
LoadState.Loading: 正在加载..
lifecycleScope.launch { mAdapter.loadStateFlow.collectLatest { loadStates -> when(loadStates.refresh){ is LoadState.Loading -> { Log.d("pppppppppppppp", "加载中") } is LoadState.Error -> { Log.d("pppppppppppppp", "加载失败") } is LoadState.NotLoading -> { Log.d("pppppppppppppp", "完事了") } else -> { Log.d("pppppppppppppp", "这是啥啊") } } } //或者: mAdapter.addLoadStateListener { ... } }
用于直接在显示的分页数据列表中呈现加载状态。 例如: 尾部显示 正在加载, 加载失败, 没有更多等;
5.1 自定义 MyLoadStateAdapter 继承 LoadStateAdapter
重写 onCreateViewHolder, onBindViewHolder
retry: 如果加载失败, 想要重试, 则提供该高阶函数参数; 否则不需要它
class MyLoadStateAdapter( /** * 当下一页加载失败时, 继续尝试加载下一页; */ private val retry: () -> Unit ) : LoadStateAdapter<LoadStateViewHolder>() { override fun onCreateViewHolder( parent: ViewGroup, loadState: LoadState ) = LoadStateViewHolder(parent, retry) override fun onBindViewHolder( holder: LoadStateViewHolder, loadState: LoadState ) = holder.bind(loadState) }
5.2 自定义 LoadStateViewHolder
功能:
- 加载中 显示 Loading;
- 加载失败 显示 错误信息. 包括 http, IO 异常, 后台给的错误 msg 等;
- 没有更多
class LoadStateViewHolder ( parent: ViewGroup, retry: () -> Unit ) : RecyclerView.ViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.view_loading_more, parent, false) ) { private val binding = ViewLoadingMoreBinding.bind(itemView) init { //当点击重试按钮时, 调用 PagingDataAdapter 的 retry() 重新尝试加载 binding.btnLoadingRetry.setOnClickListener { retry() } } fun bind(loadState: LoadState) { // 当加载失败时. if(loadState is LoadState.Error){ // 将没有更多封装成 NoMoreException; 此时显示没有更多 View if(loadState.error is NoMoreException){ hideNoMoreUi(false) //显示 没有更多 View hideErrUi(true) //隐藏 失败 View }else{ hideNoMoreUi(true) hideErrUi(false, loadState.error.message) //显示失败 View时, 填充错误 msg } }else{ hideNoMoreUi(true) hideErrUi(true) } //加载中.. binding.pbLoadingBar.visibility = if(loadState is LoadState.Loading){ View.VISIBLE }else{ View.GONE } } /** * 隐藏没有更多View; */ private fun hideNoMoreUi(hide: Boolean){ if(hide){ binding.tvLoadingHint.visibility = View.GONE }else{ binding.tvLoadingHint.visibility = View.VISIBLE } } /** * 隐藏 加载失败View; */ private fun hideErrUi(hide: Boolean, msg: String? = null){ if(hide){ binding.tvLoadingError.visibility = View.GONE binding.btnLoadingRetry.visibility = View.GONE }else{ binding.tvLoadingError.text = msg binding.tvLoadingError.visibility = View.VISIBLE binding.btnLoadingRetry.visibility = View.VISIBLE } } }
顺便补一下 NoMoreException; 用法? 在下面 PagingSource 喽.
class NoMoreException: RuntimeException()
5.3 layout view_loading_more.xml
包含: TextView: 没有更多; ProgressBar: 加载中; TextView: 错误信息; Button: 重试按钮
<layout> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:paddingHorizontal="16dp" android:layout_width="match_parent" android:layout_height="54dp"> <View android:layout_width="match_parent" android:layout_height="1px" android:background="#e5e5e5" app:layout_constraintTop_toTopOf="parent"/> <TextView android:id="@+id/tv_loading_hint" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14sp" android:textColor="#798080" android:text="已经到底了" android:visibility="gone" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> <ProgressBar android:id="@+id/pb_loading_bar" android:layout_width="32dp" android:layout_height="32dp" android:visibility="gone" android:indeterminateTint="#7671F8" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> <TextView android:id="@+id/tv_loading_error" android:layout_width="0dp" android:layout_height="wrap_content" android:textColor="@color/shape_red" android:text="错误信息" android:layout_marginEnd="8dp" android:maxLines="2" android:ellipsize="end" android:visibility="gone" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/btn_loading_retry" app:layout_constraintStart_toStartOf="parent"/> <Button android:id="@+id/btn_loading_retry" android:layout_width="60dp" android:layout_height="38dp" android:textColor="@color/white" android:text="重试" android:visibility="gone" android:background="@drawable/shape_blue_7671f8_r8" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
5.4 PagingSource 需要根据情况 返回不同的 LoadResult
代码如下, 直接看注释就可以了;
class DynamicDataSource: PagingSource<Int, DynamicTwo>() { private var maxPage = 1 override fun getRefreshKey(state: PagingState<Int, DynamicTwo>): Int? = null override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DynamicTwo> { try { val nextPageNumber = params.key ?: 1 //超过页码时, 返回没有更多状态 NoMoreException if(nextPageNumber > maxPage){ return LoadResult.Error(NoMoreException()) } //这是 Retrofit 网络请求 val map = mapOf("page" to nextPageNumber, "pageSize" to params.loadSize) val param = ApiManager.INSTANCE.getJsonBody(map) val response = ApiManager.INSTANCE.mApi.getDynamicList(param) //后台 响应错误码时; 用 RuntimeException 返回错误信息 if(response.code != 200){ return LoadResult.Error(RuntimeException(response.msg)) } //解析响应数据 val jo = response.data val list = jo?.getAsJsonArray("newsList")?.toString()?.toBeanList<DynamicTwo>() ?: mutableListOf() maxPage = jo?.get("totalPage").toString().toInt() //返回正常数据 return LoadResult.Page( data = list, prevKey = null, // Only paging forward. 只向后加载就给null // nextKey 下一页页码; 尾页给 null; 否则当前页码加1 nextKey = nextPageNumber + 1 ) } catch (e: IOException) { // IOException for network failures. return LoadResult.Error(e) } catch (e: HttpException) { // HttpException for any non-2xx HTTP status codes. return LoadResult.Error(e) } catch (e: Exception) { // IOException for network failures. return LoadResult.Error(e) } } }
代码中 请求参数只给了 page 和 pageSize; 其他参数怎么给?
- DynamicDataSource 的构造方法传入;
- 动态参数怎么办? 写回调, 从ViewModel 中组装请求数据
- 麻烦怎么办? 创建 BaseDataSource. 将相似代码封装. 请求参数通过高阶函数从ViewModel组装;
5.5 前台使用:
首先正常初始化 Adapter, RecycleView, 并调用 mViewModel?.flow?.collectLatest
其次 RecycleView 的 adaper 不要给 主数据Adapter; 而是给 withLoadStateFooter() 返回的 ConcatAdapter
val stateAdapter = mAdapter.withLoadStateFooter(MyLoadStateAdapter(mAdapter::retry)) mDataBind.rvRecycle.let { it.layoutManager = LinearLayoutManager(mActivity) // **** 这里不要给 mAdapter(主数据 Adapter); 而是给 stateAdapter *** it.adapter = stateAdapter }
PagingDataAdapter 的 withLoadStateFooter 方法会返回一个新的 ConcatAdapter 对象; 请将这个 ConcatAdapter 设置给 RecycleView
withLoadStateFooter 的参数 就是我们自定义的 MyLoadStateAdapter; retry -> mAdapter.retry()
5.6 看一下 LoadStateAdapter 的源码;
可以发现, 这是个单条目 Adapter.
并且 只有当 LoadState.Loading, LoadState.Error 时才会出现; 当然也可以重写 displayLoadStateAsItem(), 让它所有状态都出现;
当 列表状态变化时, 会设置 loadState 参数; 动态增删改 Item;
abstract class LoadStateAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() { var loadState: LoadState = LoadState.NotLoading(endOfPaginationReached = false) set(loadState) { if (field != loadState) { val oldItem = displayLoadStateAsItem(field) val newItem = displayLoadStateAsItem(loadState) if (oldItem && !newItem) { notifyItemRemoved(0) } else if (newItem && !oldItem) { notifyItemInserted(0) } else if (oldItem && newItem) { notifyItemChanged(0) } field = loadState } } final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { return onCreateViewHolder(parent, loadState) } final override fun onBindViewHolder(holder: VH, position: Int) { onBindViewHolder(holder, loadState) } final override fun getItemViewType(position: Int): Int = getStateViewType(loadState)
//条目数量, final 不可重写; final override fun getItemCount(): Int = if (displayLoadStateAsItem(loadState)) 1 else 0 abstract fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): VH abstract fun onBindViewHolder(holder: VH, loadState: LoadState) open fun getStateViewType(loadState: LoadState): Int = 0
//只有当 Loading, Error 时, 才显示 open fun displayLoadStateAsItem(loadState: LoadState): Boolean { return loadState is LoadState.Loading || loadState is LoadState.Error } }
5.7 LoadStateAdapter 改建头尾
如果我们把它强行改造成 Header footer:
- 重写 displayLoadStateAsItem() 不管什么状态, 都返回true
- loadState 不能重写, 所以 notifyItemChanged(0) 必被调用;
- 暴力一点, 直接重写 notifyItemChanged() 让它什么都不做? 好吧 它也是 final, 不能重写
- 既然要调刷新, 那就调吧 [破涕为笑]; 那怎么办 尽量少执行无用代码呗, 那就 onBindViewHolder() 啥也不干;
- 头尾由前端控制, Adapter 只需要把这个 固定View显示就 ok 了
- 如果能阻止 notifyItemChanged(0) 那就更好了. 聪明的你有没有办法呢. [666]
最终 Adapter:
class EndViewAdapter(val v: View) : LoadStateAdapter<EndHolder>() { override fun onCreateViewHolder( parent: ViewGroup, loadState: LoadState ) = EndHolder(v) override fun onBindViewHolder(holder: EndHolder, loadState: LoadState){ //啥也不干 } override fun displayLoadStateAsItem(loadState: LoadState) = true } class EndHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
好吧, 一运行, 崩了 [捂脸]; called attach on a child which is not detached
怎么办, 取消 RecycleView 的刷新闪烁动画:
(mDataBind.rvRecycle.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false;
整个 RecycleView 的条目刷新动画都没了, 这不是个事啊! 但博主已经没办法了 [捂脸]
没办法了怎么办? 不用 Header 了? 当然不是, 我们只是不用 LoadStateAdapter 做头尾了; 我们用 ConcatAdapter 做头尾;
就是在 withLoadState... 之后, 再自己组装 ConcatAdapter
6. MAP: 数据转换; 有的时候, 我们需要对响应数据 进行预先处理;
例如: 根据条件,预先改变实体内容;
val flow: Flow<PagingData<DynamicTwo>> = Pager( PagingConfig(pageSize = 10, prefetchDistance = 2,initialLoadSize = 10) ) { DynamicDataSource() }.flow .cachedIn(viewModelScope) .map { it.map { entity -> // 这里根据条件, 预先处理数据 if(entity.isLike == 1){ entity.nickName = "变变变, 我是百变小魔女" }else{ entity.nickName = "呜哈哈哈" } entity } }
例如: 组合实体; 根据条件产生不同实体;
val flow: Flow<PagingData<GroupEntity>> = Pager( PagingConfig(pageSize = 10, prefetchDistance = 2,initialLoadSize = 10) ) { DynamicDataSource() }.flow .cachedIn(viewModelScope) .map { it.map { entity -> // 这里根据条件, 预先处理数据 if(entity.isLike == 1){ GroupEntity.DynamicTwoItem(entity) }else{ GroupEntity.DynamicItem(DynamicEntity()) } } } sealed class GroupEntity{ class DynamicTwoItem (val entity: DynamicTwo): GroupEntity() class DynamicItem (val entity: DynamicEntity): GroupEntity() }
又例如: 插入实体分隔符等
Over