Trasformare gli stream di dati (visualizzazioni)

Concetti e implementazione di Jetpack Compose

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 degli stream di dati è l'aggiunta di separatori di elenchi.

In generale, l'applicazione delle trasformazioni direttamente al flusso di dati consente di mantenere separati i costrutti del repository e 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 dei dati e la loro presentazione.

Per applicare le trasformazioni a ogni oggetto PagingData nello stream, inserisci le trasformazioni all'interno di un'operazione map() sullo 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.
  });

Convertire i dati

L'operazione più semplice su un flusso 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 impaginato all'interno dell'oggetto PagingData.

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

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 nel prendere un input dall'utente, ad esempio una stringa di query, e convertirlo nell'output della richiesta da visualizzare. La configurazione richiede l'ascolto e l'acquisizione dell'input della query dell'utente, l'esecuzione della richiesta e l'invio del risultato della query all'interfaccia utente.

Puoi ascoltare l'input della query utilizzando un'API di flusso. Mantieni il riferimento allo stream nel tuo ViewModel. Il livello UI non deve avere accesso diretto; invece, definisci una funzione per notificare a ViewModel la query dell'utente.

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 dalla lingua e dal framework utilizzati, ma tutte forniscono funzionalità simili.

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 vengano restituiti all'interfaccia utente solo i risultati più recenti. Se l'utente modifica l'input della query prima del completamento dell'operazione sul database, queste operazioni ignorano i risultati della vecchia query e avviano immediatamente la nuova ricerca.

Filtrare i dati

Un'altra operazione comune è il filtraggio. Puoi filtrare i dati in base ai criteri dell'utente oppure puoi rimuovere i dati dalla UI 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 dal PagingData, la nuova istanza PagingData viene passata al livello UI per la visualizzazione.

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 elenchi

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 tutte le funzionalità, che consentono interattività, messa a fuoco dell'accessibilità e tutte le altre funzionalità fornite da un View.

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

  1. Converti il modello UI per adattarlo agli elementi separatori.
  2. Trasforma lo stream di dati per aggiungere dinamicamente i separatori tra il caricamento e la presentazione dei dati.
  3. Aggiorna la UI per gestire gli elementi separatori.

Convertire il modello UI

La libreria Paging inserisce i separatori di elenco in RecyclerView come elementi di elenco effettivi, ma gli elementi separatori devono essere distinguibili dagli elementi di dati nell'elenco per consentire loro di essere associati a un tipo ViewHolder diverso con un'interfaccia utente distinta. La soluzione è 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 di elenco e dalla classe del separatore.

Supponiamo di voler aggiungere separatori a un elenco impaginato di elementi User. Il seguente snippet mostra come creare una classe base in cui le istanze possono essere un UserModel o un SeparatorModel:

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

Trasformare 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 articolo di base.
  • Utilizza il metodo PagingData.insertSeparators() per aggiungere i separatori.

Per scoprire di più sulle operazioni di trasformazione, consulta la sezione Applica trasformazioni di base.

L'esempio seguente mostra le operazioni di trasformazione per aggiornare lo stream PagingData<User> a uno stream PagingData<UiModel> con separatori aggiunti:

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 articolo separatore. Crea un layout e un segnaposto per gli elementi separatori e modifica l'adattatore dell'elenco in modo che utilizzi RecyclerView.ViewHolder come tipo di segnaposto, in modo che possa gestire più di un tipo di segnaposto. In alternativa, puoi definire una classe base comune che estenda sia le classi del titolare della visualizzazione dell'elemento sia quelle del separatore.

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

  • Aggiungi casi ai metodi onCreateViewHolder() e onBindViewHolder() per tenere conto degli elementi dell'elenco dei separatori.
  • Implementa un nuovo comparatore.

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 è che l'app esegua operazioni non necessarie. Il recupero dei dati è un'operazione costosa e anche le trasformazioni dei dati possono richiedere molto tempo. Una volta caricati e preparati per la visualizzazione nella UI, i dati devono essere salvati in caso di modifica della configurazione e di ricreazione della UI.

L'operazione cachedIn() memorizza nella cache i risultati di tutte le trasformazioni che si verificano prima. Pertanto, cachedIn() deve essere l'ultima chiamata nel ViewModel.

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 saperne di più sulla libreria Paging, consulta le seguenti risorse aggiuntive:

Codelab