JetPack 之 Paging3.0 简单上手指南!

作者:Chsmy

之前有一篇Paging2.x的使用和分析,Paging2.x运行起来的效果无限滑动还挺不错的,不过代码写起来有点麻烦,功能也不是太完善,比如下拉刷新的方法都没有提供,我们还得自己去调用DataSource#invalidate()方法重置数据来实现。最近google出了3.0的测试版,功能更加强大,用起来更简单,现在来开始尝试一把。

先看看官网对Paging3.0的功能介绍

  • 分页数据缓存到内存中,保证应用在处理页面数据的时候,更有效的使用系统资源
  • 同时多个相同的请求只会触发一个,确保App有效的使用网络资源和系统资源
  • 可以配置RecyclerView的adapters,让其滑动到末尾自动发起请求
  • 对Kotlin协程和Flow以及LiveData、RxJava 有很好的支持
  • 内置刷新、重试、错误处理等功能

开始使用,首先引入依赖库

def paging_version = "3.0.0-alpha02"
implementation "androidx.paging:paging-runtime:$paging_version"

配置一个RecyclerView,主要需要两个部分一个是Adapter,一个是数据。先从Adapter开始

构建Adapter

Adapter的创建跟Paging2.x写法差不多,不过继承的类不一样了,Paging2.x继承的是PagedListAdapter,在3.0中PagedListAdapter已经没有了,需要继承PagingDataAdapter

class ArticleAdapter : PagingDataAdapter<Article,ArticleViewHolder>(POST_COMPARATOR){

    companion object{
        val POST_COMPARATOR = object : DiffUtil.ItemCallback<Article>() {
            override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean =
                    oldItem == newItem

            override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean =
                    oldItem.id == newItem.id
        }
    }

    override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
         holder.tvName.text = getItem(position)?.title
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
        return ArticleViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_item,parent,false))
    }

}
class ArticleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
   val tvName: TextView = itemView.findViewById(R.id.tvname)
}

写法跟写正常的RecyclerView.Adapter基本一样,就加了一样东西,需要在构造方法里传入一个DiffUtil.ItemCallback用来确定差量更新的时候的计算规则。

Adapter写完了,下面就是数据了,我们使用Retrofit和kotlin协程从网络获取数据之后将数据设置给Adapter

获取数据并设置给Adapter

从官网上来看,google提倡我使用三层架构来完成数据到Adapter的设置,比如官网上的下图

第一层 数据仓库层Repository

Repository层主要使用PagingSource这个分页组件来实现,每个PagingSource对象都对应一个数据源,以及该如何从该数据源中查找数据。PagingSource可以从任何单个数据源比如网络或者数据库中查找数据。

Repository层还有另一个分页组件可以使用RemoteMediator,它是一个分层数据源,比如有本地数据库缓存的网络数据源。

下面创建我们的PagingSource和Repository

class ArticleDataSource:PagingSource<Int,Article>() {

    /**
     * 实现这个方法来触发异步加载(例如从数据库或网络)。 这是一个suspend挂起函数,可以很方便的使用协程异步加载
     */
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {

        return try {
            val page = params.key?:0
            //获取网络数据
            val result = WanRetrofitClient.service.getHomeList(page)
            LoadResult.Page(
                    //需要加载的数据
                    data = result.data.datas,
                    //如果可以往上加载更多就设置该参数,否则不设置
                    prevKey = null,
                    //加载下一页的key 如果传null就说明到底了
                    nextKey = if(result.data.curPage==result.data.pageCount) null else page+1
            )
        }catch (e:Exception){
            LoadResult.Error(e)
        }

    }
}
  • 继承PagingSource,需要两个泛型,第一个表示下一页数据的加载方式,比如使用页码加载可以传Int,使用最后一条数据的某个属性来加载下一页就传别的类型比如String等
  • 实现其load方法来触发异步加载,可以看到它是一个用suspend修饰的挂起函数,可以很方便的使用协程异步加载。
  • 其参数LoadParams中有一个key值,我们可以拿出来用于加载下一页。
  • 返回值是一个LoadResult,出现异常调用LoadResult.Error(e),正常强开情况下调用LoadResult.Page方法来设置从网络或者数据库获取到的数据
  • prevKey 和 nextKey 分别代表下次向上加载或者向下加载的时候需要提供的加载因子,比如我们通过page的不断增加来加载每一页的数据,nextKey就可以传入下一页page+1。如果设置为null的话说明没有数据了。

创建Repository

class ArticleRepository {

    fun getArticleData() = Pager(PagingConfig(pageSize = 20)){
        ArticleDataSource()
    }.flow

}

代码虽少不过有两个重要的对象:Pager 和 PagingData

  • Pager是进入分页的主要入口,它需要4个参数:PagingConfig、Key、RemoteMediator、PagingSource其中第一个和第四个是必填的。
  • PagingConfig用来配置加载的时候的一些属性,比如多少条算一页,距离底部多远的时候开始加载下一页,初始加载的条数等等。
  • PagingData 用来存储每次分页数据获取的结果
  • flow是kotlin的异步数据流,点类似 RxJava 的 Observable

第二层ViewModel层

Repository最终返回一个异步流包裹的PagingDataFlow<PagingData>,PagingData存储了数据结果,最终可以使用它将数据跟UI界面关联。

ViewModel中一般都使用LiveData来跟UI层交互,Flow的扩展函数可以直接转换成一个LiveData可观察对象。

class PagingViewModel:ViewModel() {

    private val repository:ArticleRepository by lazy { ArticleRepository() }
    /**
     * Pager 分页入口 每个PagingData代表一页数据 最后调用asLiveData将结果转化为一个可监听的LiveData
     */
    fun getArticleData() = repository.getArticleData().asLiveData()

}

UI层

UI层其实就是到了我们的Activity中,给RecycleView设置Adapter,给Adater设置数据

class PagingActivity : AppCompatActivity() {

    private val viewModel by lazy { ViewModelProvider(this).get(PagingViewModel::class.java) }

    private val adapter: ArticleAdapter by lazy { ArticleAdapter() }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_paging)

        val refreshView:SmartRefreshLayout = findViewById(R.id.refreshView)
        val recyclerView :RecyclerView = findViewById(R.id.recyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = adapter.withLoadStateFooter(PostsLoadStateAdapter(adapter))
        //获取数据并渲染UI
        viewModel.getArticleData().observe(this, Observer {
            lifecycleScope.launchWhenCreated {
                adapter.submitData(it)
            }
        })
        //监听刷新状态当刷新完成之后关闭刷新
        lifecycleScope.launchWhenCreated {
            @OptIn(ExperimentalCoroutinesApi::class)
            adapter.loadStateFlow.collectLatest {
                if(it.refresh !is LoadState.Loading){
                    refreshView.finishRefresh()
                }
            }
        }
        refreshView.setOnRefreshListener {
            adapter.refresh()
        }
    }
}
  • 创建出前面写的Adapter的实例,并设置给RecyclerView
  • 调用viewModel.getArticleData()方法获取LiveData并监听返回数据
  • 调用adapter的submitData方法来触发页面的渲染。这个方法是一个suspend修饰的挂起方法,所以将它放到一个有生命周期的协程中调用。如果不想放到协程中可以调用另外一个两个参数的方法adapter.submitData(lifecycle,it)传入lifecycle就行了

刷新和重试

Paging3.0中调用刷新的方法比Paging2.x中方便多了,直接就提供了刷新的方法,并且还提供了加载数据出错后的重试方法。

前面的activity代码中,在下拉刷新控制的下拉监听中直接调用adapter.refresh()方法就可以完成刷新了,那什么时候关闭刷新动画呢,需要调用adapter.loadStateFlow.collectLatest方法来监听

 lifecycleScope.launchWhenCreated {
            @OptIn(ExperimentalCoroutinesApi::class)
            adapter.loadStateFlow.collectLatest {
                if(it.refresh !is LoadState.Loading){
                    refreshView.finishRefresh()
                }
            }
        }

收集流的状态,如果是不是Loading状态的说明加载完成了,可以关闭动画了。

PagingDataAdapter可以设置头部和底部的加载进度或者加载出错时候的布局,这样当处于加载中的状态的时候,可以显示加载动画,加载出错的时候可以显示出重试的按钮。用起来也简单舒服。

需要自定义一个Adapter继承自LoadStateAdapter,并将这个Adapter设置给最开始adapter就可以了

class PostsLoadStateAdapter(
        private val adapter: ArticleAdapter
) : LoadStateAdapter<NetworkStateItemViewHolder>() {
    override fun onBindViewHolder(holder: NetworkStateItemViewHolder, loadState: LoadState) {
        holder.bindTo(loadState)
    }

    override fun onCreateViewHolder(
            parent: ViewGroup,
            loadState: LoadState
    ): NetworkStateItemViewHolder {
        return NetworkStateItemViewHolder(parent) { adapter.retry() }
    }
}

ViewHolder

class NetworkStateItemViewHolder(
    parent: ViewGroup,
    private val retryCallback: () -> Unit
) : RecyclerView.ViewHolder(
    LayoutInflater.from(parent.context).inflate(R.layout.network_state_item, parent, false)
) {
    private val progressBar = itemView.findViewById<ProgressBar>(R.id.progress_bar)
    private val errorMsg = itemView.findViewById<TextView>(R.id.error_msg)
    private val retry = itemView.findViewById<Button>(R.id.retry_button)
        .also {
            it.setOnClickListener { retryCallback() }
        }

    fun bindTo(loadState: LoadState) {
        progressBar.isVisible = loadState is Loading
        retry.isVisible = loadState is Error
        errorMsg.isVisible = !(loadState as? Error)?.error?.message.isNullOrBlank()
        errorMsg.text = (loadState as? Error)?.error?.message
    }
}

LoadState有三种:NotLoading、Loading、Error,我们可以根据不同的状态来改变底部或者顶部的布局样式

最后在activity中设置,下面添加一个底部布局

recyclerView.adapter = adapter.withLoadStateFooter(PostsLoadStateAdapter(adapter))

直接调用adapter的相关方法就可以了,总共三个方法,添加底部,添加头部,两个都添加。

Paging3.0的简单使用到此完成 效果如下:

如果你对新技术比较感兴趣,欢迎关注本号,后续会推送更详细的实践相关文章。

最.最.最.最后,在这里我也分享自己收录整理的Android学习PDF,里面对Jetpack有详细的讲解,希望可以帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,可以分享给身边好友一起学习

如果你有需要的话,可以 点这领取

热门文章

暂无图片
编程学习 ·

服务器使用Nginx部署Springboot项目(jar包)

部署SpringBoot项目到后台Nginx实现多项目反向代理1,将java项目打成jar包2.准备工具3.将jar包传入服务器3.使用Xshell运行jar包4.下载安装nginx5.配置nginx.conf6通过域名访问(成功) 1,将java项目打成jar包 这里我用到的是maven工具这里有两个项目,打包完成后一个为demo.jar,另…
暂无图片
编程学习 ·

Android 人民币符号在布局中实现的效果不一样的处理方法

大致效果图如下图1 2 这2个都是在java代码中 人民币符号+ 金额 以前没怎么在意ui走查的时候提出来的bug看了半天才发现问题 就是一个是是自己手打的的人民币符号,一个是从ui的效果图上复制过来的人民币符号最后自己的处理方法就是复制ui效果图的人民币符号,大致原因也知道就…
暂无图片
编程学习 ·

springboot+Netty搭建web服务器实现物联网温湿度采集

前言:这段时间做了一个课程设计,内容是将温湿度传感器采集到的温湿度数据上传到web服务器并以表格或者折线图的方式可视化展示出来。 话不多说:上代码 ①Netty服务器搭建 NettyServer.java /*** @author cx* @Time 2020/6/29 22:00* @Description netty 服务器配置*/ public…
暂无图片
编程学习 ·

JVM GC原理

了解JVM GC原理非常重要,对于系统调优非常有用。如果一个系统频繁发生FULL GC,那么会造成系统响应卡顿,更严重的时候会导致系统崩溃。JVM的内存空间 JVM的内存空间,从大的层面上来分析包含:新生代空间(Young)和老年代空间(Old)。新生代空间(Young)又被分为2个部分(Ed…
暂无图片
编程学习 ·

EasyNTS上云网关内配置EasyNVR云终端成功后显示设备离线问题解决

之前为大家简单介绍过EasyNTS上云网关,一部分是为了进行网络穿透而产生的设备,目前有部分用户已经在进行了内测,我们也会收集一些内测用户的问题,集中排查解决,以达到更好的使用效果。有的用户在EasyNTS上云网关内进行流媒体终端EasyNVR硬件配置测试时,设备已经配置完成,…
暂无图片
编程学习 ·

Mybatis多数据源配置

pom.xml <!-- Spring Boot Mybatis 依赖 --> <dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.2.0</version> </dependency><!-- MySQL 连…
暂无图片
编程学习 ·

面试常问的22个Linux命令

1.查找文件find / -name filename.txt根据名称查找/目录下的filename.txt文件。2.查看一个程序是否运行ps –ef|grep tomcat查看所有有关tomcat的进程3.终止线程kill -9 19979终止线程号位19979的线程4.查看文件,包含隐藏文件ls -al5.当前工作目录pwd6.复制文件包括其子文件到…
暂无图片
编程学习 ·

关于JavaScript的的高速缓存未命中分析【云图智联】

免费学习视频欢迎关注云图智联:https://e.yuntuzhilian.com/在本文中,我们将讨论创建和访问数据的方式可能对应用程序性能的影响。介绍JavaScript是一种非常高级的语言,在使用JavaScript开发的时候不必对存储器中的数据存储方式作过多的考虑。在本文中,我们将探讨数据如何存…
暂无图片
编程学习 ·

前端React实现fetch取消、中止请求

场景: 项目开发过程中有时会遇到这种情况:两次查询请求相隔时间很短时,由于接口异步,第一次请求可能会覆盖第二次请求返回数据,所以需要在第二次请求前先将第一次请求中止,话不多说,实现如下: 关于axios取消请求网上有很多,可自信百度,本文主要针对于fetch请求,由于…
暂无图片
编程学习 ·

使用mimikatz获取机器的RDP凭据

当我们拿到了机器的管理员权限后,想获取其RDP凭据保存的密码。那么,该如何操作呢?#查看mstsc的连接纪录 cmdkey /list #查找本地的Credentials: dir /a %userprofile%\AppData\Local\Microsoft\Credentials\*上传mimikatz,执行以下命令:
暂无图片
编程学习 ·

学习node.js前,浏览器的一些工作原理知识的补充

浏览器概述 1、人机交互(UI) 2、网络请求部分(Socket) 3、JavaScript引擎(解析执行JavaScript) 4、渲染引擎(渲染HTML,CSS)又叫排版引擎或浏览器内核 5、数据库存储(cookie、HTML5的本地存储Localstorage、SessionStorage)渲染引擎 主流的渲染引擎有 Chrome浏览器:…
暂无图片
编程学习 ·

注释,变量

1.注释 注释就是对代码的解释(notepad++里ctrl+q, pycharm里ctrl+/) 1.1 注释的分类:(1)单行注释 #(2)多行注释 ‘’’ 或 ‘’’’’’ 注意嵌套! 1.2 注释的功能:注释有排错的功能,包裹一部分,看是否报错,循环找出错误. 2 . 变量 变量就是可以改变的量,实际指代的是内…
暂无图片
编程学习 ·

MyBatis中#{}和${}的区别详解 区别

区别1.#将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。如:order by #user_id#,如果传入的值是111,那么解析成sql时的值为order by "111", 如果传入的值是id,则解析成的sql为order by "id".2.将传入的数据直接显示生成在sql中。如:or…
暂无图片
编程学习 ·

ESP32使用MicroPython快速开发

Python基本语句一:Print语句:1. 输出字符串和数字>>>print("runoob") # 输出字符串runoob>>> print(100) # 输出数字100>>> str = runoob>>> print(str) # 输出变量runoob>>> L = [1,2,a] …
暂无图片
编程学习 ·

React基本使用 - props与state、React事件

1、props 父组件传过来的参数 可以使用组件类的defaultProps属性,设置默认的props值 // Actor.js import React,{Component} from react; export default class Actor extends Component{render(){return (<div>hi, {this.props.name} - {this.props.works}</div>…
暂无图片
编程学习 ·

python_ask_02-Are object and instance the same?

ID = python_ask_02 文章目录QuestionAnswerReference Question Are object and instance the same? Answer Are they the same? My answer is NO. We say everything is object, but we never say everything is instance. Class and instance are both object.「对象」是一…
暂无图片
编程学习 ·

jsoncpp的安装与配置

某些项目需要jsoncpp库,那么本文将介绍Ubuntu下安装jsoncpp库具体步骤。安装jsoncpp前必须安装scons。1.scons下载地址:https://sourceforge.net/projects/scons 可以选择对应的版本下载2.jsoncpp下载地址:http://sourceforge.net/projects/jsoncpp/files/3.解压scons-3.1.2…
暂无图片
编程学习 ·

从 Android 源码分析自定义 View 相关知识点

以下源码来自于 Android P。onMeasure()MeasureSpecMeasureSpec 是 View 里的一个内部类,其用来表示 View 的测量模式和测量大小,代码如下:public static class MeasureSpec {/*** Creates a measure specification based on the supplied size and mode.** The mode must a…