データ ストリームを変換する

ページング データを扱う場合、読み込み時にデータ ストリームの変換が必要になることがよくあります。たとえば、UI でアイテムを表示する前に、アイテムのリストをフィルタリングしたり、アイテムを別の型に変換したりする必要が生じる場合があります。また、データ ストリームの変換に関するよくあるユースケースとして、リスト セパレータの追加もあります。

一般に、データ ストリームに直接変換を適用することで、リポジトリの構造と UI の構造を別々に維持できるようになります。

このページは、読者がページング ライブラリの基本的な使用について熟知していることを前提としています。

基本的な変換を適用する

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() オペレーションを実行できます。

一般的なユースケースの 1 つは、ネットワーク レイヤまたはデータベース レイヤのオブジェクトを UI レイヤ向けのオブジェクトにマッピングすることです。下の例では、このタイプのマップ オペレーションを適用する方法を示しています。

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

もう 1 つの一般的なデータ変換では、クエリ文字列など、ユーザーからの入力を取得し、表示するリクエスト出力に変換します。この設定を行うには、ユーザーのクエリ入力をリッスンして取得し、リクエストを実行して、クエリ結果を UI に返す必要があります。

ストリーム API を使用してクエリ入力をリッスンできます。ストリーム参照は ViewModel に保持します。UI レイヤからは直接アクセスできないようにします。代わりに、ユーザーのクエリを ViewModel に知らせる関数を定義します。

private val queryFlow = MutableStateFlow("")

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

データ ストリームでクエリ値が変更されると、クエリ値を目的のデータ型に変換して UI レイヤに結果を返すオペレーションを実行できます。特定の変換関数は、使用する言語とフレームワークによって異なりますが、いずれも同様の機能を提供します。

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

flatMapLatest または switchMap のようなオペレーションを使用すると、最新の結果のみが UI に返されます。データベース オペレーションが完了する前にユーザーがクエリ入力を変更した場合、これらのオペレーションによって古いクエリの結果が破棄され、新しい検索がすぐに開始されます。

データをフィルタする

もう 1 つの一般的なオペレーションはフィルタリングです。ユーザーの条件に基づいてデータをフィルタできます。また、他の条件に基づいて非表示にする必要がある場合は、UI からデータを削除できます。

フィルタは PagingData オブジェクトに適用されるため、これらのフィルタ オペレーションは、map() 呼び出し内に配置する必要があります。データが PagingData から除外されると、新しい PagingData インスタンスが UI レイヤに渡され、表示されます。

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

リスト セパレータを追加する

ページング ライブラリは、動的なリスト セパレータをサポートしています。レイアウトのコンポーザブルとしてセパレータをデータ ストリームに直接挿入することで、リストの読みやすさを向上できます。そのため、セパレータは機能豊富なコンポーザブルになります。完全なインタラクティビティ、スタイリング、ユーザー補助セマンティクスを可能にします。

ページング リストにセパレータを挿入するには、以下の 3 つのステップを行います。

  1. セパレータ アイテムに対応するように UI モデルを変換します。これを行う方法の 1 つは、データアイテムとセパレータを 1 つのシールクラスにラップすることです。これにより、UI は同じリスト内の複数のアイテムタイプを処理できます。
  2. データ ストリームを変換して、データの読み込みと表示の間にセパレータを動的に追加します。
  3. セパレータ アイテムを処理するように UI を更新します。

UI モデルを変換する

Paging ライブラリは、リスト セパレータを実際のリストアイテムとして UI に挿入しますが、セパレータのアイテムをリスト内のデータアイテムと区別できるようにする必要があります。これは、両方のコンポーザブル タイプが明確にレンダリングされるようにするためです。この解決策は、データとセパレータを表すサブクラスを持つ Kotlin シール クラス を作成することです。また、リストアイテム クラスとセパレータ クラスによって拡張される基本クラスを作成することもできます。

セパレータを User アイテムのページング リストに追加するとします。次のスニペットは、インスタンスが UserModel または SeparatorModel のいずれかになる基本クラスを作成する方法を示しています。

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
    }
  }
}

UI でセパレータを処理する

最後のステップとして、セパレータのアイテムタイプに対応するように UI を変更します。Lazy レイアウトでは、発行された各 UiModel のタイプを確認することで、複数のアイテムタイプを処理できます。ページング データを反復処理する場合は、when ステートメントを使用して適切なコンポーザブルを呼び出します。これにより、データアイテムとセパレータに異なる UI を提供できます。

@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()
      }
    }
  }
}

作業の重複を避ける

避けるべき主な問題の 1 つは、アプリが不要な作業を行うことです。データの取得は負荷の高いオペレーションであり、データ変換も貴重な時間を消費します。データが読み込まれて UI に表示される準備が整ったら、構成の変更が発生して UI を再作成する必要がある場合に備えて、データを保存する必要があります。

cachedIn() オペレーションは、それ以前に発生した変換の結果をキャッシュに保存します。通常、この演算子は、コンポーザブルに Flow を公開する前に ViewModel で適用します。

キャッシュを正しく管理するには、次の例のように viewModelScope を使用して、CoroutineScopecachedIn() に渡します。

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

PagingData のストリームで cachedIn() を使用する方法の詳細については、 PagingData のストリームを設定するをご覧ください。

参考情報

ページング ライブラリについて詳しくは、以下の参考情報をご覧ください。

ドキュメント

Views のコンテンツ