Quando lavorI con dati paginati, spesso devi trasformare lo stream di dati durante il caricamento. Ad esempio, potresti dover filtrare un elenco di elementi o convertirli in un tipo diverso prima di presentarli nell'interfaccia utente. Un altro caso d'uso comune per la trasformazione dello stream di dati è l'aggiunta di separatori di elenco.
Più in generale, l'applicazione delle trasformazioni direttamente allo stream di dati ti consente di mantenere separati i costrutti del repository e quelli dell'interfaccia utente.
Questa pagina presuppone che tu abbia familiarità con l'utilizzo di base della libreria Paging.
Applicare le trasformazioni di base
Poiché PagingData
è incapsulato in uno stream reattivo, puoi applicare operazioni di trasformazione ai dati in modo incrementale tra il caricamento e la presentazione.
Per applicare le trasformazioni a ogni oggetto PagingData
nello stream, collocale all'interno di un'operazione map()
sullo stream:
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. });
Converti i dati
L'operazione di base su uno stream di dati è la conversione in un tipo diverso. Una volta ottenuto l'accesso all'oggetto PagingData
, puoi eseguire un'operazione map()
su ogni singolo elemento dell'elenco paginato all'interno dell'oggetto PagingData
.
Un caso d'uso comune è mappare un oggetto del livello di rete o del database su un oggetto utilizzato specificamente nel livello dell'interfaccia utente. L'esempio seguente mostra come applicare questo tipo di operazione di mappatura:
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) )
Un'altra conversione di dati comune consiste nell'eseguire l'input dell'utente, ad esempio una stringa di query, e nell'eseguirne la conversione in output della richiesta da visualizzare. La configurazione richiede di ascoltare e acquisire l'input della query dell'utente, eseguire la richiesta e inviare nuovamente il risultato della query all'interfaccia utente.
Puoi ascoltare l'input della query utilizzando un'API stream. Mantieni il riferimento allo stream
nel tuo ViewModel
. Il livello UI non deve avere accesso diretto, ma deve definire una funzione per notificare al ViewModel la query dell'utente.
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) }
Quando il valore della query cambia nello stream di dati, puoi eseguire operazioni per convertire il valore della query nel tipo di dati desiderato e restituire il risultato al livello dell'interfaccia utente. La funzione di conversione specifica dipende dal linguaggio e dal framework utilizzati, ma tutte forniscono funzionalità simili.
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) );
L'utilizzo di operazioni come flatMapLatest
o switchMap
garantisce che all'interfaccia utente vengano restituiti solo i risultati più recenti. Se l'utente modifica l'input della query
prima del completamento dell'operazione del database, queste operazioni ignorano i risultati
della vecchia query e avviano immediatamente la nuova ricerca.
Filtrare i dati
Un'altra operazione comune è il filtro. Puoi filtrare i dati in base ai criteri dell'utente oppure rimuoverli dall'interfaccia utente se devono essere nascosti in base ad altri criteri.
Devi inserire queste operazioni di filtro all'interno della chiamata map()
perché il
filtro si applica all'oggetto PagingData
. Una volta filtrati i dati da PagingData
, la nuova istanza PagingData
viene passata al livello dell'interfaccia utente per la visualizzazione.
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()) )
Aggiungere separatori di elenco
La libreria Paging supporta i separatori di elenchi dinamici. Puoi migliorare la leggibilità dell'elenco inserendo i separatori direttamente nello stream di dati come elementi dell'elenco RecyclerView
. Di conseguenza, i separatori sono oggetti ViewHolder
con funzionalità complete, che consentono interattività, attenzione all'accessibilità e tutte le altre funzionalità fornite da un View
.
Per inserire i separatori nell'elenco paginato sono necessari tre passaggi:
- Converti il modello dell'interfaccia utente in modo da adattarlo agli elementi separatore.
- Trasforma lo stream di dati per aggiungere dinamicamente i separatori tra il caricamento e la presentazione dei dati.
- Aggiorna l'interfaccia utente per gestire gli elementi separatore.
Converti il modello dell'interfaccia utente
La libreria Paging inserisce separatori di elenco in RecyclerView
come elementi di elenco effettivi, ma gli elementi separatore devono essere distinguibili dagli elementi di dati nell'elenco per poter essere associati a un tipo di ViewHolder
diverso con un'interfaccia utente distinta. La soluzione è creare una classe sealed di Kotlin con sottoclassi per rappresentare i dati e i separatori. In alternativa, puoi creare una classe di base estesa dalla classe dell'elemento dell'elenco e dalla classe del separatore.
Supponiamo che tu voglia aggiungere separatori a un elenco paginato di elementi User
. Il
seguente snippet mostra come creare una classe di base in cui le istanze possono essere
UserModel
o SeparatorModel
:
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; } } }
Trasforma lo stream di dati
Devi applicare le trasformazioni allo stream di dati dopo averlo caricato e prima di presentarlo. Le trasformazioni devono:
- Converti gli elementi dell'elenco caricati in modo che riflettano il nuovo tipo di elemento base.
- Utilizza il metodo
PagingData.insertSeparators()
per aggiungere i separatori.
Per scoprire di più sulle operazioni di trasformazione, consulta la sezione Applicare trasformazioni di base.
L'esempio seguente mostra le operazioni di trasformazione per aggiornare lo stream PagingData<User>
in uno stream PagingData<UiModel>
con separatori aggiunti:
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; } }); });
Gestire i separatori nell'interfaccia utente
Il passaggio finale consiste nel modificare l'interfaccia utente in modo da adattarla al tipo di elemento separatore.
Crea un layout e un visualizzatore per gli elementi separatore e modifica l'adattatore dell'elenco in modo che utilizzi RecyclerView.ViewHolder
come tipo di visualizzatore in modo da poter gestire più di un tipo di visualizzatore. In alternativa, puoi definire una classe di base comune estesa sia dalle classi di visualizzatore di elementi sia da quelle di visualizzatore di separatori.
Devi anche apportare le seguenti modifiche all'adattatore dell'elenco:
- Aggiungi casi ai metodi
onCreateViewHolder()
eonBindViewHolder()
per tenere conto degli elementi dell'elenco dei separatori. - Implementa un nuovo comparatore.
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); } }
Evitare il lavoro duplicato
Un problema fondamentale da evitare è far eseguire all'app operazioni non necessarie. Il recupero dei dati è un'operazione dispendiosa e anche le trasformazioni dei dati possono richiedere tempo prezioso. Una volta caricati e preparati per la visualizzazione nell'interfaccia utente, i dati devono essere salvati nel caso in cui si verifichi una modifica alla configurazione e l'interfaccia utente debba essere ricreata.
L'operazione cachedIn()
memorizza nella cache i risultati di eventuali trasformazioni che si verificano prima. Pertanto, cachedIn()
deve essere l'ultima chiamata nel ViewModel.
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);
Per ulteriori informazioni sull'utilizzo di cachedIn()
con uno stream di PagingData
, consulta
Configurare uno stream di
PagingData.
Risorse aggiuntive
Per scoprire di più sulla libreria Paging, consulta le seguenti risorse aggiuntive:
Codelab
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Caricare e visualizzare i dati paginati
- Testare l'implementazione della paginazione
- Gestire e presentare gli stati di caricamento