Gestire e presentare gli stati di caricamento (visualizzazioni)

Concetti e implementazione di Jetpack Compose

La libreria Paging tiene traccia dello stato delle richieste di caricamento dei dati paginati ed espone it tramite la LoadState classe. La tua app può registrare un listener con il PagingDataAdapter per ricevere informazioni sullo stato attuale e aggiornare l'UI di conseguenza. Questi stati vengono forniti dall'adattatore perché sono sincroni con l'UI. Ciò significa che il listener riceve gli aggiornamenti quando il caricamento pagina è stato applicato all'UI.

Viene fornito un segnale LoadState separato per ogni LoadType e tipo di origine dati (either PagingSource or RemoteMediator). L' CombinedLoadStates oggetto fornito dal listener fornisce informazioni sullo stato di caricamento da tutti questi segnali. Puoi utilizzare queste informazioni dettagliate per mostrare agli utenti gli indicatori di caricamento appropriati.

Stati di caricamento

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

  • Se non è presente alcuna operazione di caricamento attiva e nessun errore, LoadState è un LoadState.NotLoading oggetto. Questa sottoclasse include anche la endOfPaginationReached proprietà, che indica se è stata raggiunta la fine della paginazione.
  • Se è presente un'operazione di caricamento attiva, LoadState è un LoadState.Loading oggetto.
  • Se si verifica un errore, LoadState è un LoadState.Error oggetto.

Esistono due modi per utilizzare LoadState nell'UI: utilizzando un listener o un adattatore di elenco speciale per presentare lo stato di caricamento direttamente nell' RecyclerView elenco.

Accedere allo stato di caricamento con un listener

Per ottenere lo stato di caricamento per l'utilizzo generale nell'UI, utilizza lo loadStateFlow stream o il addLoadStateListener() metodo fornito da PagingDataAdapter. Questi meccanismi forniscono l'accesso a un CombinedLoadStates oggetto che include informazioni sul LoadState comportamento per ogni tipo di caricamento.

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 ulteriori informazioni su CombinedLoadStates, consulta Accedere a informazioni aggiuntive sullo stato di caricamento.

Presentare lo stato di caricamento con un adattatore

La libreria Paging fornisce un altro adattatore di elenco chiamato LoadStateAdapter per lo scopo di presentare lo stato di caricamento direttamente nell'elenco visualizzato dei dati paginati. Questo adattatore fornisce l'accesso allo stato di caricamento attuale dell'elenco, che puoi passare a un titolare della visualizzazione personalizzato che visualizza le informazioni.

Innanzitutto, crea una classe di titolare della visualizzazione che mantenga i riferimenti alle visualizzazioni di caricamento ed errore sullo schermo. Crea una funzione bind() che accetta un 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);
  }
}

Poi, crea una classe che implementa LoadStateAdapter e definisci i onCreateViewHolder() e i onBindViewHolder() metodi. Questi metodi creano un'istanza del titolare della visualizzazione 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 withLoadStateHeaderAndFooter() metodo 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));

In alternativa, 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 per l'implementazione di PagingSource e per l'implementazione di RemoteMediator, se esistente.

Per comodità, puoi utilizzare le refresh, append e prepend proprietà di CombinedLoadStates per accedere a un oggetto LoadState per il tipo di caricamento appropriato. Queste proprietà in genere rimandano allo stato di caricamento dell'implementazione di RemoteMediator, se esistente; in caso contrario, contengono lo stato di caricamento appropriato dell'implementazione di PagingSource. Per informazioni più dettagliate sulla logica sottostante, 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 di PagingSource sono garantiti per essere sincroni con gli aggiornamenti dell'UI. Poiché le proprietà refresh, append e prepend possono potenzialmente assumere lo stato di caricamento da PagingSource o RemoteMediator, non è garantito che siano sincrone con gli aggiornamenti dell'UI. Ciò può causare problemi dell'UI in cui il caricamento sembra terminare prima che i nuovi dati vengano aggiunti all'UI.

Per questo motivo, gli accessor di convenienza funzionano bene per visualizzare lo stato di caricamento in un'intestazione o 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 source e mediator proprietà a questo scopo. Queste proprietà espongono ciascuna un LoadStates oggetto che contiene gli oggetti LoadState per PagingSource o RemoteMediator rispettivamente:

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 concatenamento su LoadState

Poiché l'oggetto CombinedLoadStates fornisce l'accesso a tutte le modifiche dello stato di caricamento, è importante filtrare lo stream dello stato di caricamento in base a eventi specifici. In questo modo, l'UI viene aggiornata al momento opportuno per evitare interruzioni e aggiornamenti non necessari.

Supponiamo, ad esempio, di voler visualizzare una visualizzazione vuota, ma solo dopo il completamento del caricamento iniziale dei dati. Questo caso d'uso richiede di verificare che sia stato avviato un caricamento di aggiornamento dei dati, quindi attendere lo stato NotLoading per confermare che l'aggiornamento è stato completato. Devi filtrare tutti i segnali tranne quelli di cui hai bisogno:

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 l'aggiornamento dello stato di caricamento dell'aggiornamento, ma si attiva solo quando lo stato è NotLoading. In questo modo, l'aggiornamento remoto viene completato prima che vengano apportati aggiornamenti all'UI.

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