Datenstreams umwandeln

Wenn Sie mit paginaten Daten arbeiten, müssen Sie den Datenstream beim Laden häufig transformieren. Unter Umständen müssen Sie beispielsweise eine Liste von Elementen filtern oder Elemente in einen anderen Typ konvertieren, bevor Sie sie in der Benutzeroberfläche präsentieren. Ein weiterer häufiger Anwendungsfall für die Datenstream-Transformation ist das Hinzufügen von Listentrennzeichen.

Wenn Sie Transformationen direkt auf den Datenstream anwenden, können Sie Repository- und UI-Konstrukte getrennt voneinander halten.

Auf dieser Seite wird davon ausgegangen, dass Sie mit der grundlegenden Verwendung der Paging-Bibliothek vertraut sind.

Grundlegende Transformationen anwenden

Da PagingData in einem reaktiven Stream gekapselt ist, können Sie zwischen dem Laden und der Darstellung der Daten schrittweise Transformationsvorgänge auf die Daten anwenden.

Wenn Sie Transformationen auf jedes PagingData-Objekt im Stream anwenden möchten, fügen Sie sie in einen map()-Vorgang im Stream ein:

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.
  });

Daten konvertieren

Die einfachste Operation an einem Datenstream ist die Konvertierung in einen anderen Typ. Sobald Sie Zugriff auf das PagingData-Objekt haben, können Sie für jedes einzelne Element in der paginaten Liste im PagingData-Objekt eine map()-Operation ausführen.

Ein gängiger Anwendungsfall hierfür ist die Zuordnung eines Netzwerk- oder Datenbankebenenobjekts zu einem Objekt, das speziell in der UI-Ebene verwendet wird. Im folgenden Beispiel wird gezeigt, wie Sie diese Art von Kartenvorgang anwenden:

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

Eine weitere gängige Datenkonvertierung besteht darin, eine Eingabe des Nutzers, z. B. einen Abfragestring, in die anzuzeigende Anfrageausgabe umzuwandeln. Dazu müssen Sie die Abfrageeingabe des Nutzers abhören und erfassen, die Anfrage ausführen und das Abfrageergebnis an die Benutzeroberfläche zurückgeben.

Sie können mit einer Stream-API auf die Abfrageeingabe warten. Behalten Sie die Streamreferenz in Ihrem ViewModel. Die UI-Ebene sollte keinen direkten Zugriff darauf haben. Definieren Sie stattdessen eine Funktion, um das ViewModel über die Abfrage des Nutzers zu informieren.

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

Wenn sich der Abfragewert im Datenstream ändert, können Sie Vorgänge ausführen, um den Abfragewert in den gewünschten Datentyp umzuwandeln und das Ergebnis an die UI-Ebene zurückzugeben. Die spezifische Conversion-Funktion hängt von der verwendeten Sprache und dem verwendeten Framework ab, bietet aber alle ähnliche Funktionen.

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)
);

Mithilfe von Vorgängen wie flatMapLatest oder switchMap werden nur die neuesten Ergebnisse an die Benutzeroberfläche zurückgegeben. Wenn der Nutzer seine Abfrageeingabe ändert, bevor der Datenbankvorgang abgeschlossen ist, werden die Ergebnisse der alten Abfrage verworfen und die neue Suche sofort gestartet.

Daten filtern

Ein weiterer gängiger Vorgang ist das Filtern. Sie können Daten nach den Kriterien des Nutzers filtern oder Daten aus der Benutzeroberfläche entfernen, die aufgrund anderer Kriterien ausgeblendet werden sollen.

Sie müssen diese Filtervorgänge in den map()-Aufruf einfügen, da der Filter auf das PagingData-Objekt angewendet wird. Sobald die Daten aus PagingData herausgefiltert wurden, wird die neue PagingData-Instanz zur Anzeige an die UI-Ebene übergeben.

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

Listentrennzeichen hinzufügen

Die Paging-Bibliothek unterstützt dynamische Listentrennzeichen. Sie können die Lesbarkeit von Listen verbessern, indem Sie Trennzeichen direkt als RecyclerView-Listenelemente in den Datenstream einfügen. Trennlinien sind daher vollwertige ViewHolder-Objekte, die Interaktivität, Bedienungshilfen und alle anderen Funktionen eines View bieten.

Es sind drei Schritte erforderlich, um Trennzeichen in die paginaierte Liste einzufügen:

  1. Konvertieren Sie das UI-Modell, um die Trennelemente aufzunehmen.
  2. Transformieren Sie den Datenstream, um die Trennzeichen dynamisch zwischen dem Laden und dem Darstellen der Daten hinzuzufügen.
  3. Aktualisieren Sie die Benutzeroberfläche, damit Trennelemente verarbeitet werden.

UI-Modell konvertieren

Die Paging-Bibliothek fügt Listenelemente als tatsächliche Listenelemente in RecyclerView ein. Die Trennelemente müssen jedoch von den Datenelementen in der Liste unterschieden werden können, damit sie an einen anderen ViewHolder-Typ mit einer eigenen Benutzeroberfläche gebunden werden können. Die Lösung besteht darin, eine versiegelte Kotlin-Klasse mit Unterklassen zu erstellen, um Ihre Daten und Trennzeichen darzustellen. Alternativ können Sie eine Basisklasse erstellen, die durch Ihre Listenelementklasse und Ihre Trennstrichklasse erweitert wird.

Angenommen, Sie möchten einer paginaten Liste mit User Elementen Trennzeichen hinzufügen. Das folgende Snippet zeigt, wie Sie eine Basisklasse erstellen, deren Instanzen entweder eine UserModel oder eine SeparatorModel sein können:

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

Datenstream transformieren

Sie müssen Transformationen auf den Datenstream anwenden, nachdem Sie ihn geladen und bevor Sie ihn präsentieren. Die Transformationen sollten Folgendes tun:

  • Konvertieren Sie die geladenen Listenelemente in den neuen Basiselementtyp.
  • Verwenden Sie die Methode PagingData.insertSeparators(), um die Trennzeichen hinzuzufügen.

Weitere Informationen zu Transformationsvorgänge finden Sie unter Grundlegende Transformationen anwenden.

Im folgenden Beispiel werden Transformationsvorgänge gezeigt, mit denen der PagingData<User>-Stream in einen PagingData<UiModel>-Stream mit zusätzlichen Trennzeichen aktualisiert wird:

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

Trennlinien in der Benutzeroberfläche

Im letzten Schritt passen Sie die Benutzeroberfläche an den Artikeltyp „Separator“ an. Erstellen Sie ein Layout und einen Ansichtshalter für Ihre Trennelemente und ändern Sie den Listenadapter so, dass RecyclerView.ViewHolder als Ansichtshaltertyp verwendet wird, damit er mehrere Ansichtshaltertypen verarbeiten kann. Alternativ können Sie eine gemeinsame Basisklasse definieren, die sowohl von den Klassen für Artikel- als auch von den Klassen für Trennlinien-Viewholder abgeleitet wird.

Außerdem müssen Sie die folgenden Änderungen an Ihrem Listenadapter vornehmen:

  • Fügen Sie den Methoden onCreateViewHolder() und onBindViewHolder() Fälle hinzu, um Elemente in Trennlisten zu berücksichtigen.
  • Implementieren Sie einen neuen Vergleichsoperator.

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

Doppelte Arbeit vermeiden

Achten Sie darauf, dass die App nicht unnötige Arbeit leistet. Das Abrufen von Daten ist ein kostspieliger Vorgang und Datentransformationen können auch wertvolle Zeit in Anspruch nehmen. Nachdem die Daten geladen und für die Anzeige in der Benutzeroberfläche vorbereitet wurden, sollten sie gespeichert werden, falls eine Konfigurationsänderung auftritt und die Benutzeroberfläche neu erstellt werden muss.

Der Vorgang cachedIn() speichert die Ergebnisse aller Transformationen, die vor ihm ausgeführt werden, im Cache. Daher sollte cachedIn() der letzte Aufruf in Ihrem ViewModel sein.

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);

Weitere Informationen zur Verwendung von cachedIn() mit einem Stream von PagingData finden Sie unter Stream von PagingData einrichten.

Weitere Informationen

Weitere Informationen zur Paging-Bibliothek finden Sie in den folgenden zusätzlichen Ressourcen:

Codelabs