ページング データを扱う場合、読み込み時にデータ ストリームの変換が必要になることがよくあります。たとえば、UI でアイテムを表示する前に、アイテムのリストをフィルタリングしたり、アイテムを別の型に変換したりする必要が生じる場合があります。また、データ ストリームの変換に関するよくあるユースケースとして、リスト セパレータの追加もあります。
一般に、データ ストリームに直接変換を適用することで、リポジトリの構造と UI の構造を別々に維持できるようになります。
このページは、読者がページング ライブラリの基本的な使用について熟知していることを前提としています。
基本的な変換を適用する
PagingData
はリアクティブ ストリーム内にカプセル化されるため、データの読み込みと表示の間に段階的にデータに変換オペレーションを適用できます。
ストリーム内の各 PagingData
オブジェクトに変換を適用するには、ストリームの map()
オペレーション内に変換を配置します。
Kotlin
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. }
Java
PagingRx.getFlowable(pager) // Type is Flowable<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. });
Java
// Map the outer stream so that the transformations are applied to // each new generation of PagingData. Transformations.map( // Type is LiveData<PagingData<User>>. PagingLiveData.getLiveData(pager), pagingData -> { // Transformations in this block are applied to the items // in the paged data. });
データを変換する
データ ストリームの最も基本的なオペレーションは、別の型に変換することです。PagingData
オブジェクトにアクセスできるようになると、PagingData
オブジェクト内のページング リストにある各アイテムに対して map()
オペレーションを実行できます。
一般的なユースケースの 1 つは、ネットワーク レイヤまたはデータベース レイヤのオブジェクトを UI レイヤ向けのオブジェクトにマッピングすることです。下の例では、このタイプのマップ オペレーションを適用する方法を示しています。
Kotlin
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.map { user -> UiModel(user) } }
Java
// Type is Flowable<PagingData<User>>. PagingRx.getFlowable(pager) .map(pagingData -> pagingData.map(UiModel.UserModel::new) )
Java
Transformations.map( // Type is LiveData<PagingData<User>>. PagingLiveData.getLiveData(pager), pagingData -> pagingData.map(UiModel.UserModel::new) )
もう 1 つの一般的なデータ変換では、クエリ文字列など、ユーザーからの入力を取得し、表示するリクエスト出力に変換します。この設定を行うには、ユーザーのクエリ入力をリッスンして取得し、リクエストを実行して、クエリ結果を UI に返す必要があります。
ストリーム API を使用してクエリ入力をリッスンできます。ストリーム参照は ViewModel
に保持します。UI レイヤからは直接アクセスできないようにします。代わりに、ユーザーのクエリを ViewModel に知らせる関数を定義します。
Kotlin
private val queryFlow = MutableStateFlow("") fun onQueryChanged(query: String) { queryFlow.value = query }
Java
private BehaviorSubject<String> querySubject = BehaviorSubject.create(""); public void onQueryChanged(String query) { queryFlow.onNext(query) }
Java
private MutableLiveData<String> queryLiveData = new MutableLiveData(""); public void onQueryChanged(String query) { queryFlow.setValue(query) }
データ ストリームでクエリ値が変更されると、クエリ値を目的のデータ型に変換して UI レイヤに結果を返すオペレーションを実行できます。特定の変換関数は、使用する言語とフレームワークによって異なりますが、いずれも同様の機能を提供します。
Kotlin
val querySearchResults = queryFlow.flatMapLatest { query -> // The database query returns a Flow which is output through // querySearchResults userDatabase.searchBy(query) }
Java
Observable<User> querySearchResults = querySubject.switchMap(query -> userDatabase.searchBy(query));
Java
LiveData<User> querySearchResults = Transformations.switchMap( queryLiveData, query -> userDatabase.searchBy(query) );
flatMapLatest
または switchMap
のようなオペレーションを使用すると、最新の結果のみが UI に返されます。データベース オペレーションが完了する前にユーザーがクエリ入力を変更した場合、これらのオペレーションによって古いクエリの結果が破棄され、新しい検索がすぐに開始されます。
データをフィルタする
もう 1 つの一般的なオペレーションはフィルタリングです。ユーザーの条件に基づいてデータをフィルタできます。また、他の条件に基づいて非表示にする必要がある場合は、UI からデータを削除できます。
フィルタは PagingData
オブジェクトに適用されるため、これらのフィルタ オペレーションは、map()
呼び出し内に配置する必要があります。データが PagingData
から除外されると、新しい PagingData
インスタンスが UI レイヤに渡され、表示されます。
Kotlin
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.filter { user -> !user.hiddenFromUi } }
Java
// Type is Flowable<PagingData<User>>. PagingRx.getFlowable(pager) .map(pagingData -> pagingData.filter(user -> !user.isHiddenFromUi()) ) }
Java
Transformations.map( // Type is LiveData<PagingData<User>>. PagingLiveData.getLiveData(pager), pagingData -> pagingData.filter(user -> !user.isHiddenFromUi()) )
リスト セパレータを追加する
ページング ライブラリは、動的なリスト セパレータをサポートしています。RecyclerView
のリストアイテムとしてセパレータをデータ ストリームに直接挿入することで、リストの読みやすさを向上できます。そのため、セパレータは機能豊富な ViewHolder
オブジェクトになります。インタラクティビティ、ユーザー補助フォーカス、その他 View
により提供される機能を可能にします。
ページング リストにセパレータを挿入するには、以下の 3 つのステップを行います。
- セパレータ アイテムに対応するように UI モデルを変換します。
- データ ストリームを変換して、データの読み込みと表示の間にセパレータを動的に追加します。
- セパレータ アイテムを処理するように UI を更新します。
UI モデルを変換する
ページング ライブラリは、リスト セパレータを実際のリストアイテムとして RecyclerView
に挿入しますが、セパレータのアイテムをリスト内の他のアイテムと区別できるようにする必要があります。これは、UI が異なる別の ViewHolder
タイプにバインドできるようにするためです。この解決策は、データとセパレータを表すサブクラスを持つ Kotlin シールクラスを作成することです。また、リストアイテム クラスとセパレータ クラスによって拡張される基本クラスを作成することもできます。
セパレータを User
アイテムのページング リストに追加するとします。次のスニペットは、インスタンスが UserModel
または SeparatorModel
のいずれかになる基本クラスを作成する方法を示しています。
Kotlin
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() }
Java
class UiModel { private UiModel() {} static class UserModel extends UiModel { @NonNull private String mId; @NonNull private String mLabel; UserModel(@NonNull String id, @NonNull String label) { mId = id; mLabel = label; } UserModel(@NonNull User user) { mId = user.id; mLabel = user.label; } @NonNull public String getId() { return mId; } @NonNull public String getLabel() { return mLabel; } } static class SeparatorModel extends UiModel { @NonNull private String mDescription; SeparatorModel(@NonNull String description) { mDescription = description; } @NonNull public String getDescription() { return mDescription; } } }
Java
class UiModel { private UiModel() {} static class UserModel extends UiModel { @NonNull private String mId; @NonNull private String mLabel; UserModel(@NonNull String id, @NonNull String label) { mId = id; mLabel = label; } UserModel(@NonNull User user) { mId = user.id; mLabel = user.label; } @NonNull public String getId() { return mId; } @NonNull public String getLabel() { return mLabel; } } static class SeparatorModel extends UiModel { @NonNull private String mDescription; SeparatorModel(@NonNull String description) { mDescription = description; } @NonNull public String getDescription() { return mDescription; } } }
データ ストリームを変換する
データ ストリームを読み込んだ後、それを表示する前に変換を適用する必要があります。この変換では、以下の処理を行います。
- 読み込まれたリストアイテムを変換して、新しい基本アイテムタイプを反映させます。
PagingData.insertSeparators()
メソッドを使用してセパレータを追加します。
変換オペレーションの詳細については、基本的な変換を適用するをご覧ください。
次の例は、セパレータを追加して PagingData<User>
ストリームを PagingData<UiModel>
ストリームに更新する変換オペレーションを示しています。
Kotlin
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 } } }
Java
// Map outer stream, so you can perform transformations on each // paging generation. PagingRx.getFlowable(pager).map(pagingData -> { // First convert items in stream to UiModel.UserModel. PagingData<UiModel> uiModelPagingData = pagingData.map( UiModel.UserModel::new); // Insert UiModel.SeparatorModel, which produces PagingData of // generic type UiModel. return PagingData.insertSeparators(uiModelPagingData, (@Nullable UiModel before, @Nullable UiModel after) -> { if (before == null) { return new UiModel.SeparatorModel("HEADER"); } else if (after == null) { return new UiModel.SeparatorModel("FOOTER"); } else if (shouldSeparate(before, after)) { return new UiModel.SeparatorModel("BETWEEN ITEMS " + before.toString() + " AND " + after.toString()); } else { // Return null to avoid adding a separator between two // items. return null; } }); });
Java
// Map outer stream, so you can perform transformations on each // paging generation. Transformations.map(PagingLiveData.getLiveData(pager), pagingData -> { // First convert items in stream to UiModel.UserModel. PagingData<UiModel> uiModelPagingData = pagingData.map( UiModel.UserModel::new); // Insert UiModel.SeparatorModel, which produces PagingData of // generic type UiModel. return PagingData.insertSeparators(uiModelPagingData, (@Nullable UiModel before, @Nullable UiModel after) -> { if (before == null) { return new UiModel.SeparatorModel("HEADER"); } else if (after == null) { return new UiModel.SeparatorModel("FOOTER"); } else if (shouldSeparate(before, after)) { return new UiModel.SeparatorModel("BETWEEN ITEMS " + before.toString() + " AND " + after.toString()); } else { // Return null to avoid adding a separator between two // items. return null; } }); });
UI でセパレータを処理する
最後のステップとして、セパレータのアイテムタイプに対応するように UI を変更します。セパレータ アイテムのレイアウトとビューホルダーを作成し、リスト アダプターを変更して、ビューホルダーの型として RecyclerView.ViewHolder
を使用することで、複数の型のビューホルダーを処理できるようにします。また、アイテムとセパレータの両方のビューホルダー クラスが拡張する共通の基本クラスを定義することもできます。
さらに、リスト アダプターを以下のように変更します。
onCreateViewHolder()
メソッドとonBindViewHolder()
メソッドにケースを追加して、セパレータのリストアイテムを説明します。- 新しいコンパレータを実装します。
Kotlin
class UiModelAdapter : PagingDataAdapter<UiModel, RecyclerView.ViewHolder>(UiModelComparator) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ) = when (viewType) { R.layout.item -> UserModelViewHolder(parent) else -> SeparatorModelViewHolder(parent) } override fun getItemViewType(position: Int) { // Use peek over getItem to avoid triggering page fetch / drops, since // recycling views is not indicative of the user's current scroll position. return when (peek(position)) { is UiModel.UserModel -> R.layout.item is UiModel.SeparatorModel -> R.layout.separator_item null -> throw IllegalStateException("Unknown view") } } override fun onBindViewHolder( holder: RecyclerView.ViewHolder, position: Int ) { val item = getItem(position) if (holder is UserModelViewHolder) { holder.bind(item as UserModel) } else if (holder is SeparatorModelViewHolder) { holder.bind(item as SeparatorModel) } } } object UiModelComparator : DiffUtil.ItemCallback<UiModel>() { override fun areItemsTheSame( oldItem: UiModel, newItem: UiModel ): Boolean { val isSameRepoItem = oldItem is UiModel.UserModel && newItem is UiModel.UserModel && oldItem.id == newItem.id val isSameSeparatorItem = oldItem is UiModel.SeparatorModel && newItem is UiModel.SeparatorModel && oldItem.description == newItem.description return isSameRepoItem || isSameSeparatorItem } override fun areContentsTheSame( oldItem: UiModel, newItem: UiModel ) = oldItem == newItem }
Java
class UiModelAdapter extends PagingDataAdapter<UiModel, RecyclerView.ViewHolder> { UiModelAdapter() { super(new UiModelComparator(), Dispatchers.getMain(), Dispatchers.getDefault()); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == R.layout.item) { return new UserModelViewHolder(parent); } else { return new SeparatorModelViewHolder(parent); } } @Override public int getItemViewType(int position) { // Use peek over getItem to avoid triggering page fetch / drops, since // recycling views is not indicative of the user's current scroll position. UiModel item = peek(position); if (item instanceof UiModel.UserModel) { return R.layout.item; } else if (item instanceof UiModel.SeparatorModel) { return R.layout.separator_item; } else { throw new IllegalStateException("Unknown view"); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceOf UserModelViewHolder) { UserModel userModel = (UserModel) getItem(position); ((UserModelViewHolder) holder).bind(userModel); } else { SeparatorModel separatorModel = (SeparatorModel) getItem(position); ((SeparatorModelViewHolder) holder).bind(separatorModel); } } } class UiModelComparator extends DiffUtil.ItemCallback<UiModel> { @Override public boolean areItemsTheSame(@NonNull UiModel oldItem, @NonNull UiModel newItem) { boolean isSameRepoItem = oldItem instanceof UserModel && newItem instanceof UserModel && ((UserModel) oldItem).getId().equals(((UserModel) newItem).getId()); boolean isSameSeparatorItem = oldItem instanceof SeparatorModel && newItem instanceof SeparatorModel && ((SeparatorModel) oldItem).getDescription().equals( ((SeparatorModel) newItem).getDescription()); return isSameRepoItem || isSameSeparatorItem; } @Override public boolean areContentsTheSame(@NonNull UiModel oldItem, @NonNull UiModel newItem) { return oldItem.equals(newItem); } }
Java
class UiModelAdapter extends PagingDataAdapter<UiModel, RecyclerView.ViewHolder> { UiModelAdapter() { super(new UiModelComparator(), Dispatchers.getMain(), Dispatchers.getDefault()); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == R.layout.item) { return new UserModelViewHolder(parent); } else { return new SeparatorModelViewHolder(parent); } } @Override public int getItemViewType(int position) { // Use peek over getItem to avoid triggering page fetch / drops, since // recycling views is not indicative of the user's current scroll position. UiModel item = peek(position); if (item instanceof UiModel.UserModel) { return R.layout.item; } else if (item instanceof UiModel.SeparatorModel) { return R.layout.separator_item; } else { throw new IllegalStateException("Unknown view"); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { if (holder instanceOf UserModelViewHolder) { UserModel userModel = (UserModel) getItem(position); ((UserModelViewHolder) holder).bind(userModel); } else { SeparatorModel separatorModel = (SeparatorModel) getItem(position); ((SeparatorModelViewHolder) holder).bind(separatorModel); } } } class UiModelComparator extends DiffUtil.ItemCallback<UiModel> { @Override public boolean areItemsTheSame(@NonNull UiModel oldItem, @NonNull UiModel newItem) { boolean isSameRepoItem = oldItem instanceof UserModel && newItem instanceof UserModel && ((UserModel) oldItem).getId().equals(((UserModel) newItem).getId()); boolean isSameSeparatorItem = oldItem instanceof SeparatorModel && newItem instanceof SeparatorModel && ((SeparatorModel) oldItem).getDescription().equals( ((SeparatorModel) newItem).getDescription()); return isSameRepoItem || isSameSeparatorItem; } @Override public boolean areContentsTheSame(@NonNull UiModel oldItem, @NonNull UiModel newItem) { return oldItem.equals(newItem); } }
作業の重複を避ける
避けるべき主な問題の 1 つは、アプリが不要な作業を行うことです。データの取得は負荷の高いオペレーションであり、データ変換も貴重な時間を消費します。データが読み込まれて UI に表示される準備が整ったら、構成の変更が発生して UI を再作成する必要がある場合に備えて、データを保存する必要があります。
cachedIn()
オペレーションは、それ以前に発生した変換の結果をキャッシュに保存します。そのため、cachedIn()
は ViewModel の最後の呼び出しにする必要があります。
Kotlin
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.filter { user -> !user.hiddenFromUi } .map { user -> UiModel.UserModel(user) } } .cachedIn(viewModelScope)
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); PagingRx.cachedIn( // Type is Flowable<PagingData<User>>. PagingRx.getFlowable(pager) .map(pagingData -> pagingData .filter(user -> !user.isHiddenFromUi()) .map(UiModel.UserModel::new)), viewModelScope); }
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); PagingLiveData.cachedIn( Transformations.map( // Type is LiveData<PagingData<User>>. PagingLiveData.getLiveData(pager), pagingData -> pagingData .filter(user -> !user.isHiddenFromUi()) .map(UiModel.UserModel::new)), viewModelScope);
PagingData
のストリームで cachedIn()
を使用する方法の詳細については、PagingData のストリームを設定するをご覧ください。
参考情報
ページング ライブラリについて詳しくは、以下の参考情報をご覧ください。
Codelab
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- ページング データを読み込む、表示する
- Paging の実装をテストする
- 読み込み状態の管理と表示