加载并显示分页数据

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 调用中,ExamplePagingSource 都会接受当前键并返回要加载的下一个键。
图 1. 显示 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 库,请参阅下列其他资源:

文档

查看内容