Charger et afficher des données paginées

La bibliothèque Paging permet de charger et d'afficher des données paginées à partir d'un ensemble de données plus volumineux. Ce guide explique comment utiliser la bibliothèque Paging pour configurer un flux de données paginées à partir d'une source de données réseau et l'afficher dans une liste différée.

Définir une source de données

La première étape consiste à définir une implémentation PagingSource afin d'identifier la source de données. La classe d'API PagingSource inclut la méthode load, que vous devez ignorer pour indiquer comment extraire les données paginées de la source de données correspondante.

Utilisez directement la classe PagingSource pour utiliser les coroutines Kotlin pour le chargement asynchrone.

Sélectionner des types de clés et de valeurs

PagingSource<Key, Value> comporte deux paramètres de type : Key et Value. La clé définit l'identifiant utilisé pour charger les données, tandis que la valeur correspond au type des données. Par exemple, si vous chargez des pages d'objets User à partir du réseau en transmettant des numéros de page Int à Retrofit, sélectionnez Int comme type Key et User comme type Value.

Définir la PagingSource

L'exemple suivant implémente une PagingSource qui charge les pages d'éléments par numéro de page. Le type Key est Int et le type Value est User.

class ExamplePagingSource(
    val backend: ExampleBackendService,
    val query: String
) : PagingSource<Int, User>() {
  override suspend fun load(
    params: LoadParams<Int>
  ): LoadResult<Int, User> {

    init {
        // the data source is expected to be immutable
        // invalidate PagingSource if data source
        // has updated
        backEnd.addDatabaseOnChangedListener {
            invalidate()
        }
    }

    try {
      // Start refresh at page 1 if undefined.
      val nextPageNumber = params.key ?: 1
      val response = backend.searchUsers(query, nextPageNumber)
      return LoadResult.Page(
        data = response.users,
        prevKey = null, // Only paging forward.
        nextKey = nextPageNumber + 1
      )
    } catch (e: Exception) {
      // Handle errors in this block and return LoadResult.Error for
      // expected errors (such as a network failure).
    }
  }

  override fun getRefreshKey(state: PagingState<Int, User>): Int? {
    // Try to find the page key of the closest page to anchorPosition from
    // either the prevKey or the nextKey; you need to handle nullability
    // here.
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey are null -> anchorPage is the
    //    initial page, so return null.
    return state.anchorPosition?.let { anchorPosition ->
      val anchorPage = state.closestPageToPosition(anchorPosition)
      anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
    }
  }
}

Une implémentation PagingSource type transmet les paramètres fournis dans son constructeur à la méthode load afin de charger les données adéquates pour une requête. Dans l'exemple ci-dessus, ces paramètres sont les suivants :

  • backend : instance du service de backend qui fournit les données.
  • query : requête de recherche à envoyer au service indiqué par backend.

L'objet LoadParams contient des informations sur le chargement à effectuer. Celles-ci incluent la clé et le nombre d'éléments à charger.

L'objet LoadResult contient le résultat du chargement. LoadResult est une classe scellée qui prend l'une des trois formes suivantes, selon que l'appel load a réussi :

  • Si le chargement aboutit, renvoyez un objet LoadResult.Page.
  • Si le chargement échoue, renvoyez un objet LoadResult.Error.
  • Si le PagingSource n'est plus valide et doit être remplacé par une nouvelle instance (par exemple, en raison d'une modification des données sous-jacentes), renvoyez un objet LoadResult.Invalid.

La figure suivante montre comment la fonction load de cet exemple reçoit la clé pour chaque chargement et fournit la clé pour le chargement suivant.

À chaque fois que load est appelé, ExamplePagingSource utilise la clé actuelle et renvoie la clé suivante à charger.
Figure 1. Schéma montrant comment load utilise et met à jour la clé.

L'implémentation de PagingSource doit également implémenter une méthode getRefreshKey qui accepte un objet PagingState comme paramètre. Elle renvoie la clé à transmettre à la méthode load lorsque les données sont actualisées ou invalidées après le chargement initial. La bibliothèque Paging appelle automatiquement cette méthode lors des actualisations ultérieures des données.

Gérer les erreurs

Les requêtes de chargement de données peuvent échouer pour plusieurs raisons, en particulier en cas de chargement sur un réseau. Signalez les erreurs rencontrées lors du chargement en renvoyant un objet LoadResult.Error à partir de la méthode load.

Par exemple, vous pouvez détecter et signaler les erreurs de chargement dans la ExamplePagingSource de l'exemple précédent en ajoutant le code suivant à la méthode load :

catch (e: IOException) {
  // IOException for network failures.
  return LoadResult.Error(e)
} catch (e: HttpException) {
  // HttpException for any non-2xx HTTP status codes.
  return LoadResult.Error(e)
}

Pour en savoir plus sur la gestion des erreurs Retrofit, consultez les exemples fournis dans la documentation de référence de l'API PagingSource.

PagingSource collecte et transmet les objets LoadResult.Error à l'interface utilisateur pour que vous puissiez les exploiter. Pour savoir comment exposer l'état de chargement dans l'UI, consultez Gérer et présenter les états de chargement.

Configurer un flux de PagingData

Vous avez ensuite besoin d'un flux de données paginées généré par l'implémentation de PagingSource. Configurez le flux de données dans votre ViewModel. La classe Pager fournit des méthodes qui exposent un flux réactif d'objets PagingData à partir d'une PagingSource. La bibliothèque Paging expose le flux de données sous la forme d'un Flow.

Lorsque vous créez une instance Pager pour configurer votre flux réactif, indiquez-lui un objet de configuration PagingConfig et une fonction qui indique au Pager comment obtenir une instance de votre implémentation de PagingSource, comme indiqué dans l'exemple suivant.

class UserViewModel(
    private val backend: ExampleBackendService,
    private val query: String
) : ViewModel() {

    val userPagingFlow: Flow<PagingData<User>> = Pager(
        // Configure how data is loaded by passing additional properties to
        // PagingConfig, such as pageSize and enabling or disabling placeholders.
        config = PagingConfig(
            pageSize = 20,
            enablePlaceholders = true
        ),
        pagingSourceFactory = {
            ExamplePagingSource(backend, query)
        }
    )
    .flow
    .cachedIn(viewModelScope)
}

L'opérateur cachedIn rend le flux de données partageable et met en cache les données chargées avec l'élément CoroutineScope fourni. Sans cachedIn, il est impossible de se souvenir de PagingData. Cet exemple utilise le viewModelScope fourni par l'artefact lifecycle-viewmodel-ktx de cycle de vie.

L'objet Pager appelle la méthode load à partir de l'objet PagingSource en lui fournissant l'objet LoadParams et en recevant l'objet LoadResult en retour.

Collecter et afficher les données dans votre UI

Pour connecter le flux paginé à l'UI, obtenez le flux à partir de votre ViewModel et transmettez-le à votre composable de liste.

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val userFlow = viewModel.userPagingFlow
    UserList(flow = userFlow)
}

Utilisez collectAsLazyPagingItems pour convertir le flux PagingData en LazyPagingItems. Utilisez ensuite l'API items dans un LazyColumn pour disposer chaque élément.

Assurez-vous de fournir un identifiant unique et stable pour chaque article à l'aide de itemKey. L'exemple suivant utilise it.id (en référence à la propriété User.id), car il reste stable pour l'instance User lors des mises à jour des données.

@Composable
fun UserList(flow: Flow<PagingData<User>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val user = lazyPagingItems[index]
            if (user != null) {
                UserRow(user)
            } else {
                UserPlaceholder()
            }
        }
    }
}

La bibliothèque Paging utilise null pour les espaces réservés pendant le chargement d'une page. Par conséquent, si vous avez activé les espaces réservés, vous devez gérer les valeurs null dans le bloc de contenu.

La liste affiche désormais les données paginées, et la bibliothèque Paging charge des pages supplémentaires à mesure que l'utilisateur fait défiler le contenu.

Ressources supplémentaires

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

Documentation

Afficher le contenu