Transformer des flux de données

Lorsque vous utilisez des données paginées, la transformation du flux de données est souvent nécessaire lors de son chargement. Par exemple, vous devrez peut-être filtrer une liste d'éléments ou convertir des éléments vers un autre type avant de les présenter dans l'interface utilisateur. Un autre cas d'utilisation courant de la transformation de flux de données consiste à ajouter des séparateurs de liste.

Plus généralement, l'application de transformations directement au flux de données vous permet de séparer les constructions de votre dépôt et celles de l'interface utilisateur.

Dans cette page, nous partons du principe que vous maîtrisez l'utilisation de base de la bibliothèque Paging.

Appliquer des transformations de base

Comme les PagingData sont encapsulées dans un flux réactif, vous pouvez appliquer des opérations de transformation sur les données de manière incrémentielle entre leur chargement et leur présentation.

Pour appliquer des transformations à chaque objet PagingData du flux, placez les transformations dans une opération map() sur le flux :

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

Convertir les données

L'opération la plus simple sur un flux de données consiste à le convertir en un autre type. Une fois que vous avez accès à l'objet PagingData, vous pouvez effectuer une opération map() sur chaque élément de la liste paginée dans l'objet PagingData.

Un cas d'utilisation courant consiste à mapper un objet réseau ou de base de données à un objet spécifiquement utilisé dans la couche UI. L'exemple ci-dessous montre comment appliquer ce type d'opération de mappage :

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

Une autre conversion de données courante consiste à récupérer une entrée de l'utilisateur, telle qu'une chaîne de requête, et à la convertir en sortie de requête à afficher. Pour ce faire, vous devez écouter et capturer l'entrée de requête de l'utilisateur, exécuter la requête et transmettre le résultat de la requête à l'interface utilisateur.

Vous pouvez écouter l'entrée de requête à l'aide d'une API de flux. Conservez la référence de flux dans ViewModel. La couche UI ne doit pas y avoir un accès direct. À la place, définissez une fonction pour informer le ViewModel de la requête de l'utilisateur.

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

Lorsque la valeur de la requête change dans le flux de données, vous pouvez effectuer des opérations pour convertir la valeur de la requête en type de données souhaité et renvoyer le résultat à la couche de l'interface utilisateur. La fonction de conversion spécifique dépend du langage et du framework utilisés, mais ils offrent tous des fonctionnalités similaires.

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'utilisation d'opérations telles que flatMapLatest ou switchMap garantit que seuls les derniers résultats sont renvoyés à l'interface utilisateur. Si l'utilisateur modifie ses entrées de requête avant la fin de l'opération de base de données, ces opérations suppriment les résultats de l'ancienne requête et lancent immédiatement la nouvelle recherche.

Filtrer les données

Le filtrage est une autre opération courante. Vous pouvez filtrer les données en fonction des critères de l'utilisateur ou supprimer des données de l'interface utilisateur si elles doivent être masquées sur la base d'autres critères.

Vous devez placer ces opérations de filtrage dans l'appel map(), car le filtre s'applique à l'objet PagingData. Une fois les données filtrées par PagingData, la nouvelle instance PagingData est transmise à la couche de l'interface utilisateur pour être affichée.

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())
)

Ajouter des séparateurs de listes

La bibliothèque Paging accepte les séparateurs de liste dynamiques. Vous pouvez améliorer la lisibilité de la liste en insérant des séparateurs directement dans le flux de données en tant qu'éléments de liste RecyclerView. Par conséquent, les séparateurs sont des objets ViewHolder dotés de fonctionnalités complètes, permettant l'interactivité, la concentration sur l'accessibilité et toutes les autres fonctionnalités fournies par une View.

L'insertion de séparateurs dans une liste paginée comporte trois étapes :

  1. Convertir le modèle de l'interface utilisateur en fonction des éléments de séparation.
  2. Transformer le flux de données pour ajouter de façon dynamique les séparateurs entre le chargement et la présentation des données.
  3. Mettre à jour l'interface utilisateur pour gérer les éléments de séparation.

Convertir le modèle d'UI

La bibliothèque Paging insère les séparateurs de listes dans RecyclerView en tant qu'éléments de liste réels, mais les éléments de séparateur doivent se distinguer des éléments de données de la liste pour leur permettre de s'associer à un autre type ViewHolder avec une UI distincte. La solution consiste à créer une classe scellée Kotlin avec des sous-classes pour représenter vos données et vos séparateurs. Vous pouvez également créer une classe de base étendue par votre classe d'élément de liste et votre classe de séparateur.

Supposons que vous souhaitiez ajouter des séparateurs à une liste paginée d'éléments User. L'extrait de code suivant montre comment créer une classe de base où les instances peuvent être de type UserModel ou 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;
    }
  }
}

Transformer le flux de données

Vous devez appliquer des transformations au flux de données après l'avoir chargé et avant de le présenter. Les transformations doivent :

  • Convertir les éléments de la liste chargée en fonction du nouveau type d'élément de base ;
  • Utiliser la méthode PagingData.insertSeparators() pour ajouter les séparateurs.

Pour en savoir plus sur les opérations de transformation, consultez Appliquer des transformations de base.

L'exemple suivant montre les opérations de transformation permettant de mettre à jour le flux PagingData<User> vers un flux PagingData<UiModel> avec des séparateurs ajoutés :

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

Gérer les séparateurs dans l'UI

La dernière étape consiste à modifier l'interface utilisateur en fonction du type d'élément de séparation. Créez une mise en page et un conteneur de vue pour vos éléments de séparation, puis modifiez l'adaptateur de liste pour qu'il utilise RecyclerView.ViewHolder comme type de conteneur de vue afin qu'il puisse gérer plusieurs types de conteneurs de vue. Vous pouvez également définir une classe de base commune que les classes des conteneurs d'affichage des éléments et des séparateurs étendent.

Vous devez également apporter les modifications suivantes à votre adaptateur de liste :

  • Ajoutez des cas aux méthodes onCreateViewHolder() et onBindViewHolder() pour tenir compte des éléments de la liste de séparateurs.
  • Implémentez un nouveau comparateur.

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

Éviter les tâches dupliquées

Il est essentiel d'éviter que l'application effectue des tâches inutiles. L'extraction des données est une opération coûteuse, et les transformations de données peuvent également prendre du temps. Une fois les données chargées et préparées pour être affichées dans l'interface utilisateur, elles doivent être enregistrées au cas où une modification de configuration se produit et que l'interface doit être recréée.

L'opération cachedIn() met en cache les résultats de toute transformation antérieure. Par conséquent, cachedIn() doit être le dernier appel dans votre 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);

Pour en savoir plus sur l'utilisation de cachedIn() avec un flux de PagingData, consultez Configurer un flux de PagingData.

Ressources supplémentaires

Pour en savoir plus sur la bibliothèque Paging, consultez ces ressources supplémentaires :

Ateliers de programmation

Exemples