Datenstreams umwandeln

Wenn Sie mit seitenbasierten Daten arbeiten, müssen Sie den Datenstream häufig transformieren, während Sie ihn laden. Beispielsweise müssen Sie möglicherweise 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 Transformation von Datenstreams ist das Hinzufügen von Listentrennzeichen.

Allgemeiner gesagt: Wenn Sie Transformationen direkt auf den Datenstream anwenden, können Sie Ihre Repository- und UI-Konstrukte getrennt 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 Transformationen inkrementell auf die Daten anwenden, zwischen dem Laden der Daten und der Präsentation.

Wenn Sie Transformationen auf jedes PagingData Objekt im Stream anwenden möchten, platzieren Sie die Transformationen in einem map() Vorgang für den Stream:

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

Daten konvertieren

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

Ein häufiger Anwendungsfall hierfür ist das Zuordnen eines Netzwerk- oder Datenbankebenenobjekts zu einem Objekt, das speziell in der UI-Ebene verwendet wird. Das folgende Beispiel zeigt, wie Sie diese Art von Zuordnungsvorgang anwenden:

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

Eine weitere häufige Datenkonvertierung besteht darin, eine Eingabe vom Nutzer, z. B. eine Suchanfrage, zu übernehmen und in die anzuzeigende Anfrageausgabe zu konvertieren. Dazu müssen Sie die Suchanfrage des Nutzers erfassen, die Anfrage ausführen und das Suchergebnis an die Benutzeroberfläche zurückgeben.

Sie können eine Stream-API verwenden, um die Suchanfrage zu erfassen. Bewahren Sie die Stream-Referenz in Ihrem ViewModel auf. Die UI-Ebene sollte keinen direkten Zugriff darauf haben. Definieren Sie stattdessen eine Funktion, um das ViewModel über die Suchanfrage des Nutzers zu benachrichtigen.

private val queryFlow = MutableStateFlow("")

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

Wenn sich der Wert der Suchanfrage im Datenstream ändert, können Sie Vorgänge ausführen, um den Wert der Suchanfrage 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, bietet aber in allen Fällen ähnliche Funktionen.

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

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

Daten filtern

Ein weiterer häufiger Vorgang ist das Filtern. Sie können Daten anhand von 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 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 an die UI-Ebene übergeben, um sie anzuzeigen.

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

Listentrennzeichen hinzufügen

Die Paging-Bibliothek unterstützt dynamische Listentrennzeichen. Sie können die Lesbarkeit von Listen verbessern, indem Sie Trennzeichen direkt in den Datenstream als zusammensetzbare Elemente in Ihr Layout einfügen. Trennzeichen sind somit voll funktionsfähige zusammensetzbare Elemente, die vollständige Interaktivität, Formatierung und Semantik für die Bedienungshilfen ermöglichen.

Es gibt drei Schritte, um Trennzeichen in Ihre seitenbasierte Liste einzufügen:

  1. Konvertieren Sie das UI-Modell, um die Trennzeichenelemente aufzunehmen. Eine Möglichkeit besteht darin, Ihr Datenelement und das Trennzeichen in eine einzelne versiegelte Klasse einzuschließen. So kann die Benutzeroberfläche mehrere Elementtypen in derselben Liste verarbeiten.
  2. Transformieren Sie den Datenstream, um die Trennzeichen dynamisch hinzuzufügen, zwischen dem Laden und dem Präsentieren der Daten.
  3. Aktualisieren Sie die Benutzeroberfläche, um Trennzeichenelemente zu verarbeiten.

UI-Modell konvertieren

Die Paging-Bibliothek fügt Listentrennzeichen als tatsächliche Listenelemente in die Benutzeroberfläche ein. Die Trennzeichenelemente müssen sich jedoch von den Datenelementen in der Liste unterscheiden, damit beide zusammensetzbaren Typen unterschiedlich gerendert werden. Die Lösung besteht darin, eine versiegelte Kotlin-Klasse mit Unterklassen zu erstellen, um Ihre Daten und Ihre Trennzeichen darzustellen. Alternativ können Sie eine Basisklasse erstellen, die von Ihrer Listenelementklasse und Ihrer Trennzeichenklasse erweitert wird.

Angenommen, Sie möchten einer seitenbasierten Liste von User-Elementen Trennzeichen hinzufügen. Das folgende Snippet zeigt, wie Sie eine Basisklasse erstellen, bei der die Instanzen entweder ein UserModel oder ein SeparatorModel sein können:

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

Datenstream transformieren

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

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

Weitere Informationen zu Transformationen finden Sie unter Grundlegende Transformationen anwenden.

Das folgende Beispiel zeigt Transformationen, mit denen der PagingData<User> Stream in einen PagingData<UiModel> Stream mit Trennzeichen aktualisiert wird:

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

Trennzeichen in der Benutzeroberfläche verarbeiten

Im letzten Schritt müssen Sie Ihre Benutzeroberfläche so ändern, dass sie den Trennzeichen-Artikeltyp aufnehmen kann. In einem Lazy-Layout können Sie mehrere Elementtypen verarbeiten, indem Sie den Typ jedes ausgegebenen UiModel prüfen. Verwenden Sie beim Durchlaufen Ihrer seitenbasierten Daten eine when-Anweisung, um das entsprechende zusammensetzbare Element aufzurufen. So können Sie eine separate Benutzeroberfläche für Datenelemente und Trennzeichen bereitstellen.

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

Doppelte Arbeit vermeiden

Ein wichtiges Problem, das Sie vermeiden sollten, ist, dass die App unnötige Arbeit verrichtet. Das Abrufen von Daten ist ein aufwendiger Vorgang und auch Datentransformationen können viel Zeit in Anspruch nehmen. Sobald die Daten geladen und für die Anzeige in der Benutzeroberfläche vorbereitet sind, sollten sie gespeichert werden, falls eine Konfigurationsänderung erfolgt und die Benutzeroberfläche neu erstellt werden muss.

Der Vorgang cachedIn() speichert die Ergebnisse aller Transformationen, die vor ihm ausgeführt werden, im Cache. In der Regel wenden Sie diesen Operator in Ihrem ViewModel an, bevor Sie den Flow für Ihre zusammensetzbaren Elemente verfügbar machen.

Um den Cache richtig zu verwalten, übergeben Sie ein CoroutineScope an cachedIn(), wie im folgenden Beispiel mit viewModelScope gezeigt.

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

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

Zusätzliche Ressourcen

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

Dokumentation

Inhalte ansehen