Trasformare gli stream di dati

Quando lavori con dati impaginati, 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 dei flussi di dati è l'aggiunta di separatori di elenchi.

Più in generale, l'applicazione delle trasformazioni direttamente allo stream di dati consente di mantenere separati i costrutti del repository e i costrutti dell'interfaccia utente.

Questa pagina presuppone che tu conosca l'utilizzo di base della libreria di paging.

Applicare le trasformazioni di base

Poiché PagingData è incapsulato in un flusso reattivo, puoi applicare operazioni di trasformazione sui dati in modo incrementale tra il caricamento e la presentazione dei dati.

Per applicare le trasformazioni a ogni oggetto PagingData nel flusso, posizionale all'interno di un'operazione map() nel flusso:

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 più di base su un flusso di dati è convertirlo in un tipo diverso. Dopo aver ottenuto l'accesso all'oggetto PagingData, puoi eseguire un'operazione map() su ogni singolo elemento dell'elenco impaginato all'interno dell'oggetto PagingData.

Un caso d'uso comune è la mappatura di un oggetto di livello di rete o database su un oggetto specificatamente utilizzato nel livello UI. L'esempio seguente mostra come applicare questo tipo di operazione sulla mappa:

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 prende un input dall'utente, ad esempio una stringa di query, e lo converte nell'output della richiesta da visualizzare. La configurazione richiede il rilevamento e l'acquisizione dell'input della query dell'utente, l'esecuzione della richiesta e il push del risultato della query all'interfaccia utente.

Puoi ascoltare l'input della query utilizzando un'API Stream. Mantieni il riferimento dello stream in ViewModel. Il livello UI non deve avere accesso diretto; al contrario, definisci 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 nel flusso di dati, puoi eseguire operazioni per convertire il valore della query nel tipo di dati desiderato e restituire il risultato al livello UI. La funzione di conversione specifica dipende dal linguaggio e dal framework utilizzati, ma offre tutte 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 nell'interfaccia utente vengano restituiti solo i risultati più recenti. Se l'utente modifica l'input della query prima del completamento dell'operazione sul database, queste operazioni eliminano i risultati della query precedente e avviano immediatamente la nuova ricerca.

Filtra dati

Un'altra operazione comune è l'applicazione di filtri. Puoi filtrare i dati in base ai criteri dell'utente oppure rimuovere i dati 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 che i dati sono filtrati da PagingData, la nuova istanza PagingData viene passata al livello UI 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())
)

Aggiungi separatori di elenchi

La libreria Paging supporta i separatori di elenchi dinamici. Puoi migliorare la leggibilità degli elenchi 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 l'interattività, l'accessibilità e tutte le altre funzionalità fornite da un elemento View.

L'inserimento di separatori nell'elenco impaginato prevede tre passaggi:

  1. Converti il modello di UI per inserire gli elementi separatori.
  2. Trasforma il flusso di dati per aggiungere in modo dinamico i separatori tra il caricamento dei dati e la loro presentazione.
  3. Aggiorna l'interfaccia utente per gestire gli elementi separatori.

Converti il modello di UI

La libreria di paging inserisce separatori di elenco in RecyclerView come elementi dell'elenco effettivi, ma questi devono essere distinguibili dagli elementi di dati nell'elenco per consentirne l'associazione a un tipo ViewHolder diverso con una UI distinta. La soluzione consiste nel creare una classe sigillata Kotlin con sottoclassi per rappresentare i dati e i separatori. In alternativa, puoi creare una classe base estesa dalla classe dell'elemento dell'elenco e dalla classe del separatore.

Supponi di voler aggiungere separatori a un elenco impaginato di User elementi. Lo snippet seguente 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 dovrebbero:

  • Converti gli elementi dell'elenco caricati in modo che riflettano il nuovo tipo di elemento di base.
  • Utilizza il metodo PagingData.insertSeparators() per aggiungere i separatori.

Per scoprire di più sulle operazioni di trasformazione, consulta Applicare le trasformazioni di base.

L'esempio seguente mostra le operazioni di trasformazione per aggiornare il flusso PagingData<User> a un flusso 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 per adattarla al tipo di elemento separatore. Crea un layout e un contenitore di visualizzazione per gli elementi separatori e modifica l'adattatore elenco in modo che utilizzi RecyclerView.ViewHolder come tipo di contenitore delle visualizzazioni, in modo che possa gestire più di un tipo di titolare. In alternativa, puoi definire una classe di base comune estesa alle classi di titolari degli elementi e delle viste separatori.

Devi inoltre apportare le seguenti modifiche all'adattatore dell'elenco:

  • Aggiungi richieste ai metodi onCreateViewHolder() e onBindViewHolder() per tenere conto delle voci dell'elenco di separatori.
  • Implementa un nuovo strumento di confronto.

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

Evita i lavori duplicati

Un problema fondamentale da evitare è il lavoro non necessario dell'app. Il recupero dei dati è un'operazione costosa e anche le trasformazioni dei dati possono richiedere tempo prezioso. Una volta caricati e preparati per la visualizzazione nella UI, i dati devono essere salvati nel caso in cui si verifichi una modifica alla configurazione e l'UI debba essere ricreata.

L'operazione cachedIn() memorizza nella cache i risultati delle trasformazioni precedenti. Pertanto, cachedIn() deve essere l'ultima chiamata nel tuo 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 un flusso di PagingData, consulta Configurare un flusso di PagingData.

Risorse aggiuntive

Per saperne di più sulla libreria Paging, consulta le seguenti risorse aggiuntive:

Codelab

Samples