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:
- Konvertieren Sie das UI-Modell, um die Trennelemente aufzunehmen.
- Transformieren Sie den Datenstream, um die Trennzeichen dynamisch zwischen dem Laden und dem Darstellen der Daten hinzuzufügen.
- 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()
undonBindViewHolder()
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
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Seitenbasierte Daten laden und anzeigen
- Implementierung der Seitennavigation testen
- Ladestatus verwalten und anzeigen