Datenstreams umwandeln

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

Im Allgemeinen können Sie durch direkte Anwendung von Transformationen auf den Datenstream Ihre Repository- und UI-Konstrukte voneinander trennen.

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 Transformationsvorgänge zwischen dem Laden und Präsentieren der Daten inkrementell auf die Daten anwenden.

Damit Sie Transformationen auf jedes PagingData-Objekt im Stream anwenden können, müssen Sie sie in einem map()-Vorgang im Stream platzieren:

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 umwandeln

Der einfachste Vorgang an einem Datenstream ist das Konvertieren in einen anderen Typ. Sobald Sie Zugriff auf das PagingData-Objekt haben, können Sie für jedes einzelne Element in der seitenbasierten Liste innerhalb des PagingData-Objekts einen map()-Vorgang ausführen.

Ein häufiger Anwendungsfall hierfür ist die Zuordnung eines Objekts auf Netzwerk- oder Datenbankebene zu einem Objekt, das speziell auf der UI-Ebene verwendet wird. Das folgende Beispiel zeigt, wie diese Art von Kartenvorgang angewendet wird:

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 häufige Datenkonvertierung besteht darin, eine Eingabe des Nutzers, z. B. einen Abfragestring, in die anzuzeigende Anfrageausgabe zu konvertieren. Dazu muss die Abfrageeingabe des Nutzers überwacht und erfasst, die Anfrage ausgeführt und das Abfrageergebniss zurück an die UI gesendet werden.

Sie können mit einer Stream API auf die Abfrageeingabe warten. Behalten Sie die Streamreferenz im 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 zu konvertieren und das Ergebnis an die UI-Ebene zurückzugeben. Die spezifische Konvertierungsfunktion hängt von der verwendeten Sprache und dem verwendeten Framework ab, aber sie bieten 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)
);

Vorgänge wie flatMapLatest oder switchMap sorgen dafür, dass nur die neuesten Ergebnisse an die UI zurückgegeben werden. Wenn der Nutzer seine Abfrageeingabe ändert, bevor der Datenbankvorgang abgeschlossen ist, werden durch diese Vorgänge die Ergebnisse der alten Abfrage verworfen und sofort die neue Suche gestartet.

Daten filtern

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

Sie müssen diese Filtervorgänge innerhalb des Aufrufs map() platzieren, da der Filter für das Objekt PagingData gilt. Sobald die Daten aus der 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 der Liste verbessern, indem Sie Trennzeichen direkt in den Datenstream als RecyclerView-Listenelemente einfügen. Trennzeichen sind daher ViewHolder-Objekte mit vollem Funktionsumfang, die die Interaktivität, den Fokus auf Barrierefreiheit und alle anderen Funktionen ermöglichen, die von einem View-Objekt bereitgestellt werden.

Um Trennzeichen in Ihre seitenbasierte Liste einzufügen, sind drei Schritte erforderlich:

  1. Konvertieren Sie das UI-Modell, damit es die Trennelemente aufnehmen kann.
  2. Transformieren Sie den Datenstream so, dass die Trennzeichen zwischen dem Laden der Daten und der Präsentation der Daten dynamisch hinzugefügt werden.
  3. Aktualisieren Sie die Benutzeroberfläche für die Verarbeitung von Trennelementen.

UI-Modell konvertieren

Die Paging-Bibliothek fügt Listentrennzeichen als tatsächliche Listenelemente in die RecyclerView ein. Sie müssen jedoch von den Datenelementen in der Liste unterscheidbar sein, damit sie an einen anderen ViewHolder-Typ mit einer eindeutigen UI gebunden werden können. Die Lösung besteht darin, eine versiegelte Kotlin-Klasse mit abgeleiteten Klassen zu erstellen, die Ihre Daten und Ihre Trennzeichen darstellen. Alternativ können Sie eine Basisklasse erstellen, die durch Ihre Listenelementklasse und Ihre Trennzeichenklasse erweitert wird.

Angenommen, Sie möchten Trennzeichen zu einer seitenbasierten Liste mit User-Elementen hinzufügen. Das folgende Snippet zeigt, wie Sie eine Basisklasse erstellen, bei der die Instanzen entweder ein UserModel- oder ein SeparatorModel-Element 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 nach dem Laden und vor der Präsentation anwenden. Die Transformationen sollten Folgendes tun:

  • Konvertieren Sie die geladenen Listenelemente so, dass sie dem neuen Basiselementtyp entsprechen.
  • Verwenden Sie die Methode PagingData.insertSeparators(), um die Trennzeichen hinzuzufügen.

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

Das folgende Beispiel zeigt Transformationsvorgänge zum Aktualisieren des Streams PagingData<User> in einen PagingData<UiModel>-Stream mit hinzugefügten Trennzeichen:

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

Trennzeichen in der Benutzeroberfläche verarbeiten

Der letzte Schritt besteht darin, die Benutzeroberfläche so zu ändern, dass sie den Elementtyp des Trennzeichens berücksichtigt. Erstellen Sie ein Layout und einen Ansichtsinhaber für die Trennzeichenelemente und ändern Sie den Listenadapter so, dass er RecyclerView.ViewHolder als Halterungstyp verwendet, sodass er mehrere Arten von Ansichtsinhabern verarbeiten kann. Alternativ können Sie eine gemeinsame Basisklasse definieren, die sowohl die Element- als auch die Halterklassen der Trennansicht erweitern.

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

  • Fügen Sie den Methoden onCreateViewHolder() und onBindViewHolder() Fälle hinzu, um Trennzeichenlistenelemente 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

Ein wichtiges Problem, das Sie vermeiden sollten, ist, dass die App unnötige Arbeit erledigt. Das Abrufen von Daten ist ein teurer Vorgang und Datentransformationen können ebenfalls wertvolle Zeit in Anspruch nehmen. Sobald die Daten geladen und für die Anzeige in der UI vorbereitet wurden, sollten sie gespeichert werden, falls eine Konfigurationsänderung vorgenommen wird und die UI neu erstellt werden muss.

Der cachedIn()-Vorgang speichert die Ergebnisse aller Transformationen, die davor stattfinden. 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 Ressourcen:

Codelabs

Produktproben