Jetpack 系列之Paging3,看这一篇就够了~

class DataSource():PagingSource<Int,DemoReqData.DataBean.DatasBean>(){

override suspend fun load(params: LoadParams): LoadResult<Int, DemoReqData.DataBean.DatasBean> {

TODO(“Not yet implemented”)

}

}

我们可以看到PagingSource中有两个参数Key 和 Value,这里Key我们定义为Int类型Value DemoReqData 是接口返回数据对应的实体类,这里的意思就是

我们传Int类型的值(如页码)得到返回的数据信息DemoReqData对象。

这里需要提醒的是如果你使用的不是Kotlin 协程而是Java,则需要继承对应的PagingSource如RxPagingSource或ListenableFuturePagingSource。

DataSource为我们自动生成了load方法,我们主要的请求操作就在load方法中完成。主要代码如下所示:

override suspend fun load(params: LoadParams): LoadResult<Int, DemoReqData.DataBean.DatasBean> {

return try {

//页码未定义置为1

var currentPage = params.key ?: 1

//仓库层请求数据

var demoReqData = DataRespority().loadData(currentPage)

//当前页码 小于 总页码 页面加1

var nextPage = if (currentPage < demoReqData?.data?.pageCount ?: 0) {

currentPage + 1

} else {

//没有更多数据

null

}

if (demoReqData != null) {

LoadResult.Page(

data = demoReqData.data.datas,

prevKey = null,

nextKey = nextPage

)

} else {

LoadResult.Error(throwable = Throwable())

}

} catch (e: Exception) {

LoadResult.Error(throwable = e)

}

}

上面代码我们可以看到在datasource中我们通过DataRespority()仓库层,去请求数据,如果没有更多数据就返回null,最后使用 LoadResult.Page将结果返回,如果加载失败则用LoadResult.Error返回,由于 LoadResult.Page中的data 必须是非空类型的,所以我们需要判断返回是否为null。

接下来我们看下DataRespority仓库层的代码,代码比较简单,如下所示:

class DataRespority {

private var netWork = RetrofitService.createService(

DataApi::class.java

)

/**

  • 查询护理数据

*/

suspend fun loadData(

pageId: Int

): DemoReqData? {

return try {

netWork.getData(pageId)

} catch (e: Exception) {

//在这里处理或捕获异常

null

}

}

}

Load调用官方给出的流程如下所示:

从上图可以知道,load的方法 是我们通过Paging的配置自动触发的,不需要我们每次去调用,那么我们如何来使用DataSource呢?

调用PagingSource

The Pager object calls the load() method from the PagingSource object, providing it with the LoadParams object and receiving the LoadResult object in return.

这句话翻译过来的意思就是:Pager对象从PagingSource对象调用load()方法,为它提供LoadParams对象,并作为回报接收LoadResult对象。

所以我们在创建viewModel对象,并创建pager对象从而调用PagingSource方法 ,代码如下所示:

class MainActivityViewModel : ViewModel() {

/**

  • 获取数据

*/

fun getData() = Pager(PagingConfig(pageSize = 1)) {

DataSource()

}.flow

}

在viewmodel中我们定义了一个getData的方法,Pager中通过配置PagingConfig来实现特殊的定制,我们来看下PagingConfig中的参数如下:

pageSize:定义从 PagingSource 一次加载的项目数。

prefetchDistance:预取距离,简单解释就是 当距离底部还有多远的时候自动加载下一页,即自动调用load方法,默认值和pageSize相等

enablePlaceholders:是否显示占位符,当网络不好的时候,可以考到页面的框架,从而提升用户体验

还有一些其他参数这里就不一一介绍了,从构造方法的源码中可以看出pageSize这个参数是必填的,其他的是可选项,所以我们这里传了1

定义RecycleViewAdapter

这一步,和我们平时定义普通的RecycleViewAdapter没有太大的区别,只是我们继承的是PagingDataAdapter,主要代码如下所示:

class DataRecycleViewAdapter :

PagingDataAdapter<DemoReqData.DataBean.DatasBean, RecyclerView.ViewHolder>(object :

DiffUtil.ItemCallback<DemoReqData.DataBean.DatasBean>() {

override fun areItemsTheSame(

oldItem: DemoReqData.DataBean.DatasBean,

newItem: DemoReqData.DataBean.DatasBean

): Boolean {

return oldItem.id == newItem.id

}

@SuppressLint(“DiffUtilEquals”)

override fun areContentsTheSame(

oldItem: DemoReqData.DataBean.DatasBean,

newItem: DemoReqData.DataBean.DatasBean

): Boolean {

return oldItem == newItem

}

}) {

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {

var dataBean = getItem(position)

(holder as DataViewHolder).binding.demoReaData = dataBean

}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TestViewHolder {

return TestViewHolder(

DataBindingUtil.inflate(

LayoutInflater.from(parent.context),

R.layout.health_item_test,

parent,

false

)

)

}

inner class DataViewHolder(private val dataBindingUtil: ItemDataBinding) :

RecyclerView.ViewHolder(dataBindingUtil.root) {

var binding = dataBindingUtil

}

}

这里我们要提醒的是DiffUtil这个参数,用于计算列表中两个非空项目之间的差异的回调。无特殊情况一般都是固定写法。

View层数据请求并将结果显示在View上

到这里,基本工作已经差不多了,当然我们说的差不多了只是快能看到成果了,其中需要讲解的地方还有很多,最后一步我们在view中请求数据,并将结果绑定在adapter上

我们在View代码中调用viewModel中的getData方法,代码如下所示:

val manager = LinearLayoutManager(this)

rv_data.layoutManager = manager

rv_data.adapter = dataRecycleViewAdapter

btn_get.setOnClickListener {

lifecycleScope.launch {

mainActivityViewModel.getData().collectLatest {

dataRecycleViewAdapter.submitData(it)

}

}

}

我们在协程中调用getData方法,接收最新的数据,通过PagingAdapter的submitData方法为adapter提供数据,运行结果如下所示(忽略丑陋的UI.jpg)

当我们往下滑动时,当底部还剩1个(pageSize)数据的时候会自动加载下一页。

当然对于这个接口不需要传pageSize,所以返回的数据大小并不会受pageSize的影响,如此一来,我们就使用Paging3 完成了简单的数据分页请求。

Paging的加载状态


Paging3 为我们提供了获取Paging加载状态的方法,其中包含添加监听事件的方式以及在adapter中直接显示的方式,首先我们来看监听事件的方式

使用监听事件方式获取加载状态

上面我们在Activity中创建了dataRecycleViewAdapter来显示页面数据,我们可以使用addLoadStateListener方法添加加载状态的监听事件,如下所示:

dataRecycleViewAdapter.addLoadStateListener {

when (it.refresh) {

is LoadState.NotLoading -> {

Log.d(TAG, “is NotLoading”)

}

is LoadState.Loading -> {

Log.d(TAG, “is Loading”)

}

is LoadState.Error -> {

Log.d(TAG, “is Error”)

}

}

}

这里的it是CombinedLoadStates数据类,有refresh、Append、Prepend 区别如下表格所示:

refresh在初始化刷新的使用
append在加载更多的时候使用
prepend在当前列表头部添加数据的时候使用

也就是说如果监测的是it.refresh,当加载第二页第三页的时候,状态是监听不到的,这里只以it.refresh为例。

LoadState的值有三种,分别是NotLoading:当没有加载动作并且没有错误的时候

Loading和Error顾名思义即对应为正在加载 和加载错误的时候,监听方式除了addLoadStateListener外,还可以直接使用loadStateFlow的方式,由于flow内部是一个挂起函数 所以我们需要在协程中执行(Kotlin Flow 看这一篇 带你入门~),代码如下所示:

lifecycleScope.launch {

dataRecycleViewAdapter.loadStateFlow.collectLatest {

when (it.refresh) {

is LoadState.NotLoading -> {

}

is LoadState.Loading -> {

}

is LoadState.Error -> {

}

}

}

}

接下来我们运行上节的示例,运行成功后,点击查询按钮,将数据显示出来,我们看打印如下:

2020-11-14 16:39:19.841 23729-23729/com.example.pagingdatademo D/MainActivity: is NotLoading

2020-11-14 16:39:24.529 23729-23729/com.example.pagingdatademo D/MainActivity: 点击了查询按钮

2020-11-14 16:39:24.651 23729-23729/com.example.pagingdatademo D/MainActivity: is Loading

2020-11-14 16:39:25.292 23729-23729/com.example.pagingdatademo D/MainActivity: is NotLoading

首先是NotLoading 状态,因为我们什么都没有操作,点击了查询按钮后变成Loading状态因为正在加载数据,查询结束后再次回到了NotLoading的状态,符合我们的预期,那这个状态有什么用呢? 我们在Loading状态显示一个progressBar过渡提升用户体验等,当然最重要的还是Error状态,因为我们需要Error状态下告知用户。

我们重新打开App,断开网络连接,再次点击查询按钮,打印日志如下:

2020-11-14 16:48:25.943 26846-26846/com.example.pagingdatademo D/MainActivity: is NotLoading

2020-11-14 16:48:27.218 26846-26846/com.example.pagingdatademo D/MainActivity: 点击了查询按钮

2020-11-14 16:48:27.315 26846-26846/com.example.pagingdatademo D/MainActivity: is Loading

2020-11-14 16:48:27.322 26846-26846/com.example.pagingdatademo D/MainActivity: is Error

这里要注意的是什么呢,就是这个Error的状态,不是Paging为我们自动返回的,而是我们在DataSource中捕获异常后,使用LoadResult.Error方法告知的。

我们也需要在Error状态下监听具体的错误,无网络的话就显示无网络UI 服务器异常的话 就提示服务器异常,代码如下所示:

is LoadState.Error -> {

Log.d(TAG, “is Error:”)

when ((it.refresh as LoadState.Error).error) {

is IOException -> {

Log.d(TAG, “IOException”)

}

else -> {

Log.d(TAG, “others exception”)

}

}

}

我们在断网状态下,点击查询,日志如下所示:

2020-11-14 17:29:46.234 12512-12512/com.example.pagingdatademo D/MainActivity: 点击了查询按钮

2020-11-14 17:29:46.264 12512-12512/com.example.pagingdatademo D/MainActivity: 请求第1页

2020-11-14 17:29:46.330 12512-12512/com.example.pagingdatademo D/MainActivity: is Loading

2020-11-14 17:29:46.339 12512-12512/com.example.pagingdatademo D/MainActivity: is Error:

2020-11-14 17:29:46.339 12512-12512/com.example.pagingdatademo D/MainActivity: IOException

在adapter中显示

Paging3 为我们提供了添加底部、头部adapter的方法,分别为 withLoadStateFooter、withLoadStateHeader以及同时添加头部和尾部方法withLoadStateHeaderAndFooter,这里我们以添加尾部方法为例

首先我们创建viewHolder LoadStateViewHolder绑定布局是底部显示的布局,一个正在加载的显示以及一个重试按钮,xml布局如下所以:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=“http://schemas.android.com/apk/res/android”

xmlns:app=“http://schemas.android.com/apk/res-auto”

android:layout_width=“match_parent”

android:layout_height=“match_parent”>

<LinearLayout

android:id=“@+id/ll_loading”

android:layout_width=“match_parent”

android:layout_height=“wrap_content”

android:gravity=“center”

android:orientation=“horizontal”

android:visibility=“gone”

app:layout_constraintEnd_toEndOf=“parent”

app:layout_constraintStart_toStartOf=“parent”

app:layout_constraintTop_toTopOf=“parent”>

<TextView

android:layout_width=“wrap_content”

android:layout_height=“wrap_content”

android:text=“正在加载数据… …”

android:textSize=“18sp” />

<ProgressBar

android:layout_width=“20dp”

android:layout_height=“20dp” />

<Button

android:id=“@+id/btn_retry”

android:layout_width=“match_parent”

android:layout_height=“wrap_content”

android:text=“加载失败,重新请求”

android:visibility=“gone”

app:layout_constraintStart_toStartOf=“parent”

app:layout_constraintTop_toBottomOf=“@id/ll_loading” />

</androidx.constraintlayout.widget.ConstraintLayout>

正在加载提示和重新请求的布局默认都是隐藏,LoadStateViewHolder代码如下所示:

class LoadStateViewHolder(parent: ViewGroup, var retry: () -> Void) : RecyclerView.ViewHolder(

LayoutInflater.from(parent.context)

.inflate(R.layout.item_loadstate, parent, false)

) {

var itemLoadStateBindingUtil: ItemLoadstateBinding = ItemLoadstateBinding.bind(itemView)

fun bindState(loadState: LoadState) {

if (loadState is LoadState.Error) {

itemLoadStateBindingUtil.btnRetry.visibility = View.VISIBLE

itemLoadStateBindingUtil.btnRetry.setOnClickListener {

retry()

}

} else if (loadState is LoadState.Loading) {

itemLoadStateBindingUtil.llLoading.visibility = View.VISIBLE

}

}

}

我们这里是和Adapter分为两个类中的,所以我们要将adapter中的parent当做参数传过来,retry()是一个高阶函数,便于点击重试后,在adapter中做重试逻辑。

bindState 即为设置数据,根据State的状态来显示不同的UI。

接着我们来创建LoadStateFooterAdapter 继承自LoadStateAdapter,对应的viewHolder即为LoadStateViewHolder,代码如下所示:

class LoadStateFooterAdapter(private val retry: () -> Void) :

LoadStateAdapter() {

override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {

(holder as LoadStateViewHolder).bindState(loadState)

}

override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder {

return LoadStateViewHolder(parent, retry)

}

}

这里的代码比较简单,就不作讲解了,最后我们来添加这个adapter

rv_data.adapter =

dataRecycleViewAdapter.withLoadStateFooter(footer = LoadStateFooterAdapter(retry = {

dataRecycleViewAdapter.retry()

}))

这里要注意的是,应该把withLoadStateFooter返回的adapter设置给recyclerview,如果你是这样写:dataRecycleViewAdapter.withLoadStateFooter后 在单独设置recycleView的adapter,则会是没有效果的。

最后

答应大伙的备战金三银四,大厂面试真题来啦!

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960全网最全Android开发笔记》

《379页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

如何使用它?
1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

腾讯、字节跳动、阿里、百度等BAT大厂 2020-2021面试真题解析

资料收集不易,如果大家喜欢这篇文章,或者对你有帮助不妨多多点赞转发关注哦。文章会持续更新的。绝对干货!!!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960全网最全Android开发笔记》

[外链图片转存中…(img-wwnhf6QX-1715570823270)]

《379页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

如何使用它?
1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数

[外链图片转存中…(img-OGbyafOn-1715570823271)]

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

[外链图片转存中…(img-D5h8dlE2-1715570823272)]

腾讯、字节跳动、阿里、百度等BAT大厂 2020-2021面试真题解析

[外链图片转存中…(img-p3S5QE0V-1715570823273)]

资料收集不易,如果大家喜欢这篇文章,或者对你有帮助不妨多多点赞转发关注哦。文章会持续更新的。绝对干货!!!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 18
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值