「Jetpack - Paging3使用」

一、前言

Paging3,分页加载库,基于Paging2的基础上做了很大的改动,可以说完全是两个库,刚好现有的项目也用到了Paging2,可以说是痛并快乐着。而Paging3依然没有处理呼声最高的两个“需求”:局部增删的实现。当然到第三个版本仍然没有改动,Google肯定是有着自己的思考,这里是IssueTracker。

二、Paging优势
  • 内置对错误处理功能,重试、刷新等。
  • Kotlin协程和流Flow提供了一流的支持。
  • 对于结合RecyclerView使用时,自带自动请求下一页,也就是分页功能,可以说是丝滑。
  • 内置请求信息去重功能,避免流量浪费,资源利用率较高,同时支持内存缓存,处理分页时高效利用系统资源。
三、使用与结构分层
1.依赖
//引入依赖
dependencies {def paging_version = "3.1.0"implementation "androidx.paging:paging-runtime:$paging_version"
}
2.结构分层

一般主要层级会分为三层,请求层ViewModel层UI页面层

  • 请求层

主要是对数据源的定义与处理PagingSource,划重点Google推荐的是单一可信数据源,即是官方的Demo,配合网络请求与本地数据库,即是从网络获取数据也并不是直接显示到UI页面上。而是更新了数据库后,数据库的变动驱动UI页面的展示。确保了数据来源单一。这么做有什么好处与不足,一步步探讨。RemoteMediator主要是用来处理数据的分页,同时数据可以来自于网络与本地,起到了整合的作用。

  • ViewModel层

数据配置Pager配合PagingConfig对数据流PagingData进行配置,如请求的数据size大小,是否开启null占位符等。

  • UI层

适配器的定义PagingDataAdapter,分页处理加载数据的核心类。跟普通的适配器区别不是很大,但是需要配合DiffUtil对数据进行去重判断。

  • 结构分层图
四、几个重要类
1.PagingSource
public abstract class PagingSource<Key : Any, Value : Any> {...}

PagingSource的实例用于为PagingData的实例加载数据页面,每次刷新数据都会有一个单独PagingData与之对应,而配合的DiffUtil则可以处理重复内容的去重工作。Key在请求网络数据时可以表示对应的页码,请求的是数据库的数据时也可以表示为位置PositionValue则是对应DTO或者PO,当然通常的项目中对于上层UI所使用的数据一般并不会直接使用原始数据。首先服务端返回的数据并不是都能够被完全用于UI,为了简洁都会通过Mapper做一次映射,转化成合理的VO数据。也即是DTO/PO ----Mapper<>–>VO.

2.RemoteMediator
@ExperimentalPagingApi
public abstract class RemoteMediator<Key : Any, Value : Any> {....}
//目前还是实验性的Api,在以后的版本更新中可能会存在变动,不是很建议使用在线上的环境中

协同网络数据与本地数据库Room,但是官方的推荐做法并不是直接使用网络数据作为数据源,是将网络数据缓存到本地数据库,由数据库担任唯一的数据源来驱动页面。实际开发过程中,数据是有实效性的,应该在合适的时机使本地数据失效而以服务端数据为主,并刷新到本地数据库。这就要定义初始化类型initialize

//两种机制
public enum class InitializeAction {LAUNCH_INITIAL_REFRESH,SKIP_INITIAL_REFRESH
}
  • LAUNCH_INITIAL_REFRESH:完全刷新本地数据,会阻塞包括PREPENDAPPEND,直到全量刷新成功以后返回新的数据

  • SKIP_INITIAL_REFRESH:加载本地数据,跳过远程刷新。

对于RemoteMediator加载方法中Load里包含了一个参数LoadType,那么这个LoadType是什么呢?其实就是定义了刷新机制,集合RecyclerView的用户操作,不断上滑的过程中,Paging请求下一页的内容。或者切换了不同搜索条件那么自然是全量刷新,而查看已经加载过的数据,可以理解为从内存数据中加载某一段数据,也即是中间部分的数据。简单的里脊就是LoadType是用来监听UI操作的。

public enum class LoadType {//全量刷新REFRESH,//从初始开始加载数据(PaingData)PREPEND,//从PagingData最后一条开始加载数据,需要从网络获取APPEND
}
3.Pager

可以直接创建一个单纯的网络数据分页,同时也支持本地与网络共享的状态。唯一的区别就是需要提供数据库Room的查询方法,并且提供RemoteMediator实例。当然还包括一些配置条件PagingConfig,如网络加载数据一页的条目,是否开启null占位。

//...
val customDao = database.customDao()
val pager = Pager(config = PagingConfig(pageSize = NETWORK_PAGE_SIZE, enablePlaceholders = false),remoteMediator = CustomRemoteMediator(query,service,database),pagingSourceFactory = pagingSourceFactory
).flow
//....
companion object {const val NETWORK_PAGE_SIZE = 50
}
五、官网Demo

官网的CodeLab基于Paging3Room结合的方式实现了通过关键字从Github搜索代码仓库的小应用,跟着走一遍可以加深对Paging3的理解,当然目前某些Api还是实验性质的,需要等一等正式版。项目地址CodeLab,源码地址Github.编译的时候可能会报错,主要原因是因为Kotlin 1.6.0 版本在Room中(2.3.0)不支持使用 suspend @QUERY,需要升级Room的版本为2.4.0-alpha03

官网的这个Demo将数据的唯一来源定为从Room中获取,网络数据缓存到本地,本地数据库的变动通知到UI页面的刷新。

整体数据获取结构分层:

  • 数据库为单一可信数据来源Single Source of Truth,而Pager的构成部分包括RemoteMediator与本地数据库PagingSource。首先数据是从数据库获取的,当缓存的数据已经被完全加载完毕,会触发拉取远程数据并缓存到本地,本地数据的变更驱动UI完成刷新。
1.数据模型Model

服务端获取的数据实体定义,并新建数据表repos

@Entity(tableName = "repos")
data class Repo(@PrimaryKey @field:SerializedName("id") val id: Long,@field:SerializedName("name") val name: String,@field:SerializedName("full_name") val fullName: String,@field:SerializedName("description") val description: String?,@field:SerializedName("html_url") val url: String,@field:SerializedName("stargazers_count") val stars: Int,@field:SerializedName("forks_count") val forks: Int,@field:SerializedName("language") val language: String?
)
2.数据库DB

远程键值表定义,

@Entity(tableName = "remote_keys")
data class RemoteKeys(@PrimaryKey val repoId: Long,val prevKey: Int?,val nextKey: Int?
)@Dao
interface RemoteKeysDao {@Insert(onConflict = OnConflictStrategy.REPLACE)suspend fun insertAll(remoteKey: List<RemoteKeys>)@Query("SELECT * FROM remote_keys WHERE repoId = :repoId")suspend fun remoteKeysRepoId(repoId: Long): RemoteKeys?@Query("DELETE FROM remote_keys")suspend fun clearRemoteKeys()
}@Dao
interface RepoDao {@Insert(onConflict = OnConflictStrategy.REPLACE)suspend fun insertAll(repos: List<Repo>)@Query("SELECT * FROM repos WHERE " +"name LIKE :queryString OR description LIKE :queryString " +"ORDER BY stars DESC, name ASC")fun reposByName(queryString: String): PagingSource<Int, Repo>@Query("DELETE FROM repos")suspend fun clearRepos()
}@Database(entities = [Repo::class, RemoteKeys::class],version = 1,exportSchema = false
)
abstract class RepoDatabase : RoomDatabase() {abstract fun reposDao(): RepoDaoabstract fun remoteKeysDao(): RemoteKeysDaocompanion object {@Volatileprivate var INSTANCE: RepoDatabase? = nullfun getInstance(context: Context): RepoDatabase =INSTANCE ?: synchronized(this) {INSTANCE?: buildDatabase(context).also { INSTANCE = it }}private fun buildDatabase(context: Context) =Room.databaseBuilder(context.applicationContext,RepoDatabase::class.java, "Github.db").build()}
}
3.核心GithubRemoteMediator

主要看load中的具体实现:

override suspend fun load(loadType: LoadType, state: PagingState<Int, Repo>): MediatorResult {val page = when(loadType) {...}val apiQuery = query + IN_QUALIFIERtry {val apiResponse = service.searchRepos(apiQuery, page, state.config.pageSize)val repos = apiResponse.itemsval endOfPaginationReached = repos.isEmpty()repoDatabase.withTransaction {// clear all tables in the databaseif (loadType == LoadType.REFRESH) {repoDatabase.remoteKeysDao().clearRemoteKeys()repoDatabase.reposDao().clearRepos()}val prevKey = if (page == GITHUB_STARTING_PAGE_INDEX) null else page - 1val nextKey = if (endOfPaginationReached) null else page + 1val keys = repos.map {RemoteKeys(repoId = it.id, prevKey = prevKey, nextKey = nextKey)}repoDatabase.remoteKeysDao().insertAll(keys)repoDatabase.reposDao().insertAll(repos)}return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)} catch (){...}
}
  • page:加载的具体页,可以是全量即初始化时,通过loadType来确定,而loadType则是根据用户操作有关
  • apiQuery:网络请求的参数,搜索的关键字
  • 拿到服务端的结果后主要做了几件事:更新本地的远程键,简单的理解就是记录本次加载的“位置”或者“锚点”,这个位置对应远程服务端数据的位置,并将数据存储到本地(如果是全量刷新,本地数据会先本清空)。
4.项目具体源码与效果图


Github

5.局部刷新

PagingAdapter继承自Recycler.Adapter,但是需要实现局部刷新可以通过 snapshot() 拿到一份只读数据的拷贝如:

fun refreshByPotion(position: Int, newItem: CustomVO?) {if (position < 0 || position >= snapshot().size || null == newItem) {return}snapshot()[position]?.age = newItem.agesnapshot()[position]?.name = newItem.namenotifyItemChanged(position)
}
6.思考

PagingAdapter并没有提供remove/add方法,这也是被一直诟病的点,但是Paging真的是一个垃圾的库嘛?其实不然,官方其实给出了它的使用场景,数据变动不大,即服务端数据变动频率不高,获取的数据以为展示为主,并没有太多的交互。那么及时在离线的情况下系统依然是可以运行良好的。普通的业务场景可能并合适使用Paging,那么我的理解,既然不合适就没有必要硬要往上套用,选择合适的库或组件将复杂的业务简单化而不是将简单的场景复杂化。

六、实际项目中使用

在餐饮行业中,餐厅一般都会有点餐系统,需要满足什么需求呢,离线可用。菜品会变动,需要及时更新,但是更新频率较低。总结如下:

  • 本地需要保存菜品信息(Room数据库),保证离线时可用。
  • 首次登录需要批量拉取服务端所有菜品信息保存到本地数据库。
  • 菜品有下架,上架,售罄等状态,这里的变更基于MQTT通知实现,通过比对本地菜品的版本号码作全量或者增量刷新。
  • 数据单一来源仅仅从本地数据库获取。

可以发现,这个场景下天然适合Paging库,而目前项目中还是使用的Paging2,并没有迁移到Paging3原因是目前还有很多实验性的Api

1.基于Paging2实现
  • 根据分类获取菜品,凉菜、热菜等
public LiveData<PagedList<DishSpuVO>> queryByCategoryId(String categoryId) {StoreDB storeDB = StoreDBManage.getInstance().getDataBase();if (storeDB == null) {return null;}return new LivePagedListBuilder<>(storeDB.DishSpuDAO().queryByCategoryId(categoryId).map(DishProductPO::transform),50).build();
}
  • 根据下发的通知同步本地菜品,并保存版本号(基于本地版本号与服务端版本号对比来决定是否更新数据)
@NotifyType(type = NotifyType.DISH_CHANGE)
public class DishChangeHandler implements INotify {@Overridepublic void process(String jsonData) {//根据版本号判断是否更新本地数据库,当数据库变动,会驱动Paging刷新UIFlowable<Object> dishFlowable = SyncRepository.syncProduct(jsonData, 0L);}
}

当然特定的场景使用Paging还是个不错的选择,官方预计短期内也不会考虑添加局部增删操作,分页库数据本身就是一个数据快照,如果作类似这种增量的增删操作,势必只能使原先PagingSource快照失效,设置新的数据快照,这显然浪费系统资源,性能上也会打折扣。

七、文档

Paging3 developer

CodeLab

Github

「Jetpack - Paging3使用」相关推荐

  1. java string 占位符_驳《阿里「Java开发手册」中的1个bug》?

    前两天写了一篇关于<阿里Java开发手册中的 1 个bug>的文章,评论区有点炸锅了,基本分为两派,支持老王的和质疑老王的. 首先来说,无论是那一方,我都真诚的感谢你们.特别是「二师兄」, ...

  2. 七天学会「股票数据分析软件」的开发(下)

    昨天下午把<我不是药神>这部电影看了,搞得我哭的稀里哗啦,里面有一些情节触痛了内心中最薄弱的地方.药厂没有错,他们要收回前期投入的研发成本.主人公 程勇 只能算是整个事件的牺牲品,通过他的 ...

  3. 七天学会「股票数据分析软件」的开发(中)

    两天前,我写了 七天学会「股票数据分析软件」的开发(上),号召大家尝试着写写代码,不知道大家进度如何. 如果存在掌握一种技能的刚需,而且知道正确的学习方法,经过刻意练习,这门技能很快就能玩儿的有模有样 ...

  4. 「AI 质检员」在富士通上岗,效率比人工高 25%

    日本第一 IT 厂商富士通,于近日宣布开发了用于检测产品外观异常的 AI 技术,从而节省人力成本.材料成本等,同时也可节省声誉损失和退货/召回相关的成本,「无人工厂」已来. 来源 | Hyper超神经 ...

  5. 「创式纪」人工智能应用创新大赛启动,首次结合商业计划和机器学习

    谈到人工智能,技术和应用场景成为了大家广泛关注的话题.技术的演进,是推动人工智能发展的核心,而广泛的场景应用,则是人工智能真正价值所在.现阶段,精准营销.信贷风控.人脸比对等为人熟知的AI,已经经过落 ...

  6. 低配版AI车神?网友用单个CNN在「极品飞车9」里飙车

    点击上方"视学算法",选择加"星标"或"置顶" 重磅干货,第一时间送达 来源丨新智元 编辑丨极市平台 导读 单凭一个CNN网络,居然能在快2 ...

  7. 黑客帝国真的可以!这100万个「活体人脑细胞」5分钟学会打游戏

      视学算法报道   编辑:小咸鱼 桃子 [新智元导读]近日,Cortical Labs开发了一种微型人类大脑--盘中大脑 (DishBrain).AI要90分钟才学得会的「乒乓球」游戏,这个「大脑」 ...

  8. 华人科学家胡安明被判无罪!曾因「中国行动计划」被FBI紧盯两年,遭软禁18个月...

      视学算法报道   编辑:小咸鱼.David [新智元导读]2020年2月,田纳西大学诺克斯维尔分校的胡安明博士被FBI以犯有电信欺诈罪和虚假陈述罪为由逮捕.2021年9月,田纳西州联邦法官宣布这位 ...

  9. 覆盖近2亿篇论文还免费!沈向洋旗下团队「读论文神器」登B站热搜

      视学算法报道   编辑:小咸鱼 好困 [新智元导读]无意中发现B站上有个叫ReadPaper的在线论文阅读笔记神器冲上了热榜!ReadPaper由沈向洋博士创办的IDEA旗下团队研发,其收录了近2 ...

最新文章

  1. NLP 解决方案是如何被深度学习改写的?
  2. 码农与架构师之间的差距,究竟在哪里?
  3. 不合法的媒体文件 id_注意!伦敦男孩、DKNY、Coach、地素... 这些品牌的服装,抽检不合格...
  4. 笔记-项目质量管理-编制质量管理计划的工具与技术
  5. MyCat实战--读写分离/数据分片/mycat集群/haproxy负载均衡
  6. hibernate oracle boolean 数据类型,用hibernate向oracle读取blob数据类型为并下载到本地...
  7. C#6.0语言规范(八) 语句
  8. php 5.2.6升级,Centos5.5 简单方法升级php到php5.2.6
  9. 5招教你实现多线程场景下的线程安全!
  10. PHP的continue语句,PHP continue语句
  11. U811.1接口EAI系列之三--采购订单生成--VB语言
  12. Delphi 2010手动安装indy10.5.7
  13. 1. 走进Java语言 —— Java SE
  14. hp cp1025 linux 驱动,惠普HP LaserJet CP1025 打印机驱动
  15. Flume 的使用场景详解
  16. 第一章、Android基础入门 - Android移动开发基础笔记
  17. 人工智能AI工程师职业规划指南
  18. 读论文 A search-classify approach for cluttered indoor scene understanding
  19. Scala中下划线“_“的应用场景
  20. 你所不知道的CMDB:CMDB起源与发展

热门文章

  1. Javascript MVC —— View
  2. python人物抠图算法_比PS还好用!Python 20行代码批量抠图
  3. 45RF SOI CMOS
  4. lgx06:连接数据库的驱动和url
  5. 悼念512汶川大地震遇难同胞——珍惜现在,感恩生活 dp
  6. 滑动轨迹 曲线 python_Python 模拟真实运动轨迹,轻松完成长跑和打卡
  7. java json导入excel_java 导入json生成excel
  8. Android图片格式转换为JPG
  9. MFC的exe文件图标更改后依然显示原图标--WINDOWS系统图标缓存未清除或者重建造成的
  10. 《每秒处理10万订单乐视集团支付架构》学有所得