Ladezustände (Ansichten) verwalten und präsentieren

Konzepte und Jetpack Compose-Implementierung

Die Paging-Bibliothek verfolgt den Status von Ladeanfragen für Seiten-Daten und stellt ihn über die LoadState Klasse zur Verfügung. Ihre App kann einen Listener mit dem PagingDataAdapter registrieren, um Informationen zum aktuellen Status zu erhalten und die UI entsprechend zu aktualisieren. Diese Status werden vom Adapter bereitgestellt, da sie synchron mit der UI sind. Das bedeutet, dass Ihr Listener Aktualisierungen erhält, wenn der Seitenaufbau auf die UI angewendet wurde.

Für jedes separate LoadState Signal wird für jeden LoadType und Datenquelltyp (entweder PagingSource oder RemoteMediator) bereitgestellt. Das CombinedLoadStates Objekt, das vom Listener bereitgestellt wird, enthält Informationen zum Ladestatus aus all diesen Signalen. Anhand dieser detaillierten Informationen können Sie Ihren Nutzern die entsprechenden Ladeindikatoren anzeigen.

Ladestatus

Die Paging-Bibliothek stellt den Ladestatus für die Verwendung in der UI über das Objekt LoadState zur Verfügung. LoadState -Objekte haben je nach aktuellem Ladestatus eine von drei Formen:

  • Wenn kein aktiver Ladevorgang und kein Fehler vorliegt, ist LoadState ein LoadState.NotLoading Objekt. Diese Unterklasse enthält auch die endOfPaginationReached Property, die angibt, ob das Ende der Paginierung erreicht wurde.
  • Wenn ein aktiver Ladevorgang vorliegt, ist LoadState ein LoadState.Loading Objekt.
  • Wenn ein Fehler vorliegt, ist LoadState ein LoadState.Error-Objekt.

Es gibt zwei Möglichkeiten, LoadState in Ihrer UI zu verwenden: mit einem Listener oder mit einem speziellen Listenadapter, um den Ladestatus direkt in der RecyclerView Liste darzustellen.

Auf den Ladestatus mit einem Listener zugreifen

Wenn Sie den Ladestatus für die allgemeine Verwendung in Ihrer UI abrufen möchten, verwenden Sie den loadStateFlow Stream oder die addLoadStateListener() Methode, die von Ihrem PagingDataAdapter bereitgestellt werden. Diese Mechanismen ermöglichen den Zugriff auf ein CombinedLoadStates-Objekt, das Informationen zum LoadState Verhalten für jeden Ladetyp enthält.

Im folgenden Beispiel zeigt PagingDataAdapter je nach aktuellem Status des Aktualisierungsladevorgangs verschiedene UI-Komponenten an:

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);
});

Weitere Informationen zu CombinedLoadStates finden Sie unter Zusätzliche Informationen zum Ladestatus abrufen.

Ladestatus mit einem Adapter präsentieren

Die Paging-Bibliothek bietet einen weiteren Listenadapter namens LoadStateAdapter, mit dem der Ladestatus direkt in der angezeigten Liste der Seiten- Daten dargestellt werden kann. Dieser Adapter ermöglicht den Zugriff auf den aktuellen Ladestatus der Liste, den Sie an einen benutzerdefinierten View-Holder übergeben können, der die Informationen anzeigt.

Erstellen Sie zuerst eine View-Holder-Klasse, die Verweise auf die Lade- und Fehleransichten auf dem Bildschirm enthält. Erstellen Sie eine bind()-Funktion, die ein LoadState als Parameter akzeptiert. Diese Funktion sollte die Sichtbarkeit der Ansicht basierend auf dem Parameter für den Ladestatus umschalten:

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);
  }
}

Erstellen Sie als Nächstes eine Klasse, die LoadStateAdapter implementiert, und definieren Sie die onCreateViewHolder() und onBindViewHolder() Methoden. Mit diesen Methoden wird eine Instanz Ihres benutzerdefinierten View-Holders erstellt und der zugehörige Ladestatus gebunden.

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);
  }
}

Wenn Sie den Ladefortschritt in einem Header und einem Footer anzeigen möchten, rufen Sie die withLoadStateHeaderAndFooter() Methode aus Ihrem PagingDataAdapter Objekt auf:

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));

Sie können stattdessen withLoadStateHeader() oder withLoadStateFooter() aufrufen, wenn die RecyclerView Liste den Ladestatus nur im Header oder nur im Footer anzeigen soll.

Zusätzliche Informationen zum Ladestatus abrufen

Das CombinedLoadStates-Objekt von PagingDataAdapter enthält Informationen zu den Ladestatus für Ihre PagingSource-Implementierung und auch für Ihre RemoteMediator-Implementierung, falls vorhanden.

Sie können die refresh, append und prepend Properties aus CombinedLoadStates verwenden, um auf ein LoadState Objekt für den entsprechenden Ladetyp zuzugreifen. Diese Properties verweisen in der Regel auf den Ladestatus aus der RemoteMediator-Implementierung, falls vorhanden. Andernfalls enthalten sie den entsprechenden Ladestatus aus der PagingSource-Implementierung. Weitere Informationen zur zugrunde liegenden Logik finden Sie in der Referenzdokumentation zu 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;
});

Es ist jedoch wichtig zu beachten, dass nur die PagingSource-Ladestatus garantiert synchron mit UI-Aktualisierungen sind. Da die Properties refresh, append und prepend den Ladestatus möglicherweise von PagingSource oder RemoteMediator übernehmen, sind sie nicht garantiert synchron mit UI-Aktualisierungen. Dies kann zu UI-Problemen führen, bei denen der Ladevorgang abgeschlossen zu sein scheint, bevor neue Daten zur UI hinzugefügt wurden.

Aus diesem Grund eignen sich die Convenience-Accessors gut, um den Ladestatus in einem Header oder Footer anzuzeigen. Für andere Anwendungsfälle müssen Sie jedoch möglicherweise speziell auf den Ladestatus von PagingSource oder RemoteMediator zugreifen. CombinedLoadStates bietet die source und mediator Properties für diesen Zweck. Diese Properties stellen jeweils ein LoadStates-Objekt zur Verfügung, das die LoadState-Objekte für PagingSource bzw. RemoteMediator enthält:

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;
});

Operatoren für LoadState verketten

Da das CombinedLoadStates-Objekt Zugriff auf alle Änderungen des Ladestatus bietet, ist es wichtig, den Stream des Ladestatus anhand bestimmter Ereignisse zu filtern. So können Sie Ihre UI zum richtigen Zeitpunkt aktualisieren, um Ruckler und unnötige UI-Aktualisierungen zu vermeiden.

Angenommen, Sie möchten eine leere Ansicht anzeigen, aber erst nachdem der erste Ladevorgang abgeschlossen ist. In diesem Anwendungsfall müssen Sie prüfen, ob ein Datenaktualisierungsladevorgang gestartet wurde, und dann auf den Status NotLoading warten, um zu bestätigen, dass die Aktualisierung abgeschlossen ist. Sie müssen alle Signale herausfiltern, die Sie nicht benötigen:

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);
  }
});

In diesem Beispiel wird gewartet, bis der Ladestatus der Aktualisierung aktualisiert wurde, aber nur ausgelöst, wenn der Status NotLoading ist. So wird sichergestellt, dass die Remote-Aktualisierung vollständig abgeschlossen ist, bevor UI-Aktualisierungen erfolgen.

Stream-APIs ermöglichen diese Art von Vorgang. Ihre App kann die benötigten Ladeereignisse angeben und die neuen Daten verarbeiten, wenn die entsprechenden Kriterien erfüllt sind.