Jetpack系列:Paging组件帮你解决分页加载实现的痛苦
相信很多小伙伴们在项目实战中,经常会用到界面的分页显示
、加载更多
等功能。需要针对具体功能做针对性开发和调试,耗时耗力。
paging组件的使用将这部分的工作简化,从而让开发者更专注于业务的具体实现。下面我们一起来学习下paging组件的使用方法。
首先来看下使用paging组件实现的分页加载和刷新效果:
下面我们针对这两个使用paging组件的例子进行分析。
- 数据库读取分页加载示例中,数据一次性获取完成,界面分页显示,按需加载数据,减少了内存资源的使用
- 网络端分页请求数据,每次请求固定长度的数据信息进行显示,减少网络带宽的使用
paging功能的实现用到了room组件,room也是jetpack库的一部分,在sqlite上提供了一个抽象层,为开发者提供了流畅的sqlite数据库访问体验。
room简介
room组件包含三个主要组成部分:
-
数据库
其应该满足四个条件:
- 含有@database注解
- 是一个继承自roomdatabase的抽象类
- 注解内包含实体的列表信息
- 包含一个返回带@dao注解类的无参方法
-
数据实体
表示数据库中表
-
dao
包含用于访问数据库的方法
应用程序使用room组件获取与数据库关联的数据访问对象或dao,然后获取实体,将实体的所有更改同步到数据库。room三个部分之间的关系如下图:
paging的基本使用方法
paging组件支持三种不同数据结构:
- 仅从网络获取
- 仅从设备数据库获取
- 两种数据来源的组合,使用设备数据库作为缓存
下面我们以仅从设备数据库获取的方式来了解下paging分页的基本使用方法。
环境配置
首先需要在模块build.gradle中添加对应库支持。
dependencies { versions.room = "2.1.0-alpha06" versions.lifecycle = "2.2.0-alpha03" versions.paging = "2.1.0-rc01" //room数据库访问依赖 implementation "androidx.room:room-runtime:$versions.room" //lifecycle组件依赖,viewmodel implementation "androidx.lifecycle:lifecycle-runtime:$versions.lifecycle" //paging组件依赖 implementation "androidx.paging:paging-runtime-ktx:$versions.paging" kapt "androidx.room:room-compiler:$versions.room" }
布局文件
界面的布局比较简单,主界面包含一个输入框,一个按钮和一个recyclerview,列表每一项的显示采用卡片式布局,显示文本。
<androidx.cardview.widget.cardview ...> <textview android:id="@+id/name" .../> </androidx.cardview.widget.cardview>
数据准备
在主activity进行数据获取和显示前,需要做几点准备工作:
- 创建数据实体类cheese
- 创建数据库方法dao
- 创建数据库cheesedb
- 创建自定义cheeseviewmodel
1. 创建实体cheese
实体代表了数据库每条数据对象,需要注意必须加@entity注解
@entity data class cheese(@primarykey(autogenerate = true) val id: int, val name: string)
此声明创建了一个数据库实体,字段有id和name,主键为id
2. 创建数据库操作方法dao
数据库方法提供了对数据库的基本操作,必须加@dao注解
@dao interface cheesedao { @query("select * from cheese order by name collate nocase asc") fun allcheesesbyname(): datasource.factory<int, cheese> @insert fun insert(cheeses: list<cheese>) @insert fun insert(cheese: cheese) @delete fun delete(cheese: cheese) }
此处提供了针对数据库的查询,插入和删除方法,可以看到在查询方法里面会指定数据源类型,当前使用默认类型。paging还支持如下三种数据源:
-
pagekeyeddatasource
实现按上下页加载显示
-
itemkeyeddatasource
根据上一条数据获取下一条数据
-
positionaldatasource
从指定位置开始加载
关于这三种数据源的高级使用方法,请参考和示例:
3. 创建数据库
数据库为界面显示提供了数据支持,当前示例程序中,数据库创建时,插入了预置数据。
必须加@database注解
必须声明数据列表信息
必须含有无参抽象方法,返回带@dao注解的类
必须为抽象类,且继承roomdatabase
@database(entities = arrayof(cheese::class), version = 1) abstract class cheesedb : roomdatabase() { abstract fun cheesedao(): cheesedao//返回dao ... //获取数据库实例,同步且单例 @synchronized fun get(context: context): cheesedb { if (instance == null) { instance = room.databasebuilder(context.applicationcontext, cheesedb::class.java, "cheesedatabase") .addcallback(object : roomdatabase.callback() { override fun oncreate(db: supportsqlitedatabase) { //数据库创建时插入预置数据 fillindb(context.applicationcontext) } }).build() } return instance!! } private fun fillindb(context: context) { // inserts in room are executed on the current thread, so we insert in the background // cheese_data为默认数据列表 iothread { get(context).cheesedao().insert( cheese_data.map { cheese(id = 0, name = it) }) } } }
4. 创建viewmodel
创建自定义viewmodel为界面和数据提供处理支持。其包含了dao,数据列表信息等。
class cheeseviewmodel(app: application) : androidviewmodel(app) { val dao = cheesedb.get(app).cheesedao() val allcheeses = dao.allcheesesbyname().tolivedata(config( pagesize = 30,//指定页面显示的数据项数量 enableplaceholders = true,//是否允许使用占位符 maxsize = 200 //一次性加载数据的最大数量 ), fetchexecutor = executor { }//自定义executor更好地控制paging库何时从应用程序的数据库中加载列表 ) fun insert(text: charsequence) = iothread { dao.insert(cheese(id = 0, name = text.tostring())) } fun remove(cheese: cheese) = iothread { dao.delete(cheese) } }
小提示:自定义viewmodel直接继承androidviewmodel,可以在其中做一些依赖于context的资源获取等功能。
public class androidviewmodel extends viewmodel { ... public <t extends application> t getapplication() { return (t) mapplication; } }
viewmodel的创建,包含了数据的获取和更新:
- 通过dao获取数据库的数据列表
- 使用livedata组件管理数据
- 增加分页支持(pagesize,enableplaceholders,maxsize)功能
- 增加自定义executor
paging组件是依赖页面长度、占位符、最大长度三个属性来进行小块数据加载显示的。
页面大小:每页显示的实体数量
最大长度:也称预取长度,此值应为pagesize的几倍大小(具体项目可根据实际情况调试)
占位符:如果设置为true,则为尚未完成加载的列表项显示占位符
占位符的使用需要有可数的数据集合,默认显示效果,数据项有相同大小的视图显示,有以下优点:
- 提供完整滚动条支持
- 无需显示加载更多项
界面绑定
数据已经准备好了,下面开始和界面进行绑定显示。
界面显示时,需要提供与recyclerview绑定的adapter,需要注意使用paging进行分页加载,adapter需要继承自pagedlistadapter。
class cheeseadapter : pagedlistadapter<cheese, cheeseviewholder>(diffcallback) { override fun onbindviewholder(holder: cheeseviewholder, position: int) { holder.bindto(getitem(position)) } override fun oncreateviewholder(parent: viewgroup, viewtype: int): cheeseviewholder = cheeseviewholder(parent) companion object { //根据diffcallback来确认新加载的数据是否与旧数据有差异,确定是否更新显示 private val diffcallback = object : diffutil.itemcallback<cheese>() { override fun areitemsthesame(olditem: cheese, newitem: cheese): boolean = olditem.id == newitem.id //kotlin使用==会将对象的内容进行对比,使用java需要重写equals方法并替换 override fun arecontentsthesame(olditem: cheese, newitem: cheese): boolean = olditem == newitem } } } //viewholder的实现比较简单,将cheese数据更新到textview class cheeseviewholder(parent :viewgroup) : recyclerview.viewholder( layoutinflater.from(parent.context).inflate(r.layout.cheese_item, parent, false)) { private val nameview = itemview.findviewbyid<textview>(r.id.name) var cheese : cheese? = null //未绑定数据,或者打开占位符后快速滑动会出现cheese为null,实际项目中需要 //处理此种情况,数据加载时会重新rebind fun bindto(cheese : cheese?) { this.cheese = cheese nameview.text = cheese?.name } }
class mainactivity : appcompatactivity() { private val viewmodel by viewmodels<cheeseviewmodel>()//创建viewmodel override fun oncreate(savedinstancestate: bundle?) { ... val adapter = cheeseadapter()//继承pagedlistadapter的类对象 cheeselist.adapter = adapter //为recyclerview添加适配器 //viewmodel数据与adapter绑定,在数据变化时通知adapter更新ui viewmodel.allcheeses.observe(this, observer(adapter::submitlist)) initswipetodelete()//设置左滑/右滑删除数据项 initaddbuttonlistener()//设置点击添加cheese功能 ... }
好了,大功告成!
你也可以尝试使用仅网络或网络+数据库的方式进行功能开发。
源码在此: