Jetpack-Paging使用教程
一、Paging是什么?
Paging 是一个分页库,Paging 可以帮助我们优雅地渐进加载大型数据集合,同时也可以减少网络的使用和系统资源的消耗。
-
在内存中缓存分页数据,确保您的 App 在使用分页数据时有效地使用系统资源。
-
内置删除重复数据的请求,确保您的 App 有效地使用网络带宽和系统资源。
-
可配置当用户滚动到加载数据的末尾时自动请求数据。
1.1. Paging版本
截止到目前2021-01-17 Paging库现在分为两个版本,其中Paging 2 版本为稳定版,Paging 3为Alpha版。
Paging 2虽然是稳定版但是使用起来还是会有问题,如:
-
分页失败就不会在进行分页了;
-
不支持
Header和Footer;
而在 Paging 3 中很多 API 的使用方式变了,也增加了很多功能,如:
-
支持
Kotlin协程和Flow。 -
支持
Header和Footer。 -
增加请求数据时状态的回调
-
内置的错误处理支持,包括刷新和重试等功能。
1. 2. 添加Paging库依赖
使用Paging 2,请在应用或模块的 build.gradle 文件中添加以下依赖
dependencies {
def paging_version = "2.1.2"
implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx
// alternatively - without Android dependencies for testing
testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx
// optional - RxJava support
implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
}
如需使用 Paging 3,请将以下依赖项添加到 build.gradle 文件中:
dependencies {
def paging_version = "3.0.0-alpha09"
implementation "androidx.paging:paging-runtime:$paging_version"
// alternatively - without Android dependencies for tests
testImplementation "androidx.paging:paging-common:$paging_version"
// optional - RxJava2 support
implementation "androidx.paging:paging-rxjava2:$paging_version"
// optional - Guava ListenableFuture support
implementation "androidx.paging:paging-guava:$paging_version"
// Jetpack Compose Integration
implementation "androidx.paging:paging-compose:1.0.0-alpha02"
}
详情可前往官方Paing依赖声明
二、 Paging 2的使用
我们先看一下Paging 2数据流工作原理图:
-
DataSource:数据源提供者,数据的改变会驱动列表的更新。 -
PageList:核心类,它从数据源取出数据,同时,它负责页面初始化数据+分页数据什么时候加载,以何种方式加载。 -
PagedListAdapter:是RecyclerView.Adapter的实现类,从PagedList过来的数据,通过DiffUtil差分异定向更新列表数据。
2.1. DataSource
顾名思义就是数据来源,作用就是提供加载所需的数据,数据源可以是客户端本地数据库也可以是服务器。需要通过DataSource.Factory工厂类创建。DataSource<Key, Value> Key对应数据加载的条件,Value就是数据源的实体类。Paging 2的设计者提供了三种不同类型的DataSource抽象类:
-
PositionalDataSource<T>:适用于数据容量固定,要通过特定的位置加载数据。比如从某个位置开始的 100 条数据; -
ItemKeyedDataSource<Key, Value>:适用于目标数据的加载依赖特定item的信息, 即Key字段包含的是Item中的信息,比如需要根据第N项的信息加载第N+1项的数据,传参中需要传入第N项的ID时。之前钱包记账就有请求下一页内容时需要传入当前页最后一笔账单的信息。 -
PageKeyedDataSource<Key, Value>:和ItemKeyedDataSource类似,适用于以页信息加载数据的场景。比如在网络加载数据的时候,需要通过setNextKey()和setPreviousKey()方法设置下一页和上一页的标识Key。
2.1.1. PositionalDataSource
需要重写loadInitial和loadRange方法
class PositionDataSource: PositionalDataSource<Cheese>() {
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Cheese>) {
callback.onResult(
data = loadData(params.requestedStartPosition, params.pageSize),
position = 0,
totalCount = 400
)
}
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Cheese>) {
callback.onResult(
data = loadData(params.startPosition, params.loadSize)
)
}
fun loadData(itemKey: Int, pageSize: Int): List<Cheese> {
val data = ArrayList<Cheese>()
if (itemKey >= CHEESE_DATA.size - 1) {
return data
}
var end = itemKey + pageSize
if (end > CHEESE_DATA.size) {
end = CHEESE_DATA.size
}
for (i in itemKey until end) {
val cheese = Cheese(i, CHEESE_DATA[i])
data.add(cheese)
}
return data
}
}
-
loadInitial:加载初始列表数据。在用DataSource构建PageList的时候才会调用一次。 -
LoadInitialParams:初始加载的参数,包括请求的起始位置,加载大小和页面大小。与PageList.Config设置有关 -
LoadInitialCallback:接收初始负载数据(包括位置和数据集总大小)的回调。 -
loadRange:加载其他页面列表数据。类似LoadMore,在每次RecyclerView滑动到底部没有数据的时候就会调用此方法进行数据的加载。 -
LoadRangeParams:初始加载的参数,包括请求的起始位置,加载大小和页面大小 -
LoadRangeCallback:接收已加载数据的回调。
2.1.2. ItemKeyedDataSource
需要重写getKey、loadInitial、loadBefore和loadAfter四个方法
class TextItemKeyDataSource() : ItemKeyedDataSource<Int, Cheese>() {
override fun getKey(item: Cheese): Int {
return item.id
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Cheese>) {
callback.onResult(loadData(params.key, params.requestedLoadSize))
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Cheese>) {
}
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Cheese>
) {
val data = loadData(0, params.requestedLoadSize)
callback.onResult(data)
}
}
-
getKey:返回下一个loadAfter调用所需要用到的key。 -
loadInitial:加载初始列表数据。 -
loadAfter:在每次RecyclerView滑动到底部PagedList中没有数据的时候就会调用此方法进行数据的加载。 -
loadBefore:在每次RecyclerView滑动到顶部PagedList没有数据的时候就会调用此方法进行数据的加载。
2.1.3. PageKeyedDataSource
需要重写loadInitial、loadBefore和loadAfter三个方法
class TextPageKeyDataSource : PageKeyedDataSource<Int, Cheese>() {
private var pageKey: Int = 0
private var pageSize: Int = 10
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Cheese>) {
callback.onResult(loadData(params.key, pageSize), params.key + 1)
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Cheese>) {
}
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, Cheese>
) {
callback.onResult(
data = loadData(pageKey, pageSize),
previousPageKey = null,
nextPageKey = pageKey + 1
)
}
fun loadData(pageNumber: Int, pageSize: Int): List<Cheese> {
val data = ArrayList<Cheese>()
var startPosition = pageNumber * pageSize
if (startPosition > CHEESE_DATA.size) {
return data
}
var endPosition = (pageNumber + 1) * pageSize
if (endPosition > CHEESE_DATA.size) {
endPosition = CHEESE_DATA.size
}
for (i in startPosition until endPosition) {
val cheese = Cheese(i, CHEESE_DATA[i])
data.add(cheese)
}
return data
}
}
-
loadInitial:加载初始列表数据。 -
loadAfter:在每次RecyclerView滑动到底部PagedList没有数据的时候就会调用此方法进行数据的加载。 -
loadBefore:在每次RecyclerView滑动到顶部PagedList没有数据的时候就会调用此方法进行数据的加载。
2.2. DataSource.Factory
这个接口的实现类主要是用来获取我们上面实现的DataSource。
class MyDataSourceFactory() : DataSource.Factory<Int, Cheese>() {
override fun create(): DataSource<Int, Cheese> {
return TextPositionDataSource()
}
}
2.3. PageList
PagedList<T>是一种List,用来存储加载的数据。其中的所有数据均从DataSource中加载。
2.4. PageList.Config
配置PagedList如何从DataSource中加载内容。不同的DataSource设置PageList.Config的效果也有差异
val pagedListConfig = PagedList.Config.Builder()
.setPageSize(10)
.setPrefetchDistance(5)
.setEnablePlaceholders(true)
.setInitialLoadSizeHint(20)
.setMaxSize(100)
.build()
-
pageSize: 每一页的数据量。 -
prefetchDistance: 预取数据的距离,也就是距离最后一个item多远时开始加载下一页数据,默认是一页的数据量pageSize。 -
enablePlaceholders:定义PagedList是否可以显示空的占位符(如果DataSource提供的话) -
initialLoadSize:初始化加载的数量,默认为pagesize * 3。 -
maxsize:保留在内存中的最大项目数,防止由于预取而导致连续读取和丢弃负载。比如:在PositionalDataSource中maxsize=100我们加载超过100个,倒回去第一页,会重新执行loadRange()。
**注意:**默认情况下
PagingConfig.maxSize是无界的,因此永远不会删除页面。如果确实要删除页面,请确保保持maxSize足够高的数量,以免用户更改滚动方向时不会导致太多网络请求。最小值为pageSize + prefetchDistance * 2。
2.5. LivePagedListBuilder
LivePagedListBuilder是用于获取PagedList类型的LiveData对象的类,也就是LiveData<PagedList<T>>的生成器。沟通DataSource.Factory、PageList、PageList.Config三者的桥梁,将数据源工厂和相关配置统一交给PagedListBuilder,即可生成对应的LiveData<PagedList<T>>,为 PagedListAdapter提供所需要的 PagedList。
val data :LiveData<PagedList> = LivePagedListBuilder(mFactory, pagedListConfig).build()
如果需要构造Observable<PagedList>或者Flowable<PagedList>在RxJava中使用请使用RxPagedListBuilder
2.6.PagedListAdapter
PagedListAdapter和RecyclerView.Adapter差别不大,比起普通的Adapter要额外提供一个 DiffUtil.ItemCallback实例用于数据比较更新列表。
class BillRecyclerViewAdapter :
PagedListAdapter<Cheese, RecyclerView.ViewHolder>(DIFF_CALLBACK) {
companion object {
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Cheese>() {
override fun areItemsTheSame(oldItem: Cheese, newItem: Cheese): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Cheese, newItem: Cheese): Boolean =
oldItem == newItem
}
}
}
-
areItemsTheSame:比对新旧item是否是同一个item;一般比对item的唯一标识id即可,如果item不同则可能不会更新UI; -
areContentsTheSame:当areItemsTheSame确定是同一个item之后,该方法会被调用。该方法比对item的内容是否一样(数据是否相同),不一样则会更新UI;建议这里的比对把UI展示的数据都写上,写漏了会导致UI不更新对应字段;
Paging组件支持三种不同数据结构:
-
仅从网络获取
-
仅从设备数据库获取
-
两种数据来源的组合,使用设备数据库作为缓存,注意是数据库作为缓存而不是即从数据库获取又从网络获取。

在单一数据源情况下,我们使用Paging 2进行分页加载的整体架构设计一般是如下图:

如果时多层数据来源,需要使用BoundaryCallback实现监听数据边界,实现本地无数据时请求网络数据。

2.7. PagedList.BoundaryCallback
监听数据边界,在PagedList到达可用数据的末尾时发出信号。当本地存储是网络数据的缓存时,当本地缓存全部被加载完后,需要去触发网络加载新的数据。
abstract class BoundaryCallback<T : Any> {
open fun onZeroItemsLoaded() {}
open fun onItemAtFrontLoaded(itemAtFront: T) {}
open fun onItemAtEndLoaded(itemAtEnd: T) {}
}
onZeroItemsLoaded:从PagedList的数据源的初始加载返回零项时调用。onItemAtFrontLoaded:当PagedList前面存在加载项目的时候会被调用,在此方法之前,不会有更多数据添加到PagedList。PositionalDataSource:loadInitial()之后会调用一次ItemKeyedDataSource:没有见到有触发PageKeyedDataSource:loadInitial()之后会调用一次,超过PagedList.Config中设置的maxSize就会触发
onItemAtEndLoaded:当PagedList末尾的项目被加载时会被调用,此方法之后,将不再有更多数据附加到PagedList。PositionalDataSource中到达LoadInitialCallback中设置的totalCount触发ItemKeyedDataSource、PageKeyedDataSource数据加载完了就会触发
三、Paging 3的使用
Paging 3相对于Paging 2改动可以说很大,再来看一下Paging 3数据加载流程,如下图:

-
PagingSource:单一数据源。 -
RemoteMediator:其实RemoteMediator也是单一的数据源,它会在PagingSource没有数据的时候,再使用RemoteMediator提供的数据,如果既存在数据库请求,又存在网络请求,通常PagingSource用于进行数据库请求,RemoteMediator进行网络请求。 -
Pager:用于构建Flow提供给PagingDataAdapter需要的PagingData -
PagingData:数据列表,相当于Paging 2中的PageList。Paging 3中使用PagingData替换Paging 2中的PageList -
PagingConfig:配置PagedData如何从其PagingSource加载内容。Paging 3中使用PagingConfig替换Paging 2中的PagedList.Config。
2.1. PagingSource
使用的话需要继承该类并实现 load 方法来加载数据,根据加载情况返回LoadResult.Page或LoadResult.Error。
Paging 2中的三种数据源ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource合并为一个PagingSource。
class SingleDataSource : PagingSource<Int, Cheese>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Cheese> {
val position = params.key ?: 0
val dataList = loadData(position, params.loadSize)
return try {
LoadResult.Page(
data = dataList,
prevKey = if (position == 0) null else position - 1,
nextKey = if (position == 0) position + 4 else if (dataList.isEmpty()) null else position + 1
)
} catch (exception: Exception) {
return LoadResult.Error(exception)
}
}
private fun loadData(pageNumber: Int, pageSize: Int): List<Cheese> {
val data = ArrayList<Cheese>()
val startPosition = pageNumber * pageSize
if (startPosition > CHEESE_DATA.size) {
return data
}
var endPosition = (pageNumber + 1) * pageSize
if (endPosition > CHEESE_DATA.size) {
endPosition = CHEESE_DATA.size
}
for (i in startPosition until endPosition) {
val cheese = Cheese(i, CHEESE_DATA[i])
data.add(cheese)
}
return data
}
}
-
LoadParams:LoadParams.key要加载的页面的key,params.loadSize要加载的页面的数据大小 -
LoadResult.Page:PagingSource.load加载数据成功时调用。 -
LoadResult.Error:PagingSource.load加载数据失败时调用。失败将作为LoadState.Error转发到UI,并且可以重试。
2.2. Pager
Pager 主要用于构建 flow类型的PagingData对象提供给PagingDataAdapter使用 。
Paging 2中的LivePagedListBuilder和RxPagedListBuilder合并为了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>
)
initialKey和remoteMediator是可选参数,默认为null。remoteMediator上面我们讲到了,是在多层数据源下负责网络加载数据的。如果是单一数据源只需要使用PagingSource就行。基本使用方式如下:
val pager : Flow<PagingData<Cheese>> = Pager(pagingConfig, pagingSourceFactory = factory).flow
-
Kotlin-Flow使用Pager.flow。 -
LiveData使用Pager.liveData。 -
RxJava-Flowable使用Pager.flowable。 -
RxJava-Observable使用Pager.observable。
2.3. PagingConfig
主要配置一些基本的分页信息,其中部分信息例如页码、需要加载size等信息,会在PagingSource的load方法中通过 LoadParams 传递过来。与paging 2中的一致。
val pagingConfig = PagingConfig(
pageSize = 10,
prefetchDistance = 5,
enablePlaceholders = true,
initialLoadSize = 40,
maxSize = 100
)
2.4. PagingDataAdapter
和paging 2中的PagedListAdapter一样要额外提供一个DiffUtil.ItemCallback实例用于数据比较更新列表,但是比PagedListAdapter多增加了一些API。
2.4.1 监听数据加载状态
PagedListAdapter提供了loadStateFlow 和addLoadStateListener两种方法,方便我们知道数据的加载状态,从而展示页面是在加载中还是加载失败等UI。
-
PagedListAdapter.loadStateFlow返回的是个Flow<CombinedLoadStates>。 -
PagedListAdapter.addLoadStateListener方法回调给我们的是CombinedLoadStates。
CombinedLoadStates 允许我们获取3种不同类型的加载操作的加载状态:
| 变量 | 作用 |
|---|---|
| refresh | 在初始化或者刷新的时候使用 |
| append | 在当前列表末尾添加数据的时候使用 |
| prepend | 在当前列表头部添加数据的时候使用 |
其中
CombinedLoadStates来源是有区分的,区分来自PagingSource和RemoteMediator。
LoadState 也有三种状态 NotLoading 、 Loading 、 Error 代表网络请求状态。
| 变量 | 作用 |
|---|---|
| Error | 表示加载失败 |
| Loading | 表示正在加载 |
| NotLoading | 表示当前未加载 |
比如为我们的数据加载增加监听数据加载失败的情况:
lifecycleScope.launch {
mAdapter.loadStateFlow.collectLatest {
if (loadStates.append is LoadState.Error) {
val errInfo = loadStates.refresh as LoadState.Error
Toast.makeText(
this,
"\uD83D\uDE28 Wooops ${errInfo.error}",
Toast.LENGTH_LONG
).show()
}
}
}
mAdapter.addLoadStateListener { loadStates ->
if (loadStates.append is LoadState.Error) {
val errInfo = loadStates.refresh as LoadState.Error
Toast.makeText(
this,
"\uD83D\uDE28 Wooops ${errInfo.error}",
Toast.LENGTH_LONG
).show()
}
}
2.4.2 添加Header和Footer
Paging 3支持添加Header和Footer。为此PagingDataAdapter有3种可用的方法:
| 方法名 | 作用 |
|---|---|
| withLoadStateFooter | 添加到列表底部(类似于加载更多) |
| withLoadStateHeader | 添加到列表的头部 |
| withLoadStateHeaderAndFooter | 添加到头部和底部 |
他们接收的参数都是LoadStateAdapter,所以我们的Header和Footer需要实现LoadStateAdapter。
// mItemRecyclerView.adapter = mAdapter
// 添加Header和Footer时,设置adapter使用下面方法
mItemRecyclerView.adapter = mAdapter.withLoadStateHeaderAndFooter(
header = ReposLoadStateAdapter { mAdapter.retry() },
footer = ReposLoadStateAdapter { mAdapter.retry() }
)
2.4.3. 刷新和重试
-
refresh:会回到初始化页,常用于下拉更新数据。 -
retry:常用于底部更多样式,当请求网络失败的时候,显示重试按钮,点击调用retry。
2.5. RemoteMediator
用于将数据从远程源增量加载到由PagingSource包裹的本地源中,默认是先走PageSource的数据,当 PageSource 提供的数据返回为空的情况,才会走 RemoteMediator 的数据。所以我们一般的使用都是PageSource进行数据库请求提供数据,RemoteMediator 进行网络请求,然后将请求成功的数据存入数据库。

RemoteMediator 和 PagingSource 的区别
-
RemoteMediator:实现加载网络分页数据并更新到数据库中,但是数据源的变动不能直接映射到 UI 上 -
PagingSource:实现单一数据源以及如何从该数据源中查找数据,数据源的变动会直接映射到 UI 上
注意:
RemoteMediatorAPI目前处于实验阶段。所有实现的类RemoteMediator都应使用注释@OptIn(ExperimentalPagingApi::class)。
当使用此OptIn批注,请确保您的应用程序的build.gradle文件,你必须freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]在你的kotlinOptions。
@OptIn(ExperimentalPagingApi::class)
class ExampleRemoteMediator(
private val query: String,
private val database: RoomDb,
private val networkService: ExampleBackendService
) : RemoteMediator<Int, User>() {
val userDao = database.userDao()
val remoteKeyDao = database.remoteKeyDao()
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, User>
): MediatorResult {
return try {
val loadKey = when (loadType) {
LoadType.REFRESH -> null
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val remoteKey = database.withTransaction {
remoteKeyDao.remoteKeyByQuery(query)
}
if (remoteKey.nextKey == null) {
return MediatorResult.Success(endOfPaginationReached = true)
}
remoteKey.nextKey
}
}
val response = networkService.searchUsers(query, loadKey)
database.withTransaction {
if (loadType == LoadType.REFRESH) {
remoteKeyDao.deleteByQuery(query)
userDao.deleteByQuery(query)
}
remoteKeyDao.insertOrReplace(RemoteKey(query, response.nextKey))
userDao.insertAll(response.users)
}
MediatorResult.Success(endOfPaginationReached = response.nextKey == null)
} catch (e: IOException) {
MediatorResult.Error(e)
} catch (e: HttpException) {
MediatorResult.Error(e)
}
}
}
-
LoadType-这告诉我们是需要将数据加载到PagingData的末尾 (LoadType.APPEND) ,还是在PagingData的开头(LoadType.PREPEND)加载数据,还是我们是第一次加载数据(LoadType.REFRESH)。 -
PagingState-这为我们提供了有关之前加载的页面(pages),列表中最近访问的索引(anchorPosition)以及PagingConfig。pages: List<Page<Key, Value>>返回的上一页的数据,主要用来获取上一页最后一条数据作为下一页的开始位置config: PagingConfig返回的初始化设置的PagingConfig包含了pageSize、prefetchDistance、initialLoadSize等
load() 的返回值 MediatorResult,MediatorResult 是一个密封类,根据不同的结果返回不同的值
-
请求出现错误,返回
MediatorResult.Error(e) -
请求成功且后面还有有数据,返回
MediatorResult.Success(endOfPaginationReached = true) -
请求成功但是后面没有数据了,返回
MediatorResult.Success(endOfPaginationReached = false)
在 load() 方法里面将会做三件事
- 判断参数 LoadType
- 请求网络数据
- 将网络数据插入到本地数据库中
总结
-
PagingSource负责异步加载自定义源中的数据。 -
通过
Pager.flow创建了一个Flow<PagingData>,Flow提供PagingData给PagingDataAdapter使用。而Pager基于PagingConfig和PagingSource实例化。 -
数据加载失败,可以使用
PagingDataAdapter.retry方法进行重试。它会触发PagingSource.load()方法。另外刷新可以使用PagingDataAdapter.refresh方法。 -
要将加载状态显示为
header或footer,使用PagingDataAdapter.withLoadStateHeaderAndFooter()方法并实现LoadStateAdapter。 -
如果需要知道数据加载状态做对应的操作,使用
PagingDataAdapter.addLoadStateListener()回调。 -
要同时使用网络和数据库,需要实现
RemoteMediator。
参加文档:
https://developer.android.com/codelabs/android-paging
https://www.bilibili.com/video/BV1Wb411P7CR?from=search&seid=16871215559318514007
本文详细介绍Jetpack Paging库的使用方法,涵盖Paging2与Paging3版本,包括数据源设计、配置、适配器及状态管理等内容。

1304

被折叠的 条评论
为什么被折叠?



