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 un RecyclerView
.
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. La bibliothèque Paging fournit également des classes compatibles avec d'autres frameworks asynchrones :
- Pour utiliser RxJava, implémentez plutôt
RxPagingSource
. - Pour utiliser
ListenableFuture
à partir de Guava, implémentez plutôtListenableFuturePagingSource
.
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
.
Kotlin
class ExamplePagingSource( val backend: ExampleBackendService, val query: String ) : PagingSource<Int, User>() { override suspend fun load( params: LoadParams<Int> ): LoadResult<Int, User> { 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 = response.nextPageNumber ) } 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) } } }
Java
class ExamplePagingSource extends RxPagingSource<Integer, User> { @NonNull private ExampleBackendService mBackend; @NonNull private String mQuery; ExamplePagingSource(@NonNull ExampleBackendService backend, @NonNull String query) { mBackend = backend; mQuery = query; } @NotNull @Override public Single<LoadResult<Integer, User>> loadSingle( @NotNull LoadParams<Integer> params) { // Start refresh at page 1 if undefined. Integer nextPageNumber = params.getKey(); if (nextPageNumber == null) { nextPageNumber = 1; } return mBackend.searchUsers(mQuery, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new); } private LoadResult<Integer, User> toLoadResult( @NonNull SearchUserResponse response) { return new LoadResult.Page<>( response.getUsers(), null, // Only paging forward. response.getNextPageNumber(), LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } @Nullable @Override public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) { // 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. Integer anchorPosition = state.getAnchorPosition(); if (anchorPosition == null) { return null; } LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition); if (anchorPage == null) { return null; } Integer prevKey = anchorPage.getPrevKey(); if (prevKey != null) { return prevKey + 1; } Integer nextKey = anchorPage.getNextKey(); if (nextKey != null) { return nextKey - 1; } return null; } }
Java
class ExamplePagingSource extends ListenableFuturePagingSource<Integer, User> { @NonNull private ExampleBackendService mBackend; @NonNull private String mQuery; @NonNull private Executor mBgExecutor; ExamplePagingSource( @NonNull ExampleBackendService backend, @NonNull String query, @NonNull Executor bgExecutor) { mBackend = backend; mQuery = query; mBgExecutor = bgExecutor; } @NotNull @Override public ListenableFuture<LoadResult<Integer, User>> loadFuture(@NotNull LoadParams<Integer> params) { // Start refresh at page 1 if undefined. Integer nextPageNumber = params.getKey(); if (nextPageNumber == null) { nextPageNumber = 1; } ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform(mBackend.searchUsers(mQuery, nextPageNumber), this::toLoadResult, mBgExecutor); ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, mBgExecutor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, mBgExecutor); } private LoadResult<Integer, User> toLoadResult(@NonNull SearchUserResponse response) { return new LoadResult.Page<>(response.getUsers(), null, // Only paging forward. response.getNextPageNumber(), LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } @Nullable @Override public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) { // 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. Integer anchorPosition = state.getAnchorPosition(); if (anchorPosition == null) { return null; } LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition); if (anchorPage == null) { return null; } Integer prevKey = anchorPage.getPrevKey(); if (prevKey != null) { return prevKey + 1; } Integer nextKey = anchorPage.getNextKey(); if (nextKey != null) { return nextKey - 1; } return null; } }
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é parbackend
.
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 deux 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
.
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.
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()
:
Kotlin
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) }
Java
return backend.searchUsers(searchTerm, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new);
Java
ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform( backend.searchUsers(query, nextPageNumber), this::toLoadResult, bgExecutor); ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching( pageFuture, HttpException.class, LoadResult.Error::new, bgExecutor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, bgExecutor);
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'interface utilisateur, 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 accepte l'utilisation de plusieurs types de flux, y compris Flow
, LiveData
et les types Flowable
et Observable
de RxJava.
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
:
Kotlin
val flow = Pager( // Configure how data is loaded by passing additional properties to // PagingConfig, such as prefetchDistance. PagingConfig(pageSize = 20) ) { ExamplePagingSource(backend, query) }.flow .cachedIn(viewModelScope)
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); Pager<Integer, User> pager = Pager<>( new PagingConfig(/* pageSize = */ 20), () -> ExamplePagingSource(backend, query)); Flowable<PagingData<User>> flowable = PagingRx.getFlowable(pager); PagingRx.cachedIn(flowable, viewModelScope);
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); Pager<Integer, User> pager = Pager<>( new PagingConfig(/* pageSize = */ 20), () -> ExamplePagingSource(backend, query)); PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), 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. 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.
Définir un adaptateur RecyclerView
Vous devez également configurer un adaptateur pour recevoir les données dans votre liste RecyclerView
. La bibliothèque Paging fournit la classe PagingDataAdapter
à cette fin.
Définissez une classe qui étend PagingDataAdapter
. Dans l'exemple, UserAdapter
étend PagingDataAdapter
afin de fournir un adaptateur RecyclerView
pour les éléments de liste de type User
utilisant UserViewHolder
comme conteneur de vue :
Kotlin
class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) : PagingDataAdapter<User, UserViewHolder>(diffCallback) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): UserViewHolder { return UserViewHolder(parent) } override fun onBindViewHolder(holder: UserViewHolder, position: Int) { val item = getItem(position) // Note that item can be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item) } }
Java
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> { UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) { super(diffCallback); } @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserViewHolder(parent); } @Override public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { User item = getItem(position); // Note that item can be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item); } }
Java
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> { UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) { super(diffCallback); } @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserViewHolder(parent); } @Override public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { User item = getItem(position); // Note that item can be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item); } }
Votre adaptateur doit également définir les méthodes onCreateViewHolder()
et onBindViewHolder()
et préciser un DiffUtil.ItemCallback
.
Le fonctionnement est le même que lorsque vous définissez des adaptateurs de liste RecyclerView
:
Kotlin
object UserComparator : DiffUtil.ItemCallback<User>() { override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { // Id is unique. return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { return oldItem == newItem } }
Java
class UserComparator extends DiffUtil.ItemCallback<User> { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { // Id is unique. return oldItem.id.equals(newItem.id); } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }
Java
class UserComparator extends DiffUtil.ItemCallback<User> { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { // Id is unique. return oldItem.id.equals(newItem.id); } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }
Afficher les données paginées dans votre interface utilisateur
Maintenant que vous avez défini une PagingSource
, créé un moyen pour votre application de générer un flux de PagingData
et défini un PagingDataAdapter
, vous êtes prêt à connecter ces éléments et à afficher des données paginées dans votre activité.
Procédez comme suit dans la méthode onCreate
de votre activité ou onViewCreated
du fragment :
- Créez une instance de votre classe
PagingDataAdapter
. - Transmettez l'instance
PagingDataAdapter
à la listeRecyclerView
dans laquelle vous souhaitez afficher vos données paginées. - Observez le flux
PagingData
, puis transmettez chaque valeur générée à la méthodesubmitData()
de votre adaptateur.
Kotlin
val viewModel by viewModels<ExampleViewModel>() val pagingAdapter = UserAdapter(UserComparator) val recyclerView = findViewById<RecyclerView>(R.id.recycler_view) recyclerView.adapter = pagingAdapter // Activities can use lifecycleScope directly; fragments use // viewLifecycleOwner.lifecycleScope. lifecycleScope.launch { viewModel.flow.collectLatest { pagingData -> pagingAdapter.submitData(pagingData) } }
Java
ExampleViewModel viewModel = new ViewModelProvider(this) .get(ExampleViewModel.class); UserAdapter pagingAdapter = new UserAdapter(new UserComparator()); RecyclerView recyclerView = findViewById<RecyclerView>( R.id.recycler_view); recyclerView.adapter = pagingAdapter viewModel.flowable // Using AutoDispose to handle subscription lifecycle. // See: https://github.com/uber/AutoDispose. .to(autoDisposable(AndroidLifecycleScopeProvider.from(this))) .subscribe(pagingData -> pagingAdapter.submitData(lifecycle, pagingData));
Java
ExampleViewModel viewModel = new ViewModelProvider(this) .get(ExampleViewModel.class); UserAdapter pagingAdapter = new UserAdapter(new UserComparator()); RecyclerView recyclerView = findViewById<RecyclerView>( R.id.recycler_view); recyclerView.adapter = pagingAdapter // Activities can use getLifecycle() directly; fragments use // getViewLifecycleOwner().getLifecycle(). viewModel.liveData.observe(this, pagingData -> pagingAdapter.submitData(getLifecycle(), pagingData));
La liste RecyclerView
affiche maintenant les données paginées de la source de données et charge automatiquement une autre page si nécessaire.
Ressources supplémentaires
Pour en savoir plus sur la bibliothèque Paging, consultez ces ressources supplémentaires :
Ateliers de programmation
Exemples
- Exemple de pagination de composants d'architecture Android
- Exemple de pagination des composants d'architecture Android avec réseau
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé
- Page du réseau et de la base de données
- Effectuer une migration vers Paging 3
- Présentation de la bibliothèque Paging