La libreria Paging offre funzionalità efficaci per caricare e visualizzare dati di paging da un set di dati più ampio. Questa guida illustra come utilizzare la libreria di paging per configurare un flusso di dati impaginati da un'origine dati di rete e visualizzarli in una RecyclerView
.
Definire un'origine dati
Il primo passaggio consiste nel definire un'implementazione di PagingSource
per identificare l'origine dati. La classe API PagingSource
include il metodo
load()
, che sostituisci per indicare come recuperare i dati impaginati dall'origine dati corrispondente.
Utilizza la classe PagingSource
direttamente per utilizzare le coroutine Kotlin per il caricamento asincrono. La libreria Paging fornisce anche classi per supportare altri framework asincroni:
- Per utilizzare RxJava, implementa
RxPagingSource
. - Per utilizzare
ListenableFuture
di Guava, implementaListenableFuturePagingSource
.
Seleziona tipi chiave e valore
PagingSource<Key, Value>
ha due parametri di tipo: Key
e Value
. La chiave definisce l'identificatore utilizzato per caricare i dati, mentre il valore è il tipo dei dati stessi. Ad esempio, se carichi pagine di oggetti User
dalla rete
passando numeri di pagina Int
a
Retrofit,
seleziona Int
come tipo Key
e User
come tipo Value
.
Definisci PagingSource
L'esempio seguente implementa un elemento PagingSource
che carica le pagine di elementi per numero di pagina. Il tipo Key
è Int
, mentre il tipo Value
è 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; } }
Un'implementazione PagingSource
tipica trasmette i parametri forniti nel proprio
costruttore al metodo load()
per caricare i dati appropriati per una query. Nell'esempio precedente, questi parametri sono:
backend
: un'istanza del servizio di backend che fornisce i datiquery
: la query di ricerca da inviare al servizio indicato dabackend
L'oggetto LoadParams
contiene informazioni sull'operazione di caricamento da eseguire. Sono inclusi la chiave da caricare e il numero di elementi da caricare.
L'oggetto LoadResult
contiene il risultato dell'operazione di caricamento. LoadResult
è una classe Sealed che assume una delle due forme seguenti, a seconda dell'esito della chiamata load()
:
- Se il caricamento ha esito positivo, restituisci un oggetto
LoadResult.Page
. - Se il caricamento non riesce, restituisci un oggetto
LoadResult.Error
.
La figura seguente illustra il modo in cui la funzione load()
in questo esempio
riceve la chiave per ogni caricamento e fornisce la chiave per il caricamento successivo.
L'implementazione PagingSource
deve inoltre implementare un metodo
getRefreshKey()
che utilizzi un oggetto
PagingState
come
parametro. Restituisce la chiave da passare al metodo load()
quando i dati vengono aggiornati o invalidati dopo il caricamento iniziale. La libreria di paging chiama automaticamente questo metodo agli aggiornamenti successivi dei dati.
Gestire gli errori
Le richieste di caricamento dei dati possono avere esito negativo per diversi motivi, soprattutto durante il caricamento
su una rete. Segnala gli errori riscontrati durante il caricamento restituendo un oggetto LoadResult.Error
dal metodo load()
.
Ad esempio, puoi individuare e segnalare gli errori di caricamento in ExamplePagingSource
dall'esempio precedente aggiungendo quanto segue al metodo 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);
Per ulteriori informazioni sulla gestione degli errori di retrofit, consulta gli esempi nel riferimento dell'API PagingSource
.
PagingSource
raccoglie e invia LoadResult.Error
oggetti alla UI per consentirti di intervenire su di essi. Per ulteriori informazioni sull'esposizione dello stato di caricamento nella UI, consulta Gestire e presentare lo stato di caricamento.
Configura un flusso di PagingData
Successivamente, è necessario uno stream di dati impaginati dall'implementazione PagingSource
.
Configura lo stream di dati in ViewModel
. La classe Pager
fornisce metodi che espongono un flusso reattivo di oggetti PagingData
da un PagingSource
. La libreria Paging supporta l'utilizzo di diversi tipi di flusso,
tra cui Flow
, LiveData
e i tipi Flowable
e Observable
di
RxJava.
Quando crei un'istanza Pager
per configurare il flusso reattivo, devi fornire all'istanza un oggetto di configurazione PagingConfig
e una funzione che indichi a Pager
come ottenere un'istanza dell'implementazione di 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'operatore cachedIn()
rende lo stream di dati condivisibile e memorizza nella cache i dati caricati con il CoroutineScope
fornito. Questo esempio utilizza l'elemento viewModelScope
fornito dall'artefatto lifecycle-viewmodel-ktx
del ciclo di vita.
L'oggetto Pager
chiama il metodo load()
dall'oggetto PagingSource
, fornendogli l'oggetto LoadParams
e ricevendo in cambio l'oggetto LoadResult
.
Definisci un adattatore RecyclerView
Devi inoltre configurare un adattatore per ricevere i dati nell'elenco RecyclerView
. La libreria Paging fornisce la classe PagingDataAdapter
per questo
scopo.
Definisci una classe che estende PagingDataAdapter
. Nell'esempio, UserAdapter
estende PagingDataAdapter
per fornire un adattatore RecyclerView
per gli elementi elenco di tipo User
e utilizzando UserViewHolder
come proprietario della visualizzazione:
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); } }
L'adattatore deve anche definire i metodi onCreateViewHolder()
e onBindViewHolder()
e specificare un valore DiffUtil.ItemCallback
.
Funziona come di consueto quando definisci gli adattatori dell'elenco 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); } }
Visualizza i dati impaginati nell'interfaccia utente
Ora che hai definito un PagingSource
, hai creato un modo per consentire alla tua app di
generare un flusso di PagingData
e di definire un PagingDataAdapter
, puoi
collegare questi elementi e visualizzare i dati impaginati nelle tue
attività.
Esegui i seguenti passaggi nel metodo onCreate
o onViewCreated
dell'attività:
- Crea un'istanza della classe
PagingDataAdapter
. - Passa l'istanza
PagingDataAdapter
all'elencoRecyclerView
in cui vuoi visualizzare i dati impaginati. - Osserva lo stream
PagingData
e passa ogni valore generato al metodosubmitData()
dell'adattatore.
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));
L'elenco RecyclerView
ora mostra i dati impaginati dell'origine dati e carica automaticamente un'altra pagina, se necessario.
Risorse aggiuntive
Per saperne di più sulla libreria Paging, consulta le seguenti risorse aggiuntive:
Codelab
Samples
- Esempio di paging dei componenti dell'architettura Android
- Esempio di Paging dei componenti dell'architettura Android con la rete
Consigliato per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Pagina dalla rete e dal database
- Eseguire la migrazione a Paging 3
- Panoramica della libreria di pagine di destinazione