Konzepte und Jetpack Compose-Implementierung
Wenn Sie mit paginierten Daten arbeiten, müssen Sie den Datenstream oft beim Laden transformieren. Möglicherweise 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 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 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 auf die Daten anwenden, die zwischen dem Laden der Daten und der Darstellung der Daten inkrementell erfolgen.
Wenn Sie Transformationen auf jedes PagingData-Objekt im Stream anwenden möchten, platzieren Sie die Transformationen in einem map()-Vorgang für den Stream:
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
Der einfachste Vorgang für einen 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 paginierten Liste im PagingData-Objekt einen map()-Vorgang ausführen.
Ein häufiger Anwendungsfall 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 Kartenoperation anwenden:
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 Suchstring, zu verwenden und in die anzuzeigende Ausgabedaten der Anfrage zu konvertieren. Dazu müssen Sie die Eingabe der Nutzeranfrage erfassen, die Anfrage ausführen und das Ergebnis der Anfrage zurück an die Benutzeroberfläche senden.
Sie können mit einer Stream-API auf die Abfrageeingabe warten. Behalte die Streamreferenz in deinem ViewModel bei. Die UI-Schicht sollte keinen direkten Zugriff darauf haben. Definieren Sie stattdessen eine Funktion, um das ViewModel über die Anfrage des Nutzers zu informieren.
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, bietet aber in allen Fällen eine ähnliche Funktionalität.
Java
Observable<User> querySearchResults = querySubject.switchMap(query -> userDatabase.searchBy(query));
Java
LiveData<User> querySearchResults = Transformations.switchMap( queryLiveData, query -> userDatabase.searchBy(query) );
Durch die Verwendung von Vorgängen wie flatMapLatest oder switchMap wird sichergestellt, dass nur die neuesten Ergebnisse in der Benutzeroberfläche zurückgegeben werden. Wenn der Nutzer seine Anfrageeingabe ändert, bevor der Datenbankvorgang abgeschlossen ist, werden die Ergebnisse der alten Anfrage verworfen und die neue Suche wird 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 anhand anderer Kriterien ausgeblendet werden sollen.
Sie müssen diese Filtervorgänge in den map()-Aufruf einfügen, da sich der Filter auf das PagingData-Objekt bezieht. Sobald die Daten aus dem PagingData herausgefiltert wurden, wird die neue PagingData-Instanz an die UI-Ebene übergeben, um angezeigt zu werden.
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 in den Datenstream als RecyclerView-Listenelemente einfügen. Trennzeichen sind daher vollwertige ViewHolder-Objekte, die Interaktivität, Barrierefreiheit und alle anderen Funktionen eines View ermöglichen.
Das Einfügen von Trennzeichen in eine paginierte Liste umfasst drei Schritte:
- Konvertieren Sie das UI-Modell, um die Trennzeichenelemente zu berücksichtigen.
- Transformieren Sie den Datenstream, um die Trennzeichen dynamisch zwischen dem Laden und der Darstellung der Daten hinzuzufügen.
- Aktualisieren Sie die Benutzeroberfläche, damit Trennzeichenelemente verarbeitet werden.
UI-Modell konvertieren
Die Paging-Bibliothek fügt Listentrennzeichen als tatsächliche Listenelemente in RecyclerView ein. Die Trennzeichenelemente müssen sich jedoch von den Datenelementen in der Liste unterscheiden, damit sie an einen anderen ViewHolder-Typ mit einer separaten 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 von Ihrer Listenelementklasse und Ihrer Trennzeichenklasse erweitert wird.
Angenommen, Sie möchten einer paginierten Liste mit User-Elementen Trennzeichen hinzufügen. Das folgende Snippet zeigt, wie Sie eine Basisklasse erstellen, deren Instanzen entweder ein UserModel oder ein SeparatorModel sein können:
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 haben und bevor Sie ihn präsentieren. Die Transformationen sollten Folgendes tun:
- Konvertieren Sie die geladenen Listenelemente, damit sie dem neuen Basis-Artikeltyp entsprechen.
- Verwenden Sie die Methode
PagingData.insertSeparators(), um die Trennzeichen hinzuzufügen.
Weitere Informationen zu Transformationsvorgängen finden Sie unter Einfache Transformationen anwenden.
Im folgenden Beispiel sehen Sie Transformationsvorgänge, mit denen der PagingData<User>-Stream in einen PagingData<UiModel>-Stream mit hinzugefügten Trennzeichen aktualisiert wird:
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
Im letzten Schritt müssen Sie die Benutzeroberfläche an den Trennzeichen-Elementtyp anpassen.
Erstellen Sie ein Layout und einen View-Holder für Ihre Trennzeichenelemente und ändern Sie den Listenadapter so, dass er RecyclerView.ViewHolder als View-Holder-Typ verwendet, damit er mehr als einen View-Holder-Typ verarbeiten kann. Alternativ können Sie eine gemeinsame Basisklasse definieren, die sowohl von den ViewHolder-Klassen für Elemente als auch für Trennzeichen erweitert wird.
Außerdem müssen Sie die folgenden Änderungen an Ihrem Listenadapter vornehmen:
- Fügen Sie den Methoden
onCreateViewHolder()undonBindViewHolder()Fälle hinzu, um Trennzeichenlistenelemente zu berücksichtigen. - Implementieren Sie einen neuen Comparator.
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 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 auf der Benutzeroberfläche vorbereitet sind, sollten sie gespeichert werden, falls eine Konfigurationsänderung erfolgt und die Benutzeroberfläche neu erstellt werden muss.
Beim Vorgang cachedIn() werden die Ergebnisse aller Transformationen, die vor ihm stattfinden, im Cache gespeichert. Daher sollte cachedIn() der letzte Aufruf in Ihrem ViewModel sein.
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.
Zusätzliche Ressourcen
Weitere Informationen zur Paging-Bibliothek finden Sie in den folgenden zusätzlichen Ressourcen:
Codelabs
Empfehlungen für Sie
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Paginierte Daten laden und anzeigen
- Paging-Implementierung testen
- Ladezustände verwalten und präsentieren