使用 Paging 3 实现分页加载
作者 / Florina Muntenescu
Paging 3 亮点
Paging 3 的 API 对分页加载时可能需要实现的常见功能提供了支持:
跟踪获取前一页或后一页所需要的参数;
当用户滚动到现有数据的末尾时,自动请求正确的下一页;
确保不会同时触发多个请求;
跟踪加载状态,并支持您在 RecyclerView 的列表项或者界面中的其他地方展示它。为失败的加载提供简便的重试功能;
无论您是否使用 Flow、LiveData、RxJava Flowable 或 Observable,都可以对需要展示的列表使用 map 或 filter 这类常见的操作;
提供实现列表分隔符的简便方法;
简化了数据缓存,确保不会让您在每次配置更改时都执行数据转换。
迁移至 Paging 3 https://developer.android.google.cn/topic/libraries/architecture/paging/v3-migration
在您的应用中使用 Paging 3
Android 开发者文档 | Paging 3 库概述 https://developer.android.google.cn/topic/libraries/architecture/paging/v3-overview
Paging 组件及其在应用架构的集成
定义数据源
数据源的定义取决于您从哪里加载数据。您仅需实现 PagingSource 或者 PagingSource 与 RemoteMediator 的组合:
如果您从单个源加载数据,例如网络、本地数据、文件等,实现 PagingSource 即可,如果您使用了 Room,从 2.3.0-alpha 开始,它将默认为您实现 Paging Source,请参见: Android 开发文档|使用 Room DAO 访问数据;
如果您从一个多层级数据源加载数据,就像带有本地数据库缓存的网络数据源那样。那么您需要实现 RemoteMediator 来合并两个数据源到一个本地数据库缓存的 PagingSource 中。
PagingSource https://developer.android.google.cn/reference/kotlin/androidx/paging/PagingSource RemoteMediator https://developer.android.google.cn/reference/kotlin/androidx/paging/RemoteMediator Android 开发文档|使用 Room DAO 访问数据 https://developer.android.google.cn/training/data-storage/room/accessing-data#paging-integration
class DoggosRemotePagingSource(
val backend: GoodDoggosService
) : PagingSource<Int, Dog>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, Dog> {
try {
// 未定义时加载第 1 页
val nextPageNumber = params.key ?: 1
val response = backend.getDoggos(nextPageNumber)
return LoadResult.Page(
data = response.doggos,
prevKey = null, // 仅向后翻页
nextKey = response.nextPageNumber + 1
)
} catch (e: Exception) {
// 在此块中处理错误
return LoadResult.Error(exception)
}
}
}
PagingSource https://developer.android.google.cn/reference/kotlin/androidx/paging/PagingSource load() https://developer.android.google.cn/reference/kotlin/androidx/paging/PagingSource#load
PagingData 与 Pager
分页数据的容器被称为 PagingData,每次刷新数据时,都会创建一个 PagingData 的实例。如果要创建 PagingData 数据流,您需要创建一个 Pager 实例,并提供一个 PagingConfig 配置对象和一个可以告诉 Pager 如何获取您实现的 PagerSource 的实例的函数,以供 Pager 使用。
PagingData https://developer.android.google.cn/reference/kotlin/androidx/paging/PagingData PagingConfig https://developer.android.google.cn/reference/kotlin/androidx/paging/PagingConfig
val doggosPagingFlow = Pager(PagingConfig(pageSize = 10)) {
DogRemotePagingSource(goodDoggosService)
}.flow.cachedIn(viewModelScope)
cachedIn() https://developer.android.google.cn/reference/kotlin/androidx/paging/package-summary#cachedin viewModelScope https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/package-summary#viewmodelscope
PagingDataAdapter
class DogAdapter(diffCallback: DiffUtil.ItemCallback<Dog>) :
PagingDataAdapter<Dog, DogViewHolder>(diffCallback) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): DogViewHolder {
return DogViewHolder(parent)
}
override fun onBindViewHolder(holder: DogViewHolder, position: Int) {
val item = getItem(position)
if(item == null) {
holder.bindPlaceholder()
} else {
holder.bind(item)
}
}
}
PagingDataAdapter https://developer.android.google.cn/reference/kotlin/androidx/paging/PagingDataAdapter
val viewModel by viewModels<DoggosViewModel>()
val pagingAdapter = DogAdapter(DogComparator)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.adapter = pagingAdapter
lifecycleScope.launch {
viewModel.doggosPagingFlow.collectLatest { pagingData ->
pagingAdapter.submitData(pagingData)
}
}
分页数据转换
展示一个过滤后的列表
doggosPagingFlow.map { pagingData ->
pagingData.filter { dog -> dog.isPlayful }
}
有分隔符的列表
pager.flow.map { pagingData: PagingData<Dog> ->
pagingData.map { doggo ->
// 将数据流中的项目转换为 UiModel.DogModel。
UiModel.DogModel(doggo)
}
.insertSeparators<UiModel.DogModel, UiModel> { before: Dog, after: Dog ->
return if(after == null) {
// 我们到了列表的末尾
null
} else if (before == null || before.breed != after.breed) {
// 上下品种不同,显示分隔符
UiModel.SeparatorItem(after.breed)
} else {
// 无分隔符
null
}
}
}
}.cachedIn(viewModelScope)
insertSeparators https://developer.android.google.cn/reference/kotlin/androidx/paging/PagingData#insertseparators
使用 RemoteMediator 进行高级分页操作
RemoteMediator https://developer.android.google.cn/reference/kotlin/androidx/paging/RemoteMediator load() https://developer.android.google.cn/reference/kotlin/androidx/paging/RemoteMediator#load PagingState https://developer.android.google.cn/reference/kotlin/androidx/paging/PagingState LoadType https://developer.android.google.cn/reference/kotlin/androidx/paging/LoadType
override suspend fun load(loadType: LoadType, state: PagingState<Int, Dog>): MediatorResult {
val page = ... // 基于 loadType 和 state 进行计算
try {
val doggos = backend.getDoggos(page)
doggosDatabase.doggosDao().insertAll(doggos)
val endOfPaginationReached = emails.isEmpty()
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (exception: Exception) {
return MediatorResult.Error(exception)
}
}
@Query("SELECT * FROM doggos")
fun getDoggos(): PagingSource<Int, Dog>
val pagingSourceFactory = { database.doggosDao().getDoggos() }
return Pager(
config = PagingConfig(pageSize = NETWORK_PAGE_SIZE),
remoteMediator = DoggosRemoteMediator(service, database),
pagingSourceFactory = pagingSourceFactory
).flow
使用 RemoteMediator https://developer.android.google.cn/topic/libraries/architecture/paging/v3-network-db Paging codelab https://codelabs.developers.google.com/codelabs/android-paging/#14 Paging 相关代码 https://github.com/googlecodelabs/android-paging/blob/step13-19_network_and_database/app/src/main/java/com/example/android/codelabs/paging/data/GithubRemoteMediator.kt
Paging 3 仍然处于 alpha 版本,我们需要您帮助我们进一步优化!请参阅以下资源开始使用 Paging:
Android 开发文档|Paging 3 库概述
https://developer.android.google.cn/topic/libraries/architecture/paging/v3-overview
Codelab|Android Paging
https://codelabs.developers.google.com/codelabs/android-paging
代码示例|Paging With Network Sample
https://github.com/android/architecture-components-samples/tree/master/PagingWithNetworkSample
IssueTracker
https://issuetracker.google.com/issues?q=componentid:413106%20status:open
推荐阅读