Gestire e presentare gli stati di caricamento

La libreria di paging tiene traccia dello stato delle richieste di caricamento per i dati impaginati e lo espone tramite la classe LoadState. La tua app può registrare un listener con PagingDataAdapter per ricevere informazioni sullo stato attuale e aggiornare la UI di conseguenza. Questi stati vengono forniti dall'adattatore perché sono sincroni con l'interfaccia utente. Ciò significa che il listener riceve gli aggiornamenti quando il caricamento pagina viene applicato alla UI.

Viene fornito un indicatore LoadState separato per ogni LoadType e ogni tipo di origine dati (PagingSource o RemoteMediator). L'oggetto CombinedLoadStates fornito dal listener fornisce informazioni sullo stato di caricamento di tutti questi indicatori. Puoi usare queste informazioni dettagliate per mostrare gli indicatori di caricamento appropriati ai tuoi utenti.

Caricamento degli stati in corso...

La libreria Paging espone lo stato di caricamento per l'utilizzo nella UI tramite l'oggetto LoadState. LoadState oggetti assumono una delle tre forme seguenti a seconda dello stato di caricamento attuale:

  • Se non ci sono operazioni di caricamento attive e non sono presenti errori, LoadState è un oggetto LoadState.NotLoading. Questa sottoclasse include anche la proprietà endOfPaginationReached, che indica se è stata raggiunta la fine dell'impaginazione.
  • Se è in corso un'operazione di caricamento, LoadState è un oggetto LoadState.Loading.
  • Se c'è un errore, LoadState è un oggetto LoadState.Error.

Esistono due modi per utilizzare LoadState nell'interfaccia utente: tramite un listener o mediante un adattatore elenco speciale per presentare lo stato di caricamento direttamente nell'elenco RecyclerView.

Accesso allo stato di caricamento con un listener

Per ottenere lo stato di caricamento per l'uso generale nella tua UI, usa il flusso di loadStateFlow o il metodo addLoadStateListener() fornito da PagingDataAdapter. Questi meccanismi consentono di accedere a un oggetto CombinedLoadStates che include informazioni sul comportamento di LoadState per ogni tipo di carico.

Nell'esempio seguente, PagingDataAdapter mostra componenti UI diversi a seconda dello stato attuale del caricamento di aggiornamento:

Kotlin

// Activities can use lifecycleScope directly, but Fragments should instead use
// viewLifecycleOwner.lifecycleScope.
lifecycleScope.launch {
  pagingAdapter.loadStateFlow.collectLatest { loadStates ->
    progressBar.isVisible = loadStates.refresh is LoadState.Loading
    retry.isVisible = loadState.refresh !is LoadState.Loading
    errorMsg.isVisible = loadState.refresh is LoadState.Error
  }
}

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  progressBar.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.VISIBLE : View.GONE);
  retry.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.GONE : View.VISIBLE);
  errorMsg.setVisibility(loadStates.refresh instanceof LoadState.Error
    ? View.VISIBLE : View.GONE);
});

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  progressBar.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.VISIBLE : View.GONE);
  retry.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.GONE : View.VISIBLE);
  errorMsg.setVisibility(loadStates.refresh instanceof LoadState.Error
    ? View.VISIBLE : View.GONE);
});

Per maggiori informazioni su CombinedLoadStates, consulta Accedere a ulteriori informazioni sullo stato di caricamento.

Presentare lo stato di caricamento con un adattatore

La libreria di paging fornisce un altro adattatore elenco chiamato LoadStateAdapter per presentare lo stato di caricamento direttamente nell'elenco visualizzato dei dati di impaginazione. Questo adattatore consente di accedere allo stato di caricamento corrente dell'elenco, che puoi passare a un gestore di visualizzazioni personalizzato che mostra le informazioni.

Per prima cosa, crea una classe segnaposto che mantenga i riferimenti al caricamento e alle visualizzazioni di errori sullo schermo. Crea una funzione bind() che accetti LoadState come parametro. Questa funzione deve attivare/disattivare la visibilità della visualizzazione in base al parametro dello stato di caricamento:

Kotlin

class LoadStateViewHolder(
  parent: ViewGroup,
  retry: () -> Unit
) : RecyclerView.ViewHolder(
  LayoutInflater.from(parent.context)
    .inflate(R.layout.load_state_item, parent, false)
) {
  private val binding = LoadStateItemBinding.bind(itemView)
  private val progressBar: ProgressBar = binding.progressBar
  private val errorMsg: TextView = binding.errorMsg
  private val retry: Button = binding.retryButton
    .also {
      it.setOnClickListener { retry() }
    }

  fun bind(loadState: LoadState) {
    if (loadState is LoadState.Error) {
      errorMsg.text = loadState.error.localizedMessage
    }

    progressBar.isVisible = loadState is LoadState.Loading
    retry.isVisible = loadState is LoadState.Error
    errorMsg.isVisible = loadState is LoadState.Error
  }
}

Java

class LoadStateViewHolder extends RecyclerView.ViewHolder {
  private ProgressBar mProgressBar;
  private TextView mErrorMsg;
  private Button mRetry;

  LoadStateViewHolder(
    @NonNull ViewGroup parent,
    @NonNull View.OnClickListener retryCallback) {
    super(LayoutInflater.from(parent.getContext())
      .inflate(R.layout.load_state_item, parent, false));

    LoadStateItemBinding binding = LoadStateItemBinding.bind(itemView);
    mProgressBar = binding.progressBar;
    mErrorMsg = binding.errorMsg;
    mRetry = binding.retryButton;
  }

  public void bind(LoadState loadState) {
    if (loadState instanceof LoadState.Error) {
      LoadState.Error loadStateError = (LoadState.Error) loadState;
      mErrorMsg.setText(loadStateError.getError().getLocalizedMessage());
    }
    mProgressBar.setVisibility(loadState instanceof LoadState.Loading
      ? View.VISIBLE : View.GONE);
    mRetry.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
    mErrorMsg.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
  }
}

Java

class LoadStateViewHolder extends RecyclerView.ViewHolder {
  private ProgressBar mProgressBar;
  private TextView mErrorMsg;
  private Button mRetry;

  LoadStateViewHolder(
    @NonNull ViewGroup parent,
    @NonNull View.OnClickListener retryCallback) {
    super(LayoutInflater.from(parent.getContext())
      .inflate(R.layout.load_state_item, parent, false));

    LoadStateItemBinding binding = LoadStateItemBinding.bind(itemView);
    mProgressBar = binding.progressBar;
    mErrorMsg = binding.errorMsg;
    mRetry = binding.retryButton;
  }

  public void bind(LoadState loadState) {
    if (loadState instanceof LoadState.Error) {
      LoadState.Error loadStateError = (LoadState.Error) loadState;
      mErrorMsg.setText(loadStateError.getError().getLocalizedMessage());
    }
    mProgressBar.setVisibility(loadState instanceof LoadState.Loading
      ? View.VISIBLE : View.GONE);
    mRetry.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
    mErrorMsg.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
  }
}

Quindi, crea una classe che implementi LoadStateAdapter e definisci i metodi onCreateViewHolder() e onBindViewHolder(). Questi metodi creano un'istanza del supporto delle visualizzazioni personalizzato e associano lo stato di caricamento associato.

Kotlin

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is LoadState.Error.
class ExampleLoadStateAdapter(
  private val retry: () -> Unit
) : LoadStateAdapter<LoadStateViewHolder>() {

  override fun onCreateViewHolder(
    parent: ViewGroup,
    loadState: LoadState
  ) = LoadStateViewHolder(parent, retry)

  override fun onBindViewHolder(
    holder: LoadStateViewHolder,
    loadState: LoadState
  ) = holder.bind(loadState)
}

Java

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is LoadState.Error.
class ExampleLoadStateAdapter extends LoadStateAdapter<LoadStateViewHolder> {
  private View.OnClickListener mRetryCallback;

  ExampleLoadStateAdapter(View.OnClickListener retryCallback) {
    mRetryCallback = retryCallback;
  }

  @NotNull
  @Override
  public LoadStateViewHolder onCreateViewHolder(@NotNull ViewGroup parent,
    @NotNull LoadState loadState) {
    return new LoadStateViewHolder(parent, mRetryCallback);
  }

  @Override
  public void onBindViewHolder(@NotNull LoadStateViewHolder holder,
    @NotNull LoadState loadState) {
    holder.bind(loadState);
  }
}

Java

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is LoadState.Error.
class ExampleLoadStateAdapter extends LoadStateAdapter<LoadStateViewHolder> {
  private View.OnClickListener mRetryCallback;

  ExampleLoadStateAdapter(View.OnClickListener retryCallback) {
    mRetryCallback = retryCallback;
  }

  @NotNull
  @Override
  public LoadStateViewHolder onCreateViewHolder(@NotNull ViewGroup parent,
    @NotNull LoadState loadState) {
    return new LoadStateViewHolder(parent, mRetryCallback);
  }

  @Override
  public void onBindViewHolder(@NotNull LoadStateViewHolder holder,
    @NotNull LoadState loadState) {
    holder.bind(loadState);
  }
}

Per visualizzare l'avanzamento del caricamento in un'intestazione e un piè di pagina, chiama il metodo withLoadStateHeaderAndFooter() dall'oggetto PagingDataAdapter:

Kotlin

pagingAdapter
  .withLoadStateHeaderAndFooter(
    header = ExampleLoadStateAdapter(adapter::retry),
    footer = ExampleLoadStateAdapter(adapter::retry)
  )

Java

pagingAdapter
  .withLoadStateHeaderAndFooter(
    new ExampleLoadStateAdapter(pagingAdapter::retry),
    new ExampleLoadStateAdapter(pagingAdapter::retry));

Java

pagingAdapter
  .withLoadStateHeaderAndFooter(
    new ExampleLoadStateAdapter(pagingAdapter::retry),
    new ExampleLoadStateAdapter(pagingAdapter::retry));

Puoi chiamare withLoadStateHeader() o withLoadStateFooter() se vuoi che l'elenco RecyclerView mostri lo stato di caricamento solo nell'intestazione o solo nel piè di pagina.

Accedere a informazioni aggiuntive sullo stato di caricamento

L'oggetto CombinedLoadStates di PagingDataAdapter fornisce informazioni sugli stati di caricamento dell'implementazione PagingSource e anche dell'implementazione RemoteMediator, se esistente.

Per praticità, puoi utilizzare le proprietà refresh, append e prepend di CombinedLoadStates per accedere a un oggetto LoadState per il tipo di caricamento appropriato. Queste proprietà in genere fanno riferimento allo stato di caricamento dell'implementazione RemoteMediator, se esistente; in caso contrario, contengono lo stato di caricamento appropriato dell'implementazione PagingSource. Per informazioni più dettagliate sulla logica di base, consulta la documentazione di riferimento per CombinedLoadStates.

Kotlin

lifecycleScope.launch {
  pagingAdapter.loadStateFlow.collectLatest { loadStates ->
    // Observe refresh load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    refreshLoadState: LoadState = loadStates.refresh
    // Observe prepend load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    prependLoadState: LoadState = loadStates.prepend
    // Observe append load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    appendLoadState: LoadState = loadStates.append
  }
}

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Observe refresh load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState refreshLoadState = loadStates.refresh;
  // Observe prepend load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState prependLoadState = loadStates.prepend;
  // Observe append load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState appendLoadState = loadStates.append;
});

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Observe refresh load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState refreshLoadState = loadStates.refresh;
  // Observe prepend load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState prependLoadState = loadStates.prepend;
  // Observe append load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState appendLoadState = loadStates.append;
});

Tuttavia, è importante ricordare che solo gli stati di caricamento PagingSource sono garantiti in modo sincrono con gli aggiornamenti dell'interfaccia utente. Poiché le proprietà refresh, append e prepend possono assumere lo stato di caricamento da PagingSource o RemoteMediator, non è garantito che siano sincroni con gli aggiornamenti dell'interfaccia utente. Ciò può causare problemi nell'interfaccia utente in cui il caricamento sembra completare prima che i nuovi dati siano stati aggiunti alla UI.

Per questo motivo, le funzioni di accesso pratico sono ideali per visualizzare lo stato di caricamento in un'intestazione o in un piè di pagina, ma per altri casi d'uso potrebbe essere necessario accedere in modo specifico allo stato di caricamento da PagingSource o RemoteMediator. CombinedLoadStates fornisce le proprietà source e mediator a questo scopo. Ognuna di queste proprietà espone un oggetto LoadStates che contiene rispettivamente gli oggetti LoadState per PagingSource o RemoteMediator:

Kotlin

lifecycleScope.launch {
  pagingAdapter.loadStateFlow.collectLatest { loadStates ->
    // Directly access the RemoteMediator refresh load state.
    mediatorRefreshLoadState: LoadState? = loadStates.mediator.refresh
    // Directly access the RemoteMediator append load state.
    mediatorAppendLoadState: LoadState? = loadStates.mediator.append
    // Directly access the RemoteMediator prepend load state.
    mediatorPrependLoadState: LoadState? = loadStates.mediator.prepend
    // Directly access the PagingSource refresh load state.
    sourceRefreshLoadState: LoadState = loadStates.source.refresh
    // Directly access the PagingSource append load state.
    sourceAppendLoadState: LoadState = loadStates.source.append
    // Directly access the PagingSource prepend load state.
    sourcePrependLoadState: LoadState = loadStates.source.prepend
  }
}

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Directly access the RemoteMediator refresh load state.
  LoadState mediatorRefreshLoadState = loadStates.mediator.refresh;
  // Directly access the RemoteMediator append load state.
  LoadState mediatorAppendLoadState = loadStates.mediator.append;
  // Directly access the RemoteMediator prepend load state.
  LoadState mediatorPrependLoadState = loadStates.mediator.prepend;
  // Directly access the PagingSource refresh load state.
  LoadState sourceRefreshLoadState = loadStates.source.refresh;
  // Directly access the PagingSource append load state.
  LoadState sourceAppendLoadState = loadStates.source.append;
  // Directly access the PagingSource prepend load state.
  LoadState sourcePrependLoadState = loadStates.source.prepend;
});

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Directly access the RemoteMediator refresh load state.
  LoadState mediatorRefreshLoadState = loadStates.mediator.refresh;
  // Directly access the RemoteMediator append load state.
  LoadState mediatorAppendLoadState = loadStates.mediator.append;
  // Directly access the RemoteMediator prepend load state.
  LoadState mediatorPrependLoadState = loadStates.mediator.prepend;
  // Directly access the PagingSource refresh load state.
  LoadState sourceRefreshLoadState = loadStates.source.refresh;
  // Directly access the PagingSource append load state.
  LoadState sourceAppendLoadState = loadStates.source.append;
  // Directly access the PagingSource prepend load state.
  LoadState sourcePrependLoadState = loadStates.source.prepend;
});

Operatori di catena su LoadState

Poiché l'oggetto CombinedLoadStates consente di accedere a tutte le modifiche dello stato di caricamento, è importante filtrare il flusso dello stato di caricamento in base a eventi specifici. In questo modo puoi aggiornare l'interfaccia utente al momento opportuno per evitare interruzioni e aggiornamenti non necessari dell'interfaccia utente.

Ad esempio, supponiamo che tu voglia mostrare una visualizzazione vuota, ma solo al termine del caricamento iniziale dei dati. Questo caso d'uso richiede la verifica dell'inizio del caricamento di un aggiornamento dei dati, quindi attendi lo stato NotLoading per confermare che l'aggiornamento sia stato completato. Devi filtrare tutti gli indicatori tranne quelli necessari:

Kotlin

lifecycleScope.launchWhenCreated {
  adapter.loadStateFlow
    // Only emit when REFRESH LoadState for RemoteMediator changes.
    .distinctUntilChangedBy { it.refresh }
    // Only react to cases where REFRESH completes, such as NotLoading.
    .filter { it.refresh is LoadState.NotLoading }
    // Scroll to top is synchronous with UI updates, even if remote load was
    // triggered.
    .collect { binding.list.scrollToPosition(0) }
}

Java

PublishSubject<CombinedLoadStates> subject = PublishSubject.create();
Disposable disposable =
  subject.distinctUntilChanged(CombinedLoadStates::getRefresh)
  .filter(
    combinedLoadStates -> combinedLoadStates.getRefresh() instanceof LoadState.NotLoading)
  .subscribe(combinedLoadStates -> binding.list.scrollToPosition(0));

pagingAdapter.addLoadStateListener(loadStates -> {
  subject.onNext(loadStates);
});

Java

LiveData<CombinedLoadStates> liveData = new MutableLiveData<>();
LiveData<LoadState> refreshLiveData =
  Transformations.map(liveData, CombinedLoadStates::getRefresh);
LiveData<LoadState> distinctLiveData =
  Transformations.distinctUntilChanged(refreshLiveData);

distinctLiveData.observeForever(loadState -> {
  if (loadState instanceof LoadState.NotLoading) {
    binding.list.scrollToPosition(0);
  }
});

Questo esempio attende che venga aggiornato lo stato di caricamento dell'aggiornamento, ma si attiva solo quando lo stato è NotLoading. In questo modo l'aggiornamento remoto è stato completato prima di qualsiasi aggiornamento dell'interfaccia utente.

Le API Stream rendono possibile questo tipo di operazione. La tua app può specificare gli eventi di carico di cui ha bisogno e gestire i nuovi dati quando vengono soddisfatti i criteri appropriati.