转换数据流

使用分页数据时,您通常需要在加载数据流时对其进行转换。例如,您可能需要过滤一系列数据项,或者将数据项转换为其他类型,才能在界面中呈现它们。数据流转换的另一个常见用例是添加列表分隔符

概括来说,直接对数据流进行转换,您便可将仓库构造和界面构造保持分开。

本页面假定您熟悉 Paging 库的基本用法

执行基本转换

由于 PagingData 封装在响应式数据流中,因此您可以在加载数据之后、呈现数据之前逐步对数据执行转换操作。

为了对数据流中的每个 PagingData 对象执行转换, 请将转换操作放入对数据流的 map() 操作中:

pager.flow // Type is Flow<PagingData<User>>.
  // Map the outer stream so that the transformations are applied to
  // each new generation of PagingData.
  .map { pagingData ->
    // Transformations in this block are applied to the items
    // in the paged data.
}

转换数据

可对数据流进行的最基本操作就是将其转换为其他类型。获得对 PagingData 对象的访问权限后,您可以对 PagingData 对象的分页列表中的每个单项执行 map() 操作。

该操作的一个常见用例是将某个网络或数据库层对象映射到界面层中专用的某个对象。以下示例演示了如何执行此类映射操作:

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.map { user -> UiModel(user) }
  }

另一种常见的数据转换是获取用户输入(例如查询字符串),然后将其转换为要显示的请求输出。若要设置该数据转换,您需要监听并捕获用户查询输入、执行相应请求并将查询结果推送回界面。

您可以使用数据流 API 来监听查询输入。将数据流引用保留在 ViewModel 中。界面层不应直接访问该类;相反,应该定义一个函数来通知 ViewModel 相关用户查询。

private val queryFlow = MutableStateFlow("")

fun onQueryChanged(query: String) {
  queryFlow.value = query
}

当数据流中的查询值发生更改时,您可以执行操作将查询值转换为所需的数据类型,并将结果返回到界面层。具体的转换函数取决于所使用的语言和框架,但它们都可提供相似的功能。

val querySearchResults: Flow<User> = queryFlow.flatMapLatest { query ->
  // The database query returns a Flow which is output through
  // querySearchResults
  userDatabase.searchBy(query)
}

使用 flatMapLatestswitchMap 等操作可以确保只将最新结果返回到界面。如果用户在数据库操作结束前更改查询输入,这些操作会舍弃旧查询的结果,并立即启动新的搜索。

过滤数据

另一种常见操作是过滤。您可以根据用户条件来过滤数据;如果根据其他条件应该隐藏数据,您也可以从界面中移除相应数据。

您需要将这些过滤操作放入 map() 调用中,因为该过滤条件适用于 PagingData 对象。数据从 PagingData 中过滤掉后,系统会将新的 PagingData 实例传递到界面层进行显示。

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
  }

添加列表分隔符

Paging 库支持动态列表分隔符。您可以通过将分隔符作为布局中的可组合项直接插入到数据流中来提高列表的可读性。因此,分隔符是功能完备的可组合项,可支持完全互动、样式设置和无障碍功能语义。

将分隔符插入到分页列表中需要执行以下三个步骤:

  1. 转换界面模型,以适应分隔符项。一种方法是将数据项和分隔符封装到单个密封类中。这样,界面就可以在同一列表中处理多种项类型。
  2. 转换数据流,以便在加载数据之后、呈现数据之前动态添加分隔符。
  3. 更新界面,以处理分隔符项。

转换界面模型

Paging 库将列表分隔符作为实际的列表项插入到界面中,但分隔符项必须与列表中的数据项区分开来,以确保两种可组合项类型呈现出不同的效果。 解决方案是创建一个具有表示数据和分隔符的子类的 Kotlin 密封 类 。或者,您也可以创建一个可由您的列表项类和分隔符类扩展的基类。

假设您想要向 User 项的分页列表添加分隔符。以下代码段展示了如何创建基类,其中的实例可以是 UserModelSeparatorModel

sealed class UiModel {
  class UserModel(val id: String, val label: String) : UiModel() {
    constructor(user: User) : this(user.id, user.label)
  }

  class SeparatorModel(val description: String) : UiModel()
}

转换数据流

在加载数据流之后、呈现数据流之前必须对数据流进行转换。转换过程应该执行以下操作:

  • 转换加载的列表项,以反映新的基本项类型。
  • 使用 PagingData.insertSeparators() 方法添加分隔符。

如需详细了解转换操作,请参阅执行基本转换

以下示例演示了用于将 PagingData<User> 流更新为 PagingData<UiModel> 流的转换操作,其中添加了分隔符 :

pager.flow.map { pagingData: PagingData<User> ->
  // Map outer stream, so you can perform transformations on
  // each paging generation.
  pagingData
  .map { user ->
    // Convert items in stream to UiModel.UserModel.
    UiModel.UserModel(user)
  }
  .insertSeparators<UiModel.UserModel, UiModel> { before, after ->
    when {
      before == null -> UiModel.SeparatorModel("HEADER")
      after == null -> UiModel.SeparatorModel("FOOTER")
      shouldSeparate(before, after) -> UiModel.SeparatorModel(
        "BETWEEN ITEMS $before AND $after"
      )
      // Return null to avoid adding a separator between two items.
      else -> null
    }
  }
}

处理界面中的分隔符

最后一步是更改界面以适应分隔符项类型。在延迟布局中,您可以通过检查每个发出的 UiModel 的类型来处理多种项类型。在迭代分页数据时,请使用 when 语句调用相应的可组合项。这样,您就可以为数据项和分隔符提供不同的界面。

@Composable fun UserList(pagingItems: LazyPagingItems) {
  LazyColumn {
    items(
      count = pagingItems.itemCount,
      key = { index ->
        val item = pagingItems.peek(index)
        when (item) {
          is UiModel.UserModel -> item.user.id
          is UiModel.SeparatorModel -> item.description
          else -> index
        }
      }
    ) { index ->
      when (val item = pagingItems[index]) {
        is UiModel.UserModel -> UserItemComposable(item.user)
        is UiModel.SeparatorModel -> SeparatorComposable(item.description)
        null -> PlaceholderComposable()
      }
    }
  }
}

避免重复工作

需要避免的一个主要问题是让应用执行不必要的工作。提取数据是一项成本高昂的操作,并且数据转换也需要花费宝贵的时间。 一旦数据加载完毕并准备好在界面中显示,系统就应该保存数据,以备在发生配置更改且需要重新创建界面时使用。

cachedIn() 操作会缓存在它执行之前发生的任何转换的结果。通常,您会在 ViewModel 中应用此运算符,然后再向可组合项公开 Flow

如需正确管理缓存,请将 CoroutineScope 传递给 cachedIn(),如以下使用 viewModelScope 的示例所示。

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
      .map { user -> UiModel.UserModel(user) }
  }
  .cachedIn(viewModelScope)

如需详细了解如何将 cachedIn() 用于 PagingData 流,请参阅 设置 PagingData 流

其他资源

如需详细了解 Paging 库,请参阅下列其他资源:

文档

查看内容