Paging3 (一)  入门

前言:

官方分页工具,  确实香.   但数据源不开放, 无法随意增删改操作;  只能借助 Room;  但列表数据不一定都要用 Room吧;

如果偏查询的分页数据用 Paging3 ;  其他一概用 老Adapter;  这倒也算个方案. [苦笑]

目录:

  1. 简单使用  -  数据源,Viewmodel,Adapter 等
  2. LoadResult  -  Error, Page.  Error 用法等
  3. PagingConfig
  4. 监听列表加载状态
  5. LoadStateAdapter  -  loading, 加载失败, 没有更多等
  6. 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.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. LoadResult

它是一个密封类;   它表示加载操作的结果;

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>() {

3.PagingConfig

分页配置

参数:

pageSize:  每页容量

prefetchDistance:  当RecycleView 滑动到底部时, 会自动加载下一页.   如果能提前预加载, 可以省去部分等待加载的时间.

        prefetchDistance 就是距离底部提前加载的距离.  默认 = pageSize;   = 0 时将不会加载更多

enablePlaceholders:  允许使用占位符.  想了解的点这里

initialLoadSize: 初始加载数量,  默认 = pageSize * 3

maxSize:   似乎意义没有那么简单.  还没看源码,不清楚;  不能 < pageSize + prefetchDistance * 2

jumpThreshold: 某阈值!  好吧我摊牌了, 我不知道. [奸笑]

4.监听加载状态:  

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. 状态适配器  LoadStateAdapter

用于直接在显示的分页数据列表中呈现加载状态。 例如:  尾部显示 正在加载, 加载失败, 没有更多等;

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;  其他参数怎么给?  

  1. DynamicDataSource 的构造方法传入;
  2. 动态参数怎么办?  写回调, 从ViewModel 中组装请求数据
  3. 麻烦怎么办?  创建 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: 

  1. 重写 displayLoadStateAsItem() 不管什么状态, 都返回true
  2. loadState 不能重写,  所以 notifyItemChanged(0) 必被调用;
  3. 暴力一点, 直接重写 notifyItemChanged() 让它什么都不做?   好吧  它也是 final, 不能重写
  4. 既然要调刷新, 那就调吧 [破涕为笑];  那怎么办 尽量少执行无用代码呗,   那就 onBindViewHolder() 啥也不干;
  5. 头尾由前端控制,  Adapter 只需要把这个 固定View显示就 ok 了
  6. 如果能阻止 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

回到顶部

最新文章

  1. mobileControls与移动控件适配
  2. 对ToString(&quot;X2 &quot;)的理解
  3. FreeMarker 学习
  4. 你真的会写单例模式吗-------Java实现
  5. 网页中插入swf动画(embed)
  6. [读书笔记]Mindset
  7. 在探索中感悟,在摸索中前进--ET之快递法
  8. iOS开发--泛型
  9. Mysql 与 php动态网站开发 入门教程
  10. c#中总是提示“在代码运行时或者在禁用“只要一个进程中断,就中断所有进程”选项时,不允许进行更改。
  11. MySQL优化 - 索引优化
  12. pandas数据结构之DataFrame操作
  13. 深入理解String类详解
  14. 正则RegExp的懒惰性和贪婪性; 分组捕获;
  15. Logging from multiple processes using log4net
  16. unigui 设置单元格颜色
  17. Java继承访问权限
  18. 质数——1到n遍历法
  19. 消息监听器无法注入bean
  20. JS获取当前时间并格式化&quot;yyyy-MM-dd HH:mm:ss&quot;

热门文章

  1. ColyseusJS 轻量级多人游戏服务器开发框架 - 中文手册(下)
  2. 一行代码解决JS数字大于2^53精度错误的问题
  3. webpack 快速入门 系列 —— 实战一
  4. qsort和sort学习与比较
  5. 笔记&#183;RCNN系相关
  6. JSX语法详解
  7. nginx负载均衡搭建phpmyadmin加入redis了解session会话原理
  8. 007.kubernets的headless service配置和ingress的简单配置
  9. AD中如何解决 同一局域网下证书冲突问题
  10. JDK5.0新特性 (Day_07)