Principes de base d'Android Paging

1. Introduction

Points abordés

  • Principaux composants de la bibliothèque Paging
  • Comment ajouter la bibliothèque Paging à votre projet

Objectifs de l'atelier

Dans cet atelier de programmation, vous allez commencer avec une application exemple qui affiche déjà une liste d'articles. Cette liste est statique. Elle contient 500 articles qui sont tous conservés dans la mémoire du téléphone :

7d256d9c74e3b3f5.png

À mesure que vous progressez dans l'atelier de programmation, vous allez découvrir :

  • le concept de pagination ;
  • les principaux composants de la bibliothèque Paging ;
  • comment implémenter la pagination avec la bibliothèque Paging.

Lorsque vous aurez terminé, vous disposerez d'une application sur laquelle :

  • la pagination est correctement implémentée ;
  • la communication avec l'utilisateur est efficace quand il s'agit de récupérer des données supplémentaires.

Voici un aperçu de l'interface utilisateur finale :

6277154193f7580.gif

Prérequis

Souhaitable :

2. Configurer votre environnement

Au cours de cette étape, vous téléchargerez l'intégralité du code de cet atelier de programmation, puis exécuterez un exemple d'application simple.

Pour vous aider à démarrer le plus rapidement possible, nous avons préparé un projet de démarrage.

Si git est installé, vous pouvez simplement exécuter la commande ci-dessous. Pour vérifier si git est installé, saisissez git --version dans le terminal ou la ligne de commande, et vérifiez qu'il fonctionne correctement.

 git clone https://github.com/googlecodelabs/android-paging

Si vous n'avez pas accès à git, cliquez sur le bouton ci-dessous pour télécharger l'ensemble du code de cet atelier de programmation : .

Le code est divisé en deux dossiers : basic et advanced. Pour cet atelier de programmation, seul le dossier basic est concerné.

En outre, le dossier basic contient deux autres dossiers : start et end. Nous allons commencer à travailler sur le code du dossier start. À la fin de cet atelier de programmation, le code du dossier start doit être identique à celui du dossier end.

  1. Ouvrez le projet dans le répertoire basic/start d'Android Studio.
  2. Exécutez la configuration d'exécution app sur un appareil ou un émulateur.

89af884fa2d4e709.png

La liste des articles doit s'afficher. Faites défiler la page jusqu'en bas pour vérifier que la liste est statique. En d'autres termes, lorsque vous arrivez en fin de liste, aucun élément supplémentaire n'est récupéré. Revenez en haut de la page pour vérifier que tous les éléments sont toujours affichés.

3. Présentation de la pagination

Les listes constituent l'une des méthodes les plus courantes pour présenter des informations aux utilisateurs. Cependant, il arrive que ces listes ne proposent qu'une petite partie de tous les contenus disponibles pour les utilisateurs. Lorsque ceux-ci font défiler les informations disponibles, ils s'attendent souvent à ce que davantage de données supplémentaires soient extraites pour compléter les informations déjà consultées. Chaque fois que les données sont extraites, l'expérience utilisateur doit être efficace et fluide afin que les charges incrémentielles ne lui nuisent pas. En outre, les charges incrémentielles offrent un avantage en termes de performances, car l'application n'a pas besoin de stocker de grandes quantités de données en mémoire simultanément.

Dans ce processus d'extraction d'informations incrémentielle, appelé pagination, chaque page correspond à un segment de données à extraire. Pour demander une page, la source de données parcourue nécessite souvent une requête qui définit les informations requises. Le reste de cet atelier de programmation présente la bibliothèque Paging et montre comment celle-ci vous permet d'implémenter rapidement et efficacement la pagination dans votre application.

Principaux composants de la bibliothèque Paging

Les principaux composants de la bibliothèque Paging sont les suivants :

  • PagingSource – une classe de base permettant de charger des segments de données pour une requête de page spécifique. Cette classe fait partie de la couche de données et est généralement exposée à partir d'une classe DataSource, puis par le Repository pour être utilisée dans le ViewModel.
  • PagingConfig – une classe de définition des paramètres qui régissent le comportement de la pagination. Ces paramètres incluent entre autres la taille de la page et l'activation des espaces réservés.
  • Pager – une classe chargée de produire le flux PagingData. Cette opération dépend de la PagingSource, et la classe doit être créée dans ViewModel.
  • PagingData – un conteneur pour les données paginées. Chaque actualisation des données correspond à une émission de PagingData distincte qui repose sur sa propre PagingSource.
  • PagingDataAdapter – une sous-classe RecyclerView.Adapter qui présente les PagingData dans une RecyclerView. Le PagingDataAdapter peut être connecté à un Flow Kotlin, des LiveData, un élément RxJava Flowable, un élément RxJava Observable ou même une liste statique à l'aide de méthodes de fabrique. Le PagingDataAdapter suit les événements de chargement PagingData internes et met à jour l'interface utilisateur de manière efficace pendant le chargement des pages.

566d0f6506f39480.jpeg

Dans les sections suivantes, vous allez implémenter des exemples de chacun des composants décrits ci-dessus.

4. Présentation du projet

Sous sa forme actuelle, l'application affiche une liste statique d'articles. Pour chaque article, un titre, une description et une date de création sont disponibles. Les listes statiques fonctionnent pour un petit nombre d'éléments, mais ne s'adaptent pas bien aux ensembles de données volumineux. Pour résoudre ce problème, nous allons implémenter la pagination à l'aide de la bibliothèque Paging. Commençons par examiner les composants déjà disponibles dans l'application.

L'application suit l'architecture recommandée dans le Guide de l'architecture des applications. Voici le contenu de chaque package :

Couche de données :

  • ArticleRepository : dépôt chargé de fournir la liste des articles et leur conservation en mémoire
  • Article : classe représentant le modèle de données, une représentation des informations extraites de la couche de données

Couche de l'interface utilisateur :

  • Activity, RecyclerView.Adapter et RecyclerView.ViewHolder : classes chargées d'afficher la liste dans l'interface utilisateur
  • ViewModel : conteneur d'état chargé de créer l'état que l'interface utilisateur doit afficher.

Le dépôt expose tous ses articles dans un Flow qui contient le champ articleStream. Ce dépôt est à son tour lu par l'élément ArticleViewModel de la couche de l'interface utilisateur, qui le prépare ensuite à être utilisé par l'UI dans ArticleActivity avec le champ state, soit StateFlow.

L'exposition des articles d'un dépôt en tant que Flow permet de mettre à jour les articles présentés à mesure qu'ils changent. Par exemple, si le titre d'un article a changé, vous pouvez facilement en informer les collecteurs de articleStream. L'utilisation d'un StateFlow pour l'état de l'interface utilisateur dans le ViewModel garantit que même en cas d'arrêt (par exemple, lorsque Activity est recréée pendant une modification de configuration), la collecte de l'état de l'interface puisse reprendre là où nous l'avons interrompue.

Comme indiqué précédemment, l'articleStream actuel du dépôt ne présente que les actualités du jour. Cela peut suffire pour certains utilisateurs, mais d'autres peuvent souhaiter consulter les articles plus anciens après avoir fait défiler tous les articles disponibles pour la journée en cours. Avec une telle attente, l'affichage des articles est un cas d'utilisation idéal de la pagination. Voici d'autres bonnes raisons d'explorer la pagination par le biais des articles :

  • Le ViewModel conserve tous les éléments chargés en mémoire dans le StateFlow de items. Cela représente un problème majeur lorsque l'ensemble de données devient très volumineux, car les performances peuvent être affectées.
  • Plus le volume de la liste des articles augmente, plus le coût de la mise à jour d'un ou plusieurs articles qu'elle contient est élevé.

La bibliothèque Paging permet de résoudre tous ces problèmes tout en fournissant une API cohérente pour récupérer les données de manière incrémentielle (pagination) dans vos applis.

5. Définir la source de données

Lors de l'implémentation de la pagination, vous devez vous assurer que les conditions suivantes sont remplies :

  • Les requêtes sont gérées de manière appropriée pour les données de l'interface utilisateur, afin de garantir que plusieurs requêtes ne sont pas déclenchées en même temps pour la même requête.
  • Une quantité gérable de données récupérées en mémoire est conservée.
  • Les requêtes extraient davantage de données en complément de celles déjà récupérées.

Tout cela est possible grâce à une PagingSource. Une PagingSource définit la source de données en spécifiant comment récupérer des données par segments incrémentiels. L'objet PagingData extrait ensuite les données de la PagingSource en réponse aux indices de chargement générés lorsque l'utilisateur fait défiler une RecyclerView.

Notre PagingSource charge les articles. Dans data/Article.kt, vous trouverez le modèle défini comme suit :

data class Article(
    val id: Int,
    val title: String,
    val description: String,
    val created: LocalDateTime,
)

Pour créer la PagingSource, vous devez définir les éléments suivants :

  • Le type de clé de pagination - la définition du type de requête de page utilisé pour demander davantage de données. Dans le cas présent, nous extrayons les articles en dessous ou au-dessus d'un certain identifiant d'article, car les ID sont forcément triés et incrémentaux.
  • Le type de données chargées - chaque page renvoie une List d'articles, donc le type est Article.
  • L'emplacement de récupération des données : généralement, il s'agit d'une base de données, d'une ressource réseau ou de toute autre source de données paginées. Toutefois, dans cet atelier de programmation, nous utilisons des données générées localement.

Dans le package data, nous allons créer une implémentation PagingSource dans un nouveau fichier nommé ArticlePagingSource.kt :

package com.example.android.codelabs.paging.data

import androidx.paging.PagingSource
import androidx.paging.PagingState

class ArticlePagingSource : PagingSource<Int, Article>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        TODO("Not yet implemented")
    }
   override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
        TODO("Not yet implemented")
    }
}

PagingSource nous oblige à implémenter deux fonctions : load() et getRefreshKey().

La fonction load() sera appelée par la bibliothèque Paging pour extraire de façon asynchrone plus de données à afficher lorsque l'utilisateur fera défiler le contenu. L'objet LoadParams conserve les informations liées à l'opération de chargement, y compris les éléments suivants :

  • Clé de la page à charger - si c'est la première fois que load() est appelé, la valeur de LoadParams.key sera null. Dans ce cas, vous devez définir la clé de page initiale. Dans notre projet, nous utilisons l'ID d'article comme clé. Ajoutons également une constante STARTING_KEY de 0 en haut du fichier ArticlePagingSource pour la clé de page initiale.
  • Taille de chargement – nombre d'éléments à charger.

La fonction load() renvoie un LoadResult. Le LoadResult peut être l'un des types suivants :

  • LoadResult.Page, si le résultat a abouti.
  • LoadResult.Error, en cas d'erreur.
  • LoadResult.Invalid, si la valeur de PagingSource doit être invalidée, car elle ne peut plus garantir l'intégrité de ses résultats.

Le champ LoadResult.Page nécessite trois arguments :

  • data : liste (List) des éléments récupérés.
  • prevKey : clé utilisée par la méthode load() pour extraire des éléments en arrière-plan de la page active.
  • nextKey : clé utilisée par la méthode load() pour extraire des éléments après la page active.

En outre, deux options facultatives peuvent être définies :

  • itemsBefore : nombre d'espaces réservés à afficher avant les données chargées.
  • itemsAfter : nombre d'espaces réservés à afficher après les données chargées.

Notre clé de chargement est le champ Article.id. Nous pouvons l'utiliser comme clé, car l'ID d'Article s'incrémente de 1 pour chaque article. En d'autres termes, les ID des articles sont des nombres entiers linéaires consécutifs.

La valeur du champ nextKey ou prevKey est null si aucune autre donnée n'est chargée dans la direction correspondante. Dans notre cas, pour prevKey :

  • Si la valeur de startKey est identique à STARTING_KEY, la valeur est nulle, car nous ne pouvons pas charger plus d'éléments derrière cette clé.
  • Sinon, nous récupérons le premier élément de la liste et chargeons LoadParams.loadSize en arrière-plan, en veillant à ne jamais renvoyer de clé inférieure à STARTING_KEY. Pour ce faire, nous définissons la méthode ensureValidKey().

Ajoutez la fonction suivante qui vérifie si la clé de pagination est valide :

class ArticlePagingSource : PagingSource<Int, Article>() {
   ...
   /**
     * Makes sure the paging key is never less than [STARTING_KEY]
     */
    private fun ensureValidKey(key: Int) = max(STARTING_KEY, key)
}

Pour nextKey :

  • Étant donné que nous proposons le chargement d'éléments illimités, nous transmettons range.last + 1.

Par ailleurs, comme un champ created est disponible pour chaque article, nous devons également générer une valeur pour celui-ci. Ajoutez les lignes suivantes au début du fichier :

private val firstArticleCreatedTime = LocalDateTime.now()

class ArticlePagingSource : PagingSource<Int, Article>() {
   ...
}

Avec tout ce code en place, nous pouvons désormais implémenter la fonction load() :

import kotlin.math.max
...

private val firstArticleCreatedTime = LocalDateTime.now()

class ArticlePagingSource : PagingSource<Int, Article>() {
   override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        // Start paging with the STARTING_KEY if this is the first load
        val start = params.key ?: STARTING_KEY
        // Load as many items as hinted by params.loadSize
        val range = start.until(start + params.loadSize)

        return LoadResult.Page(
            data = range.map { number ->
                Article(
                    // Generate consecutive increasing numbers as the article id
                    id = number,
                    title = "Article $number",
                    description = "This describes article $number",
                    created = firstArticleCreatedTime.minusDays(number.toLong())
                )
            },

            // Make sure we don't try to load items behind the STARTING_KEY
            prevKey = when (start) {
                STARTING_KEY -> null
                else -> ensureValidKey(key = range.first - params.loadSize)
            },
            nextKey = range.last + 1
        )
    }

    ...
}

Nous devons ensuite implémenter getRefreshKey(). Cette méthode est appelée lorsque la bibliothèque Paging doit actualiser les éléments pour l'interface utilisateur, car les données de la PagingSource de sauvegarde ont changé. Dans ce cas, nommé invalidation, les données sous-jacentes d'une PagingSource ont changé et doivent être mises à jour dans l'UI. Une fois les données invalidées, la bibliothèque Paging crée une PagingSource pour les actualiser, puis informe l'UI en émettant de nouvelles PagingData. Vous en apprendrez plus sur l'invalidation dans une prochaine section.

Lors du chargement à partir d'une nouvelle PagingSource, getRefreshKey() est appelé pour fournir la clé avec laquelle la nouvelle PagingSource doit démarrer le chargement, afin que l'utilisateur ne perde pas sa place actuelle dans la liste après l'actualisation.

L'invalidation de la bibliothèque de pagination peut se produire pour l'une des deux raisons suivantes :

  • Vous avez appelé refresh() au niveau de PagingAdapter.
  • Vous avez appelé invalidate() au niveau de PagingSource.

La clé renvoyée (Int, dans notre cas) est transmise à l'appel suivant de la méthode load() dans la nouvelle PagingSource via l'argument LoadParams. Pour éviter que des éléments ne sautent après l'invalidation, nous devons nous assurer que la clé renvoyée charge suffisamment d'éléments pour remplir l'écran. Ainsi, le nouvel ensemble d'éléments est plus susceptible de contenir des éléments présents dans les données invalidées, ce qui permet de conserver la position de défilement actuelle. Examinons l'implémentation dans notre appli :

   // The refresh key is used for the initial load of the next PagingSource, after invalidation
   override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
        // In our case we grab the item closest to the anchor position
        // then return its id - (state.config.pageSize / 2) as a buffer
        val anchorPosition = state.anchorPosition ?: return null
        val article = state.closestItemToPosition(anchorPosition) ?: return null
        return ensureValidKey(key = article.id - (state.config.pageSize / 2))
    }

Dans l'extrait ci-dessus, nous utilisons PagingState.anchorPosition. Il s'agit d'un bon indice si vous vous demandez comment la bibliothèque Paging peut extraire davantage d'éléments. Lorsque l'interface utilisateur tente de lire les éléments de PagingData, il le fait à un certain index. Si les données ont été lues, elles sont affichées dans l'interface utilisateur. Toutefois, s'il n'y a pas de données, la bibliothèque Paging sait qu'elle doit extraire les données pour répondre à la requête de lecture ayant échoué. Le dernier index qui a réussi à récupérer les données en lecture est anchorPosition.

Lors de l'actualisation, nous récupérons la clé de l'Article le plus proche de anchorPosition afin de l'utiliser comme clé de chargement. Ainsi, lorsque nous relancez le chargement à partir d'une nouvelle PagingSource, l'ensemble d'éléments récupérés inclut les éléments déjà chargés, ce qui garantit une expérience utilisateur fluide et cohérente.

Ensuite, vous avez entièrement défini une PagingSource. L'étape suivante consiste à le connecter à l'interface utilisateur.

6. Produire des PagingData pour l'UI

Dans l'implémentation actuelle, nous utilisons un Flow<List<Article>> dans le ArticleRepository pour exposer les données chargées dans le ViewModel. Le ViewModel conserve à son tour un état des données toujours disponible avec l'opérateur stateIn pour l'exposition à l'interface utilisateur.

Avec la bibliothèque Paging, nous affichons à la place un Flow<PagingData<Article>> à partir du ViewModel. PagingData est un type qui encapsule les données que nous avons chargées, permet à la bibliothèque Paging de décider quand récupérer davantage de données, et veille à ce que la même page ne soit pas demandée deux fois.

Pour construire les PagingData, nous allons utiliser l'une des différentes méthodes de création possibles avec la classe Pager, en fonction de l'API à utiliser pour transmettre les PagingData à d'autres couches de notre appli :

  • Utilisez Pager.flow avec un Flow Kotlin.
  • Utilisez Pager.liveData avec LiveData.
  • Utilisez Pager.flowable avec un élément RxJava Flowable.
  • Utilisez Pager.observable avec un élément RxJava Observable.

Comme nous utilisons déjà Flow dans notre application, nous continuerons à suivre cette approche, mais en utilisant Flow<List<Article>> au lieu de Flow<PagingData<Article>>.

Quel que soit le compilateur PagingData utilisé, vous devrez transmettre les paramètres suivants :

  • PagingConfig, la classe qui définit les options relatives au chargement de contenu à partir d'une PagingSource (par exemple, la limite de chargement anticipé, la requête de taille du chargement initial, etc.). La seule valeur que vous devez impérativement définir est la taille de page, qui définit le nombre d'éléments devant être chargés sur chaque page. Par défaut, Paging conserve en mémoire toutes les pages que vous chargez. Pour éviter de gaspiller la mémoire lorsque l'utilisateur fait défiler sa liste, définissez le paramètre maxSize dans PagingConfig. Par défaut, Paging renvoie des éléments nuls pour réserver l'espace du contenu qui reste à charger si ces éléments non chargés peuvent être comptés et si l'indicateur de configuration enablePlaceholders est défini sur true. Vous pourrez ainsi afficher un espace réservé dans l'adaptateur. Pour simplifier le travail dans cet atelier de programmation, désactivez les espaces réservés en transmettant enablePlaceholders = false.
  • Une fonction qui définit la façon de créer la PagingSource. Dans le cas présent, nous allons créer une ArticlePagingSource. Il nous faut donc une fonction qui indique à la bibliothèque Paging comment procéder.

Modifions à présent ArticleRepository.

Mettre à jour ArticleRepository

  • Supprimez le champ articlesStream.
  • Ajoutez une méthode appelée articlePagingSource() qui renvoie le ArticlePagingSource que nous venons de créer.
class ArticleRepository) {

    fun articlePagingSource() = ArticlePagingSource()
}

Nettoyer ArticleRepository

La bibliothèque Paging remplit de nombreuses fonctions :

  • Gestion du cache en mémoire
  • Demande de données lorsque l'utilisateur approche de la fin de la liste

En conséquence, tous les autres éléments de notre ArticleRepository peuvent être supprimés, à l'exception du articlePagingSource(). Votre fichier ArticleRepository devrait à présent se présenter comme suit :

package com.example.android.codelabs.paging.data

import androidx.paging.PagingSource

class ArticleRepository {
    fun articlePagingSource() = ArticlePagingSource()
}

Des erreurs de compilation devraient à présent apparaître dans ArticleViewModel. Voyons quelles modifications y apporter.

7. Requérir et mettre en cache PagingData dans le ViewModel

Avant de corriger les erreurs de compilation, examinons le ViewModel.

class ArticleViewModel(...) : ViewModel() {

    val items: StateFlow<List<Article>> = ...
}

Pour intégrer la bibliothèque Paging dans le ViewModel, nous allons remplacer le type de items renvoyé de StateFlow<List<Article>> à Flow<PagingData<Article>>. Pour ce faire, commencez par ajouter une constante privée appelée ITEMS_PER_PAGE en tête du fichier :

private const val ITEMS_PER_PAGE = 50

class ArticleViewModel {
    ...
}

Nous mettons ensuite à jour le items pour obtenir le résultat d'une instance Pager. Pour ce faire, nous transmettons les deux paramètres Pager :

  • Une PagingConfig avec un pageSize de ITEMS_PER_PAGE et des espaces réservés désactivés
  • Une PagingSourceFactory qui fournit une instance de la ArticlePagingSource que nous venons de créer.
class ArticleViewModel(...) : ViewModel() {

   val items: Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
        pagingSourceFactory = { repository.articlePagingSource() }
    )
        .flow
        ...
}

Pour conserver l'état Paging en modifiant la configuration ou la navigation, nous utilisons la méthode cachedIn() en lui transmettant le androidx.lifecycle.viewModelScope.

Une fois les modifications ci-dessus effectuées, le fichier ViewModel devrait se présenter comme suit :

package com.example.android.codelabs.paging.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.example.android.codelabs.paging.data.Article
import com.example.android.codelabs.paging.data.ArticleRepository
import com.example.android.codelabs.paging.data.ITEMS_PER_PAGE
import kotlinx.coroutines.flow.Flow

private const val ITEMS_PER_PAGE = 50

class ArticleViewModel(
    private val repository: ArticleRepository,
) : ViewModel() {

    val items: Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
        pagingSourceFactory = { repository.articlePagingSource() }
    )
        .flow
        .cachedIn(viewModelScope)
}

Autre point à noter concernant PagingData : il s'agit d'un type autonome qui contient un flux modifiable de mises à jour des données à afficher dans RecyclerView. Chaque émission de PagingData est entièrement indépendante, et plusieurs PagingData peuvent être émises pour une même requête si la sauvegarde de PagingSource est invalidée par les modifications apportées à l'ensemble de données sous-jacent. De ce fait, les Flows de PagingData doivent être exposés indépendamment des autres Flows.

Et voilà ! Une fonctionnalité de pagination est désormais disponible dans ViewModel.

8. Faire fonctionner l'adaptateur avec PagingData

Pour rattacher les PagingData à une RecyclerView, utilisez un PagingDataAdapter. Le PagingDataAdapter est alerté à chaque chargement du contenu PagingData, puis indique à la RecyclerView de se mettre à jour.

Mettre à jour ArticleAdapter pour qu'il fonctionne avec un flux PagingData

  • Pour le moment, ArticleAdapter implémente ListAdapter. Remplacez cela par l'implémentation de PagingDataAdapter. Le reste du corps de classe reste inchangé :
import androidx.paging.PagingDataAdapter
...

class ArticleAdapter : PagingDataAdapter<Article, RepoViewHolder>(ARTICLE_DIFF_CALLBACK) {
// body is unchanged
}

Nous avons apporté de nombreuses modifications. Il ne reste plus qu'une étape avant de pouvoir exécuter l'application : connecter l'interface utilisateur !

9. Intégrer PagingData dans l'interface utilisateur

L'implémentation actuelle présente une méthode nommée binding.setupScrollListener() qui appelle le ViewModel pour charger plus de données si certaines conditions sont remplies. La bibliothèque Paging effectue toutes ces opérations automatiquement. Nous pouvons donc supprimer cette méthode et ses utilisations.

Ensuite, comme ArticleAdapter n'est plus un ListAdapter mais un PagingDataAdapter, nous apportons deux modifications mineures :

  • Dans le Flow, l'opérateur terminal ViewModel est remplacé par collectLatest au lieu de collect.
  • Les ArticleAdapter sont informés des modifications avec submitData() au lieu de submitList().

Nous utilisons collectLatest sur le Flow de pagingData. Ainsi, cette collection des émissions précédentes des pagingData est annulée lorsqu'une nouvelle instance pagingData est émise.

Une fois les modifications ci-dessus apportées, Activity doit se présenter comme suit :

import kotlinx.coroutines.flow.collectLatest

class ArticleActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityArticlesBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        val viewModel by viewModels<ArticleViewModel>(
            factoryProducer = { Injection.provideViewModelFactory(owner = this) }
        )

        val items = viewModel.items
        val articleAdapter = ArticleAdapter()

        binding.bindAdapter(articleAdapter = articleAdapter)

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                items.collectLatest {
                    articleAdapter.submitData(it)
                }
            }
        }
    }
}

private fun ActivityArticlesBinding.bindAdapter(
    articleAdapter: ArticleAdapter
) {
    list.adapter = articleAdapter
    list.layoutManager = LinearLayoutManager(list.context)
    val decoration = DividerItemDecoration(list.context, DividerItemDecoration.VERTICAL)
    list.addItemDecoration(decoration)
}

L'application doit désormais se compiler et s'exécuter. Bravo ! Vous avez migré l'application vers la bibliothèque Paging.

f97136863cfa19a0.gif

10. Afficher les états de chargement dans l'interface utilisateur

Lorsque la bibliothèque Paging récupère d'autres éléments à afficher dans l'interface utilisateur, il est recommandé d'indiquer à l'utilisateur que davantage de données sont en cours d'acheminement. Heureusement, la bibliothèque Paging permet d'accéder facilement à l'état de chargement avec le type CombinedLoadStates.

Les instances CombinedLoadStates décrivent l'état de chargement de tous les composants de la bibliothèque Paging qui chargent des données. Dans le cas présent, nous nous intéressons au LoadState de ArticlePagingSource uniquement. Nous allons donc travailler principalement avec le type LoadStates dans le champ CombinedLoadStates.source. Vous pouvez accéder à CombinedLoadStates par le biais de PagingDataAdapter via PagingDataAdapter.loadStateFlow.

CombinedLoadStates.source est un type LoadStates avec des champs pour trois types différents de LoadState :

  • LoadStates.append : pour les éléments LoadState extraits après la position actuelle de l'utilisateur
  • LoadStates.prepend : pour les éléments LoadState extraits avant la position actuelle de l'utilisateur
  • LoadStates.refresh : pour la valeur LoadState du chargement initial

Chaque service LoadState peut être à l'un des états suivants :

  • LoadState.Loading : les éléments sont en cours de chargement
  • LoadState.NotLoading : les éléments ne se chargent pas.
  • LoadState.Error : une erreur de chargement s'est produite.

Dans notre cas, seule la valeur LoadState de LoadState.Loading nous intéresse, car notre ArticlePagingSource ne comprend pas de cas d'erreur.

Nous commençons par ajouter des barres de progression en haut et en bas de l'interface utilisateur pour indiquer l'état de chargement des extractions dans les deux sens.

Dans activity_articles.xml, ajoutez deux barres LinearProgressIndicator comme suit :

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.ArticleActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scrollbars="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.progressindicator.LinearProgressIndicator
        android:id="@+id/prepend_progress"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.progressindicator.LinearProgressIndicator
        android:id="@+id/append_progress"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Ensuite, nous répondons à CombinedLoadState en collectant le LoadStatesFlow du PagingDataAdapter. Collectez l'état dans ArticleActivity.kt :

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                articleAdapter.loadStateFlow.collect {
                    binding.prependProgress.isVisible = it.source.prepend is Loading
                    binding.appendProgress.isVisible = it.source.append is Loading
                }
            }
        }
        lifecycleScope.launch {
        ...
    }

Enfin, nous ajoutons un léger délai dans ArticlePagingSource pour simuler la charge :

private const val LOAD_DELAY_MILLIS = 3_000L

class ArticlePagingSource : PagingSource<Int, Article>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        val start = params.key ?: STARTING_KEY
        val range = startKey.until(startKey + params.loadSize)

        if (start != STARTING_KEY) delay(LOAD_DELAY_MILLIS)
        return ...

}

Exécutez de nouveau l'application et faites défiler la liste jusqu'en bas. La barre de progression inférieure doit s'afficher pendant que la bibliothèque Paging récupère d'autres éléments, et lorsqu'elle est terminée.

6277154193f7580.gif

11. Conclusion

Passons rapidement en revue les points abordés. Nous avons :

  • étudié un aperçu de la pagination et vu pourquoi elle est nécessaire ;
  • ajouté la pagination à notre application en créant un Pager, en définissant une PagingSource et en émettant des PagingData ;
  • mis en cache les PagingData dans le ViewModel à l'aide de l'opérateur cachedIn ;
  • utilisé les PagingData dans l'UI à l'aide d'un PagingDataAdapter ;
  • réagi aux CombinedLoadStates à l'aide de PagingDataAdapter.loadStateFlow.

Et voilà ! Pour découvrir des concepts de pagination plus avancés, consultez l'atelier de programmation Paging avancé.