Concetti e implementazione di Jetpack Compose
La libreria Paging offre potenti funzionalità per il caricamento e la visualizzazione
di dati paginati da un set di dati più grande. Questa guida mostra come utilizzare la libreria Paging per configurare un flusso di dati impaginati da un'origine dati di rete e visualizzarli in un RecyclerView.
Definire un'origine dati
Il primo passaggio consiste nel definire un'implementazione PagingSource per identificare l'origine dati. La classe API PagingSource include il metodo load,
che esegui l'override per indicare come recuperare i dati impaginati dall'origine dati
corrispondente.
Utilizza direttamente la classe PagingSource 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
ListenableFuturedi Guava, implementaListenableFuturePagingSource.
Seleziona i tipi di 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 di
dati stesso. Ad esempio, se carichi pagine di User oggetti dalla rete
passando i numeri di pagina Int a Retrofit, seleziona Int come
tipo Key e User come tipo Value.
Definisci PagingSource
L'esempio seguente implementa un PagingSource che carica le pagine degli elementi
in base al numero di pagina. Il tipo Key è Int e il tipo Value è User.
Java (RxJava)
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 (Guava/LiveData)
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;
}
}
Una tipica implementazione di PagingSource passa i parametri forniti nel costruttore al metodo load per caricare i dati appropriati per una query. Nell'esempio
riportato sopra, 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 sigillata che assume una delle due forme, a seconda che la chiamata load sia andata a buon fine:
- Se il caricamento ha esito positivo, restituisci un oggetto
LoadResult.Page. - Se il caricamento non va a buon fine, restituisci un oggetto
LoadResult.Error.
La figura seguente illustra come la funzione load in questo esempio riceve la chiave per ogni caricamento e fornisce la chiave per il caricamento successivo.
load utilizza e aggiorna la chiave.
L'implementazione di PagingSource deve implementare anche un metodo getRefreshKey
che accetta 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 Paging chiama automaticamente questo metodo
nei successivi aggiornamenti dei dati.
Gestisci gli errori
Le richieste di caricamento dei dati possono non riuscire per diversi motivi, soprattutto quando il caricamento
avviene tramite una rete. Segnala gli errori riscontrati durante il caricamento restituendo un oggetto
LoadResult.Error dal metodo load.
Ad esempio, puoi rilevare e segnalare gli errori di caricamento in ExamplePagingSource
dell'esempio precedente aggiungendo quanto segue al metodo load:
Java (RxJava)
return backend.searchUsers(searchTerm, nextPageNumber)
.subscribeOn(Schedulers.io())
.map(this::toLoadResult)
.onErrorReturn(LoadResult.Error::new);
Java (Guava/LiveData)
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 API PagingSource.
PagingSource raccoglie e distribuisce gli oggetti LoadResult.Error all'interfaccia utente in modo che tu possa agire di conseguenza. Per ulteriori informazioni sull'esposizione dello stato di caricamento nell'interfaccia utente, consulta Gestire e presentare gli stati di caricamento.
Configurare un flusso di PagingData
A questo punto, hai bisogno di un flusso di dati paginati dall'implementazione di 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 stream, 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 PagingSource:
Java (RxJava)
// 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 (Guava/LiveData)
// 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 condivisibile lo stream di dati e memorizza nella cache i dati caricati con il CoroutineScope fornito. Questo esempio utilizza viewModelScope
fornito dall'artefatto del ciclo di vita lifecycle-viewmodel-ktx.
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 anche 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 di elenco
di tipo User e utilizzando UserViewHolder come view holder:
Kotlin (coroutine)
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 (RxJava)
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 (Guava/LiveData)
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 DiffUtil.ItemCallback. Funziona
come di consueto quando si definiscono gli adattatori di elenco RecyclerView:
Kotlin (coroutine)
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 (RxJava)
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 (Guava/LiveData)
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);
}
}
Visualizzare i dati impaginati nella UI
Ora che hai definito un PagingSource, creato un modo per la tua app di generare un flusso di PagingData e definito un PagingDataAdapter, puoi collegare questi elementi e visualizzare i dati impaginati nella tua attività.
Esegui i seguenti passaggi nel metodo onCreate o onViewCreated dell'attività o del fragment:
- Crea un'istanza della classe
PagingDataAdapter. - Passa l'istanza
PagingDataAdapterall'elencoRecyclerViewin cui vuoi visualizzare i dati paginati. - Osserva lo stream
PagingDatae passa ogni valore generato al metodosubmitData()dell'adattatore.
Kotlin (coroutine)
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 (RxJava)
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 (Guava/LiveData)
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 quando necessario.