Carica e visualizza i dati impaginati

La libreria Paging offre funzionalità efficaci per caricare e visualizzare i dati paginati di un set di dati più grande. Questa guida illustra come utilizzare la libreria Paging per configurare uno stream di dati paginati da un'origine dati di rete e visualizzarlo in un RecyclerView.

Definire un'origine dati

Il primo passaggio consiste nel definire un'implementazione di PagingSource per identificare l'origine dati. La classe dell'API PagingSource include il metodo load() che puoi sostituire per indicare come recuperare i dati paginati 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:

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 e il valore è il tipo di dati stessi. Ad esempio, se carichi pagine di oggetti User 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 pagine di elementi in base al numero di pagina. Il tipo Key è Int e 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 passa i parametri forniti nel suo 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 dati
  • query: la query di ricerca da inviare al servizio indicato da backend

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 a load() sia riuscita:

  • Se il caricamento va a buon fine, 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.

A ogni chiamata di load(), ExamplePagingSource acquisisce la chiave corrente
    e restituisce la chiave successiva da caricare.
Figura 1. Diagramma che mostra come load() utilizza e aggiorna la chiave.

L'implementazione di PagingSource deve anche implementare un metodo getRefreshKey() che accetti un oggetto PagingState come parametro. Restituisce la chiave da passare al metodo load() quando i dati vengono aggiornati o non sono più validi dopo il caricamento iniziale. La libreria di paginazione chiama questo metodo automaticamente agli aggiornamenti successivi dei dati.

Gestire gli errori

Le richieste di caricamento dei dati possono non riuscire per diversi motivi, in particolare quando il caricamento avviene su una rete. Segnala gli errori rilevati 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():

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 PagingSource riferimento all'API.

PagingSource raccoglie e invia oggetti LoadResult.Error all'interfaccia utente in modo che tu possa intervenire. Per ulteriori informazioni sull'esposizione dello stato di caricamento nell'interfaccia utente, consulta Gestire e presentare gli stati di caricamento.

Configurare uno stream di PagingData

Poi, devi avere uno stream di dati paginati dall'implementazione di PagingSource. Configura lo stream di dati in ViewModel. La classe Pager fornisce metodi che espongono uno stream 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 lo stream reattivo, devi fornire all'istanza un oggetto di configurazione PagingConfig e una funzione che indichi a Pager come ottenere un'istanza dell'implementazione 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 condivisibile lo stream di dati e memorizza nella cache i dati caricati con il CoroutineScope fornito. Questo esempio utilizza viewModelScope fornito dall'elemento lifecycle-viewmodel-ktx del ciclo di vita.

L'oggetto Pager chiama il metodo load() dell'oggetto PagingSource, fornendogli l'oggetto LoadParams e ricevendo in cambio l'oggetto LoadResult.

Definire un adattatore RecyclerView

Devi anche configurare un'opzione di importazione per ricevere i dati nell'elenco RecyclerView. La libreria Paging fornisce la classe PagingDataAdapter per questo scopo.

Definisci una classe che estenda PagingDataAdapter. Nell'esempio, UserAdapter estende PagingDataAdapter per fornire un adattatore RecyclerView per gli elementi dell'elenco di tipo User e utilizza UserViewHolder come contenitore di visualizzazioni:

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 DiffUtil.ItemCallback. Il funzionamento è lo stesso che normalmente si verifica quando si definiscono gli adattatori di 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 paginati nell'interfaccia utente

Ora che hai definito un PagingSource, hai creato un modo per la tua app di generare uno stream di PagingData e hai definito un PagingDataAdapter, puoi collegare questi elementi e visualizzare i dati paginati nella tua attività.

Esegui i seguenti passaggi nel metodo onCreate o onViewCreated della tua attività o del tuo frammento:

  1. Crea un'istanza della classe PagingDataAdapter.
  2. Passa l'istanza PagingDataAdapter all'elenco RecyclerView in cui vuoi visualizzare i dati paginati.
  3. Osserva lo stream PagingData e passa ogni valore generato al metodo submitData() 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 paginati dell'origine dati e carica automaticamente un'altra pagina, se necessario.

Risorse aggiuntive

Per scoprire di più sulla libreria Paging, consulta le seguenti risorse aggiuntive:

Codelab