前几天 Google 更新了几个 Jetpack 新成员 Hilt、Paging 三、App Startup 等等,在以前的文章里面分了 App Startup 是什么、App Startup 为咱们解决了什么问题,若是以前没有看过能够点击下面链接前往查看文章和代码。android
今天这边文章主要来分析 Paging3,并配有完整的项目演示,Paging3 会分为两篇文章,去详细的分析其原理:git
文章一(本文):主要来分析 Paging 3 如何加载本地数据库,代码已经上传到 GitHub,AndroidX-Jetpack-Practice 下面的 Paging3Simplegithub
GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice面试
文章二:主要来分析 Paging3 如何加载网络请求,核心主要是针对于不一样的分页策略,咱们如何去实现,在 Paging3 以前提供了 ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource,Paging3 以后统一为 PagingSource。算法
经过这篇文章你将学习到如下内容:数据库
在分析以前咱们先来了解一下本文实战项目中用到的技术:编程
使用 Koin 做为依赖注入,能够看我以前写的篇文章:[译][2.4K Star] 放弃 Dagger 拥抱 Koin。后端
使用 Composing builds 做为依赖库的版本管理,能够看我以前写篇文章:再见吧 buildSrc, 拥抱 Composing builds 提高 Android 编译速度。设计模式
JDataBinding 是我基于 DataBinding 封装的库,能够看我以前写篇文章:项目中封装 Kotlin + Android Databinding。数组
数据映射(Data Mapper): 将数据源的实体,转换为上层用到的 model,在项目中起到了很大重要,我看了不少项目的,这个概念不多被说起到,看国外的大牛的写的文章时,它们说起到了这个概念,后面会对它详细的分析。
项目中用到了一些 Kotlin 技巧,能够查看我另一篇文章:为数很少的人知道的 Kotlin 技巧以及 原理解析。
还有 Paging 三、Room、Anko、Repository 设计模式、MVVM 架构等等。
Paging 是一个分页库,它能够帮助您从本地存储或经过网络加载显示数据。这种方法使你的 App 更有效地使用网络带宽和系统资源。
Paging3 是使用 Kotlin 协程彻底重写的库,经历了从 Paging1x 到 Paging2x 在到如今的 Paging3,深入领悟到 Paging3 比 Paging1 和 Paging2 真的方便了不少。
Google 推荐使用 Paging 做为 App 架构的一部分,它能够很方便的和 Jetpack 组件集成,Paging3 包含了如下功能:
Google 推荐咱们使用 Paging3 时,在应用程序的三层中操做,以及它们如何协同工做加载和显示分页数据,以下图所示:
可是我我的认为应该在增长一层 Data Mapper (下面会有详细的介绍),以下图所示:
数据映射(Data Mapper)将数据源的实体,转换为上层用到的 model,每每会被咱们忽略掉,可是在项目中起到了很大重要,我看了不少项目的,这个概念不多被说起到,我只在国外的大牛的写的文章中,它们说起到了这个概念。关于数据映射(Data Mapper) 后面会单独写一篇文章,配合 Demo 去验证,这里只是简单说起一下。
在一个快速开发的项目中,为了越快完成第一个版本交付,下意识的将数据源和 UI 绑定到一块儿,当业务逐渐增多,数据源变化了,上层也要一块儿变化,致使后期的重构工做量很大,核心的缘由耦合性太强了。
使用数据映射(Data Mapper)优势以下:
在 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 层主要用到了 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 中的主要到了 Paging3 的组件 PagingDataAdapter,PagingDataAdapter 是一个处理分页数据的可回收视图适配器,您可使用 AsyncPagingDataDiffer 组件来构建本身的自定义适配器,本文中用到是 PagingDataAdapter。
在 App 模块中的 build.gradle 文件中添加如下代码:
dependencies {
def paging_version = "3.0.0-alpha01"
implementation "androidx.paging:paging-runtime:$paging_version"
}
复制代码
接下来我将按照上面说的每层去实现,首先咱们先来看一下项目的结构。
@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>>
。
经过 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。
经过 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())
复制代码
方法二
在 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)
}
复制代码
关于 Paging 加载本地数据到这里就结束了,咱们将在下一篇文章讲解如何加载网络数据,最后上一个效果图。
这篇文章主要介绍了如下内容:
Paging3 是什么以及它的优势
Paging 是一个分页库,它能够帮助您从本地存储或经过网络加载和显示数据。这种方法使你的 App 更有效地使用网络带宽和系统资源,而 Paging3 是使用 Kotlin 协程彻底重写的库:
Paging3 的架构以及类的职能源码分析
数据映射(Data Mapper)
数据映射(Data Mapper)将数据源的实体,转换为上层用到的 model,每每会被咱们忽略掉的,可是在项目中起到了很大重要,使用 数据映射(Data Mapper)优势以下:
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) }
这行代码的意思了。
GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice
计划创建一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增长 Jetpack 新成员,仓库持续更新,能够前去查看:AndroidX-Jetpack-Practice
致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,能够关注我,若是这篇文章对你有帮助给个 star,正在努力写出更好的文章,一块儿来学习,期待与你一块儿成长。
因为 LeetCode 的题库庞大,每一个分类都能筛选出数百道题,因为每一个人的精力有限,不可能刷完全部题目,所以我按照经典类型题目去分类、和题目的难易程度去排序。
每道题目都会用 Java 和 kotlin 去实现,而且每道题目都有解题思路,若是你同我同样喜欢算法、LeetCode,能够关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一块儿来学习,期待与你一块儿成长。
正在写一系列的 Android 10 源码分析的文章,了解系统源码,不只有助于分析问题,在面试过程当中,对咱们也是很是有帮助的,若是你同我同样喜欢研究 Android 源码,能够关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。
目前正在整理和翻译一系列精选国外的技术文章,不只仅是翻译,不少优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深刻的解读,能够关注我 GitHub 上的 Technical-Article-Translation,文章都会同步到这个仓库。