Jetpack成员Paging3 数据库实践及原理分析(一)
前言
Google 新增加了 Jetpack 成员 Hilt、Paging 3、App Startup 等等,在之前的文章 Jetpack 成员 App Startup 实践及原理分析 里面介绍了 App Startup 是什么、App Startup 为我们解决了什么问题。
今天这边文章主要来分析 Paging3,Paging3 会分为三篇文章,每篇文章都有完整的项目示例。
Jetpack 成员 Paging3 数据库实践及原理分析(一) Jetpack 成员 Paging3 网络实践及原理分析(二) Jetpack 成员 Paging3 网络分页数据并更新到数据库中(三)
而这篇文章主要来分析 Jetpack 成员 Paging3 数据库实践及原理分析(一),文章中的示例代码,已经上传到 GitHub 欢迎前去查看 AndroidX-Jetpack-Practice/Paging3Simple
https://github.com/hi-dhl/AndroidX-Jetpack-Practice
通过这篇文章你将学习到以下内容:
Paging3 是什么? Paging3 在项目中的架构以及类的职能源码分析? 如何在项目中正确使用 Paging3? 数据映射(Data Mapper)是什么? Kotlin Flow 是什么?
这篇文章涉及到技术包含 : Room、Databinding、Anko、Koin、JDatabinding、Data Mapper、 Composing builds 作为依赖库的版本管理、Repository 设计模式、MVVM 架构等等,这些技术之前没有了解过,可以点击下面链接前往查看。
为数不多的人知道的 Kotlin 技巧及解析(一) 为数不多的人知道的 Kotlin 技巧及解析(二) 放弃 Dagger 拥抱 Koin
https://juejin.im/post/5ebc1eb8e51d454dcf45744e项目中封装 Kotlin + Android Databinding
https://juejin.im/post/5e9c434a51882573663f6cc6
Paging3 是什么?
Paging 是一个分页库,它可以帮助您从本地存储或通过网络加载显示数据。这种方法使你的 App 更有效地使用网络带宽和系统资源。
Paging3 是使用 Kotlin 协程完全重写的库,经历了从 Paging1x 到 Paging2x 在到现在的 Paging3,深刻领悟到 Paging3 比 Paging1 和 Paging2 真的方便了很多。
Google 推荐使用 Paging 作为 App 架构的一部分,它可以很方便的和 Jetpack 组件集成,Paging3 包含了以下功能:
在内存中缓存分页数据,确保您的 App 在使用分页数据时有效地使用系统资源。 内置删除重复数据的请求,确保您的 App 有效地使用网络带宽和系统资源。 可配置 RecyclerView 的 adapters,当用户滚动到加载数据的末尾时自动请求数据。 支持 Kotlin 协程和 Flow, 以及 LiveData 和 RxJava。 内置的错误处理支持,包括刷新和重试等功能。
Paging3 的架构以及类的职能源码分析
Google 推荐我们使用 Paging3 时,在应用程序的三层中操作,以及它们如何协同工作加载和显示分页数据,如下图所示:
但是我个人认为应该在增加一层 Data Mapper (下面会有详细的介绍),如下图所示:
数据映射(Data Mapper)将数据源的实体,转换为上层用到的 model,往往会被我们忽略掉,但是在项目中起到了很大重要,我看了很多项目的,这个概念很少被提及到,我只在国外的大牛的写的文章中,它们提及到了这个概念。关于数据映射(Data Mapper) 后面会单独写一篇文章,配合 Demo 去验证,这里只是简单提及一下。
Data Mapper
在一个快速开发的项目中,为了越快完成第一个版本交付,下意识的将数据源和 UI 绑定到一起,当业务逐渐增多,数据源变化了,上层也要一起变化,导致后期的重构工作量很大,核心的原因耦合性太强了。
使用数据映射(Data Mapper)优点如下:
数据源的更改不会影响上层的业务。 糟糕的后端实现不会影响上层的业务 (想象一下,如果你被迫执行2个网络请求,因为后端不能在一个请求中提供你需要的所有信息,你会让这个问题影响你的整个代码吗)。 Data Mapper 便于做单元测试,确保不会因为数据源的变化,而影响上层的业务。 在本文案例项目 Paging3Simple 中会用到 Data Mapper 作为数据映射,在代码中有详细的注释。
Repository layer
在 Repository layer 中的主要使用 Paging3 组件中的 PagingSource,每个 PagingSource 对象定义一个数据源以及如何从该数据源查找数据, PagingSource 对象可以从任何一个数据源加载数据,包括网络数据和本地数据。
PagingSource 是一个抽象类,其中有两个重要的方法 load 和 和 getRefreshKey,load 方法如下所示:
abstract suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value>
这是一个挂起函数,实现这个方法来触发异步加载,另外一个 getRefreshKey 方法
open fun getRefreshKey(state: PagingState<Key, Value>): Key? = null
该方法只在初始加载成功且加载页面的列表不为空的情况下被调用。
在这一层中还有另外一个 Paging3 的组件 RemoteMediator,RemoteMediator 对象处理来自分层数据源的分页,例如具有本地数据库缓存的网络数据源。
ViewModel layer
在 ViewModel layer 层主要用到了 Paging3 的组件 Pager,Pager 是主要的入口页面,在其构造方法中接受 PagingConfig、initialKey、remoteMediator、pagingSourceFactory,代码如下所示:
class Pager<Key : Any, Value : Any>
@JvmOverloads constructor(
config: PagingConfig,
initialKey: Key? = null,
@OptIn(ExperimentalPagingApi::class)
remoteMediator: RemoteMediator<Key, Value>? = null,
pagingSourceFactory: () -> PagingSource<Key, Value>
)
今天这篇文章和项目主要用到了 PagingConfig 和 PagingSource,PagingSource 上面已经说过了,所以我们主要来分一下 PagingConfig。
val pagingConfig = PagingConfig(
// 每页显示的数据的大小
pageSize = 60,
// 开启占位符
enablePlaceholders = true,
// 预刷新的距离,距离最后一个 item 多远时加载数据
prefetchDistance = 3,
/**
* 初始化加载数量,默认为 pageSize * 3
*
* internal const val DEFAULT_INITIAL_PAGE_MULTIPLIER = 3
* val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER
*/
initialLoadSize = 60,
/**
* 一次应在内存中保存的最大数据
* 这个数字将会触发,滑动加载更多的数据
*/
maxSize = 200
)
将 ViewModel 层连接到 UI 层用到了 Paging3 的组件 PagingData,PagingData 对象是分页数据的容器,它查询一个 PagingSource 对象并存储结果。
Google 推荐我们将组件 Pager 放到 ViewModel layer,但是我更喜欢放到 Repository layer,详见下文。
UI layer
在 UI layer 中的主要到了 Paging3 的组件 PagingDataAdapter,PagingDataAdapter 是一个处理分页数据的可回收视图适配器,您可以使用 AsyncPagingDataDiffer 组件来构建自己的自定义适配器,本文中用到是 PagingDataAdapter。
Paging 3 如何在项目中使用
在 App 模块中的 build.gradle 文件中添加以下代码:
dependencies {
def paging_version = "3.0.0-alpha01"
implementation "androidx.paging:paging-runtime:$paging_version"
}
接下来我将按照上面说的每层去实现,首先我们先来看一下项目的结构。
bean: 存放上层需要的 model,会和 RecyclerView 的 Adapter 绑定在一起。 loca: 存放和本地数据库相关的操作。 mapper: 数据映射,主要将数据源的实体 转成上层的 model。 repository:主要来处理和数据源相关的操作(本地、网络、内存中缓存等等)。 di: 和依赖注入相关。 ui:数据的展示。
数据库部分
@Dao
interface PersonDao {
@Query("SELECT * FROM PersonEntity order by updateTime desc")
fun queryAllData(): PagingSource<Int, PersonEntity>
@Insert
fun insert(personEntity: List<PersonEntity>)
@Delete
fun delete(personEntity: PersonEntity)
}
关于 Dao 这里需要解释一下, queryAllData 方法返回了一个 PagingSource,后面会通过 Pager 转换成 flow<PagingData<Value>>
。
Repository 部分
通过 Koin 注入 RepositoryFactory,通过 RepositoryFactory 管理相关的 Repository,RepositoryFactory 代码如下:
class RepositoryFactory(val appDataBase: AppDataBase) {
// 传递 PagingConfig 和 Data Mapper
fun makeLocalRepository(): Repository =
PersonRepositoryImpl(appDataBase, pagingConfig,Person2PersonEntityMapper(), PersonEntity2PersonMapper())
val pagingConfig = PagingConfig(
// 每页显示的数据的大小
pageSize = 60,
// 开启占位符
enablePlaceholders = true,
// 预刷新的距离,距离最后一个 item 多远时加载数据
prefetchDistance = 3,
/**
* 初始化加载数量,默认为 pageSize * 3
*
* internal const val DEFAULT_INITIAL_PAGE_MULTIPLIER = 3
* val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER
*/
initialLoadSize = 60,
/**
* 一次应在内存中保存的最大数据
* 这个数字将会触发,滑动加载更多的数据
*/
maxSize = 200
)
}
这里主要是生成 PagingConfig 和 Data Mapper 然后传递给 PersonRepositoryImpl,我们来看一下 PersonRepositoryImpl 相关代码。
class PersonRepositoryImpl(
val db: AppDataBase,
val pageConfig: PagingConfig,
val mapper2PersonEntity: Mapper<Person, PersonEntity>,
val mapper2Person: Mapper<PersonEntity, Person>
) : Repository {
private val mPersonDao by lazy { db.personDao() }
override fun postOfData(): Flow<PagingData<Person>> {
return Pager(pageConfig) {
// 加载数据库的数据
mPersonDao.queryAllData()
}.flow.map { pagingData ->
// 数据映射,数据库实体 PersonEntity ——> 上层用到的实体 Person
pagingData.map { mapper2Person.map(it) }
}
}
}
Pager 是主要的入口页面,在其构造方法中接受 PagingConfig、pagingSourceFactory。
pagingSourceFactory: () -> PagingSource<Key, Value>
pagingSourceFactory 是一个 lambda 表达式,在 Kotlin 中可以直接用花括号表示,在花括号内,执行加载数据库的数据的请求。
最后调用 flow 返回 Flow<PagingData<Value>>
,然后通过 Flow 的 map 将数据库实体 PersonEntity 转换成上层用到的实体 Person。
Flow 库是在 Kotlin Coroutines 1.3.2 发布之后新增的库,也叫做异步流,类似 RxJava 的 Observable,本文主要用到了 Flow 当中的 map 方法进行数据转换,简单实例如下所示:
flow{
for (i in 1..4) {
emit(i)
}
}.map {
it * it
}
到这里我们在回过去看,项目中 pagingData.map { mapper2Person.map(it) }
这行代码,其中 mapper2Person 是我们自己实现的 Data Mapper,代码如下所示:
class PersonEntity2PersonMapper : Mapper<PersonEntity, Person> {
override fun map(input: PersonEntity): Person = Person(input.id, input.name, input.updateTime)
}
数据库实体 PersonEntity 转换为 上层用到的实体 Person。
UI 部分
通过 koin 依赖注入 MainViewModel,并传递参数 Repository。
class MainViewModel(val repository: Repository) : ViewModel() {
// 调用 Flow 的 asLiveData 方法转为 LiveData
val pageDataLiveData3: LiveData<PagingData<Person>> = repository.postOfData().asLiveData()
}
在 Activity 当中注册 observe,并将数据绑定给 Adapter,如下所示:
mMainViewModel.pageDataLiveData3.observe(this, Observer { data ->
mAdapter.submitData(lifecycle, data)
})
知识扩充
刚才我们调用了 asLiveData 方法转为 LiveData,其实还有两种方法(作为了解即可)。
方法一
在 LifeCycle 2.2.0 之前使用的方法,使用两个 LiveData,一个是可变的,一个是不可变的,如下所示:
// 私有的 MutableLiveData 可变的,对内访问
private val _pageDataLiveData: MutableLiveData<Flow<PagingData<Person>>>
by lazy { MutableLiveData<Flow<PagingData<Person>>>() }
// 对外暴露不可变的 LiveData,只能查询
val pageDataLiveData: LiveData<Flow<PagingData<Person>>> = _pageDataLiveData
_pageDataLiveData.postValue(repository.postOfData())
准备一私有的 MutableLiveData,只对内访问。 对外暴露不可变的 LiveData。 将值赋值给 _pageDataLiveData。
方法二
在 LifeCycle 2.2.0 之后,可以用更精简的方法来完成,使用 LiveData 协程构造方法 (coroutine builder)。
val pageDataLiveData2 = liveData {
emit(repository.postOfData())
}
liveData 协程构造方法提供了一个协程代码块,产生的是一个不可变的 LiveData,emit() 方法则用来更新 LiveData 的数据。
最后添加左右滑动删除功能
调用 recyclerview 封装好的 ItemTouchHelper 实现 左右滑动删除 item 功能。
private fun initSwipeToDelete() {
/**
* 位于 [androidx.recyclerview.widget] 包下,已经封装好的控件
*/
ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int =
makeMovementFlags(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)
override fun onMove(
recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
(viewHolder as PersonViewHolder).mBinding.person?.let {
// 当 item 左滑 或者 右滑 的时候删除 item
mMainViewModel.remove(it)
}
}
}).attachToRecyclerView(rvList)
}
关于 Paging3 加载本地数据到这里就结束了,我们将在下一篇文章讲解如何加载网络数据,这篇文章相关代码,已经上传到 GitHub 欢迎前去查看 AndroidX-Jetpack-Practice/Paging3Simple
https://github.com/hi-dhl/AndroidX-Jetpack-Practice
总结
这篇文章主要介绍了以下内容:
Paging3 是什么以及它的优点
Paging 是一个分页库,它可以帮助您从本地存储或通过网络加载和显示数据。这种方法使你的 App 更有效地使用网络带宽和系统资源,而 Paging3 是使用 Kotlin 协程完全重写的库:
在内存中缓存分页数据,确保您的 App 在使用分页数据时有效地使用系统资源。 内置删除重复数据的请求,确保您的 App 有效地使用网络带宽和系统资源。 可配置 RecyclerView 的 adapters,当用户滚动到加载数据的末尾时自动请求数据。 支持 Kotlin 协程和 Flow, 以及 LiveData 和 RxJava。 内置的错误处理支持,包括刷新和重试功能。
Paging3 的架构以及类的职能源码分析
PagingSource:每个 PagingSource 对象定义一个数据源以及如何从该数据源查找数据。 RemoteMediator:RemoteMediator 对象处理来自分层数据源的分页,例如具有本地数据库缓存的网络数据源。 Pager:是主要的入口页面,在其构造方法中接受 PagingConfig、initialKey、remoteMediator、pagingSourceFactory。 PagingDataAdapter:是一个处理分页数据的可回收视图适配器,您可以使用 AsyncPagingDataDiffer 组件来构建自己的自定义适配器。
数据映射(Data Mapper)
数据映射(Data Mapper)将数据源的实体,转换为上层用到的 model,往往会被我们忽略掉的,但是在项目中起到了很大重要,使用 数据映射(Data Mapper)优点如下:
数据源的更改不会影响上层的业务。 糟糕的后端实现不会影响上层的业务 (想象一下,如果你被迫执行2个网络请求,因为后端不能在一个请求中提供你需要的所有信息,你会让这个问题影响你的整个代码吗)。 Data Mapper 便于做单元测试,确保不会因为数据源的变化,而影响上层的业务。 在本文案例项目 Paging3Simple 中会用到 Data Mapper 作为数据映射,点击下方链接前往查看
https://github.com/hi-dhl/AndroidX-Jetpack-Practice
Kotlin Flow
Flow 库是在 Kotlin Coroutines 1.3.2 发布之后新增的库,也叫做异步流,类似 RxJava 的 Observable,本文主要用到了 flow 当中的 map 方法进行数据转换,如下面的例子所示:
flow{
for (i in 1..4) {
emit(i)
}
}.map {
it * it
}
到这里我相信应该理解了,项目中 pagingData.map { mapper2Person.map(it) }
这行代码的意思了。
推荐阅读:
再见 SharedPreferences 拥抱 Jetpack DataStore Jetpack 成员 App Startup 实践及原理分析 Kotlin StateFlow 搜索功能的实践 DB + NetWork
最后推荐我一直在更新维护的项目和网站:
最新的 AndroidX Jetpack 相关组件的实战项目 以及 原理分析的文章
https://github.com/hi-dhl/AndroidX-Jetpack-PracticeLeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析
剑指 offer:https://offer.hi-dhl.com
LeetCode:https://leetcode.hi-dhl.com最新 Android 10 源码分析系列文章
https://github.com/hi-dhl/Leetcode-Solutions-with-Java-And-Kotlin一系列精选国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的分析
https://github.com/hi-dhl/Technical-Article-Translation「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址
https://site.51git.cn
致力于分享一系列最新技术原创文章
长按二维码即可关注
我知道你在看哟