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 PagingData objet du flux, placez les transformations dans une map() opération sur le flux :

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

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 :

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.map { user -> UiModel(user) }
  }

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.

private val queryFlow = MutableStateFlow("")

fun onQueryChanged(query: String) {
  queryFlow.value = 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.

val querySearchResults: Flow<User> = queryFlow.flatMapLatest { query ->
  // The database query returns a Flow which is output through
  // querySearchResults
  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.

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
  }

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 que composables dans votre mise en page. Par conséquent, les séparateurs sont des composables dotés de fonctionnalités complètes, permettant une interactivité, un style et une sémantique d'accessibilité complets.

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. Pour ce faire, vous pouvez encapsuler votre élément de données et votre séparateur dans une seule classe scellée. Cela permet à l'interface utilisateur de gérer plusieurs types d'éléments dans la même liste.
  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 l'UI 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 s'assurer que les deux types composables sont rendus de manière 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 :

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

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 PagingData<User> flux vers un PagingData<UiModel> flux avec des séparateurs ajoutés :

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

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. Dans une mise en page différée, vous pouvez gérer plusieurs types d'éléments en vérifiant le type de chaque UiModel émis. Lorsque vous parcourez vos données paginées, utilisez une instruction when pour appeler le composable approprié. Cela vous permet de fournir une interface utilisateur distincte pour les éléments de données et les séparateurs.

@Composable fun UserList(pagingItems: LazyPagingItems) {
  LazyColumn {
    items(
      count = pagingItems.itemCount,
      key = { index ->
        val item = pagingItems.peek(index)
        when (item) {
          is UiModel.UserModel -> item.user.id
          is UiModel.SeparatorModel -> item.description
          else -> index
        }
      }
    ) { index ->
      when (val item = pagingItems[index]) {
        is UiModel.UserModel -> UserItemComposable(item.user)
        is UiModel.SeparatorModel -> SeparatorComposable(item.description)
        null -> PlaceholderComposable()
      }
    }
  }
}

É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. En règle générale, vous appliquez cet opérateur dans votre ViewModel avant d'exposer le Flow à vos composables.

Pour gérer correctement le cache, transmettez un CoroutineScope à cachedIn(), comme illustré dans l'exemple suivant à l'aide de viewModelScope.

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
      .map { user -> UiModel.UserModel(user) }
  }
  .cachedIn(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 :

Documentation

Afficher le contenu