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 :
- Convertir le modèle de l'interface utilisateur en fonction des éléments de séparation.
- 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.
- 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()
etonBindViewHolder()
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
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé
- Charger et afficher des données paginées
- Tester votre implémentation de Paging
- Gérer et présenter les états de chargement