Paging 库提供了强大的功能,用于从较大的数据集加载和显示分页数据。本指南将演示如何使用 Paging 库设置来自网络数据源的分页数据流并将其显示在延迟列表中。
定义数据源
第一步是定义用于标识数据源的 PagingSource 实现。PagingSource API 类包含 load 方法,您需要替换该方法,以指明如何从相应数据源检索分页数据。
直接使用 PagingSource 类即可通过 Kotlin 协程进行异步加载。
选择键和值类型
PagingSource<Key, Value> 有两种类型参数:Key 和 Value。键定义了用于加载数据的标识符,值是数据本身的类型。例如,如果您通过将 Int 页码传递给 Retrofit 来从网络加载各页 User 对象,则应选择 Int 作为 Key 类型,选择 User 作为 Value 类型。
定义 PagingSource
下面的示例实现了按页码加载各页对象的 PagingSource。Key 类型为 Int,Value 类型为 User。
class ExamplePagingSource(
val backend: ExampleBackendService,
val query: String
) : PagingSource<Int, User>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, User> {
init {
// the data source is expected to be immutable
// invalidate PagingSource if data source
// has updated
backEnd.addDatabaseOnChangedListener {
invalidate()
}
}
try {
// Start refresh at page 1 if undefined.
val nextPageNumber = params.key ?: 1
val response = backend.searchUsers(query, nextPageNumber)
return LoadResult.Page(
data = response.users,
prevKey = null, // Only paging forward.
nextKey = nextPageNumber + 1
)
} catch (e: Exception) {
// Handle errors in this block and return LoadResult.Error for
// expected errors (such as a network failure).
}
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
// Try to find the page key of the closest page to anchorPosition from
// either the prevKey or the nextKey; you need to handle nullability
// here.
// * prevKey == null -> anchorPage is the first page.
// * nextKey == null -> anchorPage is the last page.
// * both prevKey and nextKey are null -> anchorPage is the
// initial page, so return null.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
典型的 PagingSource 实现会将其构造函数中提供的参数传递给 load 方法,以便为查询加载适当的数据。在上面的示例中,这些参数包括:
backend:提供数据的后端服务实例。query:要发送到backend指示的服务的搜索查询。
LoadParams 对象包含有关要执行的加载操作的信息,这包括要加载的键和要加载的内容数量。如需区分 load 中的加载类型,请检查传入的 LoadParams 子类:LoadParams.Refresh、LoadParams.Append 或 LoadParams.Prepend。
LoadResult 对象包含加载操作的结果。LoadResult 是一个密封类,根据 load 调用是否成功,它会采用以下三种形式之一:
- 如果加载成功,则返回
LoadResult.Page对象。如果无法在该方向加载更多数据,则为nextKey或prevKey传递null。 - 如果加载失败,则返回
LoadResult.Error对象。 - 如果
PagingSource因底层数据发生更改而不再有效,则返回LoadResult.Invalid对象。
下图说明了此示例中的 load 函数如何接收每次加载的键并为后续加载提供键。
load 如何使用和更新键的流程图。
PagingSource 实现还必须实现 getRefreshKey 方法,该方法接受 PagingState 对象作为参数。当数据在初始加载后刷新或失效时,该方法会返回要传递给 load 方法的键。在后续刷新数据时,Paging 库会自动调用此方法。对于基于偏移的分页(其中键表示项位置),您可以计算相对于 state.anchorPosition 的刷新键:
// Only for positional or offset PagingSources where Key is a 0-based item index
// rather than a page number.
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
// Center the refreshed load around the user's current viewport so the list
// does not jump back to the top on invalidation.
return state.anchorPosition?.let { anchorPosition ->
(anchorPosition - state.config.initialLoadSize / 2).coerceAtLeast(0)
}
}
处理错误
数据加载请求可能因多种原因而失败,特别是在通过网络加载时。通过从 load 方法返回 LoadResult.Error 对象,可报告在加载过程中遇到的错误。
例如,对于上一个示例,您可以通过向 load 方法添加以下内容来捕获和报告 ExamplePagingSource 中的加载错误:
catch (e: IOException) {
// IOException for network failures.
return LoadResult.Error(e)
} catch (e: HttpException) {
// HttpException for any non-2xx HTTP status codes.
return LoadResult.Error(e)
}
如需详细了解如何处理 Retrofit 错误,请参阅 PagingSource API 参考文档中的示例。
PagingSource 会收集 LoadResult.Error 对象并将其传递给界面,以便您对其执行操作。如需详细了解如何在界面中显示加载状态,请参阅管理和显示加载状态。
设置 PagingData 流
接下来,您需要来自 PagingSource 实现的分页数据流。在 ViewModel 中设置数据流。Pager 类提供的方法可显示来自 PagingSource 的 PagingData 对象的响应式流。Paging 库会将数据流公开为 Flow。
创建 Pager 实例以设置响应式流时,您必须为该实例提供 PagingConfig 配置对象以及一个函数,该函数用于告知 Pager 如何获取 PagingSource 实现的实例。您还可以指定可选的 initialKey,例如 1(表示从 1 开始的页码)或 0(表示基于偏移量的分页)。在 PagingConfig 中,您可以配置 pageSize、initialLoadSize、prefetchDistance 和 enablePlaceholders。prefetchDistance 属性定义了从已加载内容的任一边缘开始,有多少项会触发下一次提取。
class UserViewModel(
private val backend: ExampleBackendService,
private val query: String
) : ViewModel() {
val userPagingFlow: Flow<PagingData<User>> = Pager(
config = PagingConfig(
pageSize = 20,
// Load 3 pages initially so the user can scroll immediately without
// hitting a loading state.
initialLoadSize = 60,
// Fetch the next page far enough ahead of the scroll position to
// avoid visible pauses.
prefetchDistance = 20,
enablePlaceholders = true
),
initialKey = 1,
pagingSourceFactory = {
ExamplePagingSource(backend, query)
}
)
.flow
.cachedIn(viewModelScope)
}
cachedIn 运算符使数据流可共享,并使用提供的 CoroutineScope 缓存加载的数据。如果没有 cachedIn,则无法重新收集 PagingData。此示例使用生命周期 lifecycle-viewmodel-ktx 工件提供的 viewModelScope。
Pager 对象会调用 PagingSource 对象的 load 方法,为其提供 LoadParams 对象,并接收 LoadResult 对象作为交换。
在界面中收集和显示数据
如需将分页数据流连接到界面,请从 ViewModel 获取数据流,并将其传递给列表可组合项。
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
val userFlow = viewModel.userPagingFlow
UserList(flow = userFlow)
}
使用 collectAsLazyPagingItems 将 PagingData 流转换为 LazyPagingItems。然后,在 LazyColumn 中使用 items API 来布置每个项。
请务必使用 itemKey 为每件商品提供一个唯一且稳定的标识符。
以下示例使用 it.id(引用 User.id 属性),因为它在数据更新期间对 User 实例保持稳定。
@Composable
fun UserList(flow: Flow<PagingData<User>>) {
val lazyPagingItems = flow.collectAsLazyPagingItems()
LazyColumn {
items(
lazyPagingItems.itemCount,
key = lazyPagingItems.itemKey { it.id }
) { index ->
val user = lazyPagingItems[index]
if (user != null) {
UserRow(user)
} else {
UserPlaceholder()
}
}
}
}
分页库会在页面加载时使用 null 作为占位符,因此,如果您启用了占位符,则必须在内容块中处理 null 值。
现在,列表会显示分页数据,并且 Paging 库会在用户滚动时加载更多页面。
其他资源
如需详细了解 Paging 库,请参阅下列其他资源:
文档
查看内容
为你推荐
- 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
- 从网络和数据库加载页面
- 迁移到 Paging 3
- Paging 库概览