作者 / Florina Muntenescu
Paging 库能够帮忙您优雅地渐进加载大型数据汇合,同时也能够缩小网络的应用和系统资源的耗费。基于您的反馈咱们得悉,Paging 2.0 API 还不能满足开发者们的需要——开发者们心愿以更简便的形式处理错误;以更灵便的形式实现列表数据的转换操作,例如 map 和 filter;以及反对宰割符、页眉和页脚。基于以上反馈,咱们推出了 Paging 3.0。这是一个齐全应用 Kotlin 协程重写的库 (仍然反对 Java 用户),它将为您提供您所要求的性能。
Paging 3 亮点
Paging 3 的 API 对分页加载时可能须要实现的常见性能提供了反对:
- 跟踪获取前一页或后一页所须要的参数;
- 当用户滚动到现有数据的开端时,主动申请正确的下一页;
- 确保不会同时触发多个申请;
- 跟踪加载状态,并反对您在 RecyclerView 的列表项或者界面中的其余中央展现它。为失败的加载提供简便的重试性能;
- 无论您是否应用 Flow、LiveData、RxJava Flowable 或 Observable,都能够对须要展现的列表应用 map 或 filter 这类常见的操作;
- 提供实现列表分隔符的简便办法;
- 简化了数据缓存,确保不会让您在每次配置更改时都执行数据转换。
咱们还让 Paging 3 的一些组件向后兼容 Paging 2.0。因而,如果您曾经在利用中应用了 Paging,则能够逐渐 迁徙至 Paging 3。
在您的利用中应用 Paging 3
假如咱们正在实现一个展现所有狗狗的利用。狗狗的数据从 GoodDoggos API 取得,该 API 反对基于索引的分页。让咱们钻研下须要实现的 Paging 组件,以及如何将 Paging 集成到现有的利用架构。接下来的例子将应用 Kotlin 及其协程性能编写,如果您须要应用 LiveData/RxJava 实现的 Java 编程语言示例,请参阅 Android 开发者文档 | Paging 3 库概述。
下图为您利用的各个层级中举荐间接接入 Paging 的 Android 利用架构:
Paging 组件及其在利用架构的集成
定义数据源
数据源的定义取决于您从哪里加载数据。您仅需实现 PagingSource 或者 PagingSource 与 RemoteMediator 的组合:
- 如果您从 单个源 加载数据,例如网络、本地数据、文件等,实现 PagingSource 即可,如果您应用了 Room,从 2.3.0-alpha 开始,它将默认为您实现 Paging Source,请参见: Android 开发文档|应用 Room DAO 拜访数据;
- 如果您从一个 多层级数据源 加载数据,就像带有本地数据库缓存的网络数据源那样。那么您须要实现 RemoteMediator 来合并两个数据源到一个本地数据库缓存的 PagingSource 中。
PagingSource
PagingSource 能够定义一个分页数据的数据源,以及从该数据源获取数据的形式。PagingSource 该当为资源库层的一部分。您能够实现 load() 函数来从数据源获取分页数据,并返回加载好的数据和加载前后页的参数信息。load() 是一个挂起函数,您能够在这里调用其余的 挂起函数,例如网络申请:
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)
}
}
}
PagingData 与 Pager
分页数据的容器被称为 PagingData,每次刷新数据时,都会创立一个 PagingData 的实例。如果要创立 PagingData 数据流,您须要创立一个 Pager 实例,并提供一个 PagingConfig 配置对象和一个能够通知 Pager 如何获取您实现的 PagerSource 的实例的函数,以供 Pager 应用。
您要在 ViewModel 中结构 Pager 对象并向 UI 裸露一个 Flow<PagingData>。Flow<PagingData> 有一个不便的 cachedIn() 办法,该办法使得数据流能够被共享,也让您能够在 CoroutineScope 中缓存 Flow<PagingData> 的内容。这样一来,如果您在数据流中实现了任何转换操作,当 Activity 被重建并使得您从 flow 中获取数据时,不会再次触发这些操作。因为咱们心愿数据在配置产生变动后依然存在,缓存该当尽可能凑近 UI 层,但又不能在 UI 层中,那么最好的地位便是在 ViewModel 中,并应用 viewModelScope:
val doggosPagingFlow = Pager(PagingConfig(pageSize = 10)) {DogRemotePagingSource(goodDoggosService)
}.flow.cachedIn(viewModelScope)
PagingDataAdapter
为了将 RecyclerView 与 PagingData 分割起来,您须要实现一个 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)
}
}
}
接下来,在您的 Activity/Fragment
中,您须要收集 Flow<PagingData>
并将其提交给 PagingDataAdapter
。上面是一个在 Activity
的 onCreate()
函数中实现该操作的示例:
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)
}
}
分页数据转换
展现一个过滤后的列表
转换 PagingData 流与您在其余数据流中所做的同类操作类似。举例来说,如果咱们只想要展现 Flow<PagingData<Dog>> 中那些淘气的狗狗,咱们可能须要映射 Flow 对象并过滤 PagingData:
doggosPagingFlow.map { pagingData ->
pagingData.filter {dog -> dog.isPlayful}
}
有分隔符的列表
向列表中增加 分隔符 同样是分页数据转换,这里咱们通过转换 PagingData 向列表中插入分隔对象。举例来说,咱们能够为狗狗的名字插入字母分隔符。当应用分隔符时,您须要本人实现 UI 模型类以反对新的分隔项。当您批改 PagingData 并插入分隔符时,您会用到 insertSeparators 转换:
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)
就像后面一样,咱们会在数据达到 UI 层之前应用 cachedIn,这样便能够缓存所有曾经加载的数据以及数据转换的后果。当配置产生扭转时,这些缓存就会被复用。
应用 RemoteMediator 进行高级分页操作
当您从一个 多层级数据源 加载数据时,该当实现一个 RemoteMediator。举例来说,在此类的实现中,您该当从网络申请数据并存入数据库。每当数据库中没有数据能够被展现时,就会触发 load() 办法。基于 PagingState 和 LoadType,咱们能够结构下一页的数据申请。
因为 Paging 库并不知道您的 API 是怎么的,所以定义如何结构和获取前一页和下一页的近程数据的工作便须要由您本人来实现。举例来说,您能够将您从网络接管到的每个我的项目与近程关键字关联起来并存入数据库。
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)
}
}
如果您从网络申请数据并存入数据库,那么数据库才是屏幕上所展现数据的真正数据源——这意味着 UI 会展现从数据库获取的数据,所以您须要为您的数据库实现 PagingSource。如果您正在应用 Room,那么您只须要向您的 DAO 增加一个返回 PagingSource 的查问:
@Query("SELECT * FROM doggos")
fun getDoggos(): PagingSource<Int, Dog>
这种状况下 Pager 的实现略有不同,您还须要传入 RemoteMediator 实例:
val pagingSourceFactory = {database.doggosDao().getDoggos()}
return Pager(config = PagingConfig(pageSize = NETWORK_PAGE_SIZE),
remoteMediator = DoggosRemoteMediator(service, database),
pagingSourceFactory = pagingSourceFactory
).flow
您能够参阅文档理解 应用 RemoteMediator 的详细信息。如果您须要 RemoteMediator 在利用中的残缺实现,能够参阅 Paging codelab 和 Paging 相干代码。
咱们将 Paging 3 设计为一个帮您涵盖简略和简单情景下的分页加载的库。它能够让您更不便地应用大规模数据汇合,无论数据来自网络、数据库、内存缓存还是上述几种状况的组合。Paging 库基于 协程和 Flow 实现,使得它能够很简略地调用挂起函数并且解决数据流。
Paging 3 依然处于 alpha 版本,咱们须要您帮忙咱们进一步优化!请参阅以下资源开始应用 Paging:
- Android 开发文档|Paging 3 库概述
- Codelab|Android Paging
- 代码示例|Paging With Network Sample
- IssueTracker