Gérer et présenter les états de chargement

La bibliothèque Paging suit l'état des requêtes de chargement pour les données paginées et les expose via la classe LoadState. Votre application peut enregistrer un écouteur avec le PagingDataAdapter pour recevoir des informations sur l'état actuel et mettre à jour l'interface utilisateur en conséquence. Ces états sont fournis par l'adaptateur, car ils coïncident avec les mises à jour de l'UI. Cela signifie que votre écouteur reçoit des mises à jour lorsque le chargement de la page a été appliqué à l'UI.

Un signal LoadState distinct est fourni pour chaque LoadType et type de source de données (soit PagingSource, soit RemoteMediator). L'objet CombinedLoadStates fourni par l'écouteur apporte des informations sur l'état de chargement de tous ces signaux. Vous pouvez utiliser ces informations détaillées pour afficher les indicateurs de chargement appropriés pour vos utilisateurs.

États de chargement

La bibliothèque Paging expose l'état de chargement en vue de son utilisation dans l'interface utilisateur via l'objet LoadState. Les objets LoadState peuvent prendre l'une des trois formes suivantes selon l'état de chargement actuel :

  • S'il n'y a pas d'opération de chargement active ni d'erreur, LoadState est un objet LoadState.NotLoading. Cette sous-classe inclut également la propriété endOfPaginationReached, qui indique si la fin de la pagination a été atteinte.
  • Si une opération de chargement est active, LoadState est un objet LoadState.Loading.
  • En cas d'erreur, LoadState est un objet LoadState.Error.

Il existe deux façons d'utiliser le LoadState dans votre UI : utiliser un écouteur ou un adaptateur de liste spécial pour présenter l'état de chargement directement dans la liste RecyclerView.

Accéder à l'état de chargement avec un écouteur

Pour obtenir l'état de chargement en vue d'une utilisation générale dans l'interface utilisateur, utilisez le flux loadStateFlow ou la méthode addLoadStateListener() fournie par votre PagingDataAdapter. Ces mécanismes permettent d'accéder à un objet CombinedLoadStates qui inclut des informations sur le comportement du LoadState pour chaque type de chargement.

Dans l'exemple suivant, PagingDataAdapter affiche différents composants d'interface utilisateur en fonction de l'état actuel du chargement de l'actualisation :

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

Pour en savoir plus sur CombinedLoadStates, consultez Accéder aux informations supplémentaires sur l'état de chargement.

Présenter l'état de chargement avec un adaptateur

La bibliothèque Paging fournit un autre adaptateur de liste appelé LoadStateAdapter, qui permet de présenter l'état de chargement directement dans la liste des données paginées. Cet adaptateur permet d'accéder à l'état de chargement actuel de la liste, que vous pouvez transmettre à un conteneur de vue personnalisé qui affiche les informations.

Tout d'abord, créez une classe de conteneur de vue qui conserve les références aux vues de chargement et d'erreur à l'écran. Créez une fonction bind() qui accepte un paramètre LoadState. Cette fonction doit activer/désactiver la visibilité de la vue en fonction du paramètre d'état de chargement :

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

Ensuite, créez une classe qui implémente LoadStateAdapter, puis définissez les méthodes onCreateViewHolder() et onBindViewHolder(). Ces méthodes créent une instance de votre conteneur de vue personnalisé et associent l'état de chargement associé.

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

Pour afficher la progression du chargement dans un en-tête et un pied de page, appelez la méthode withLoadStateHeaderAndFooter() à partir de votre objet 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));

Vous pouvez appeler withLoadStateHeader() ou withLoadStateFooter() si vous souhaitez afficher la liste RecyclerView dans l'en-tête ou dans le pied de page.

Accéder à des informations supplémentaires sur l'état de chargement

L'objet CombinedLoadStates de PagingDataAdapter fournit des informations sur les états de chargement de votre implémentation PagingSource ainsi que de votre implémentation RemoteMediator, le cas échéant.

Pour plus de commodité, vous pouvez utiliser les méthodes refresh, append et prepend de CombinedLoadStates pour accéder à un objet LoadState correspondant au type de chargement approprié. Ces propriétés s'appliquent généralement à l'état de chargement de l'implémentation RemoteMediator, le cas échéant. Sinon, ils contiennent l'état de chargement approprié de l'implémentation PagingSource. Pour en savoir plus sur la logique sous-jacente, consultez la documentation de référence sur 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;
});

Toutefois, il est important de noter que seuls les états de chargement PagingSource coïncident avec les mises à jour de l'interface utilisateur. Étant donné que les propriétés refresh, append et prepend peuvent récupérer l'état de chargement à partir de la PagingSource ou du RemoteMediator, il n'est pas garanti qu'elles coïncident avec les mises à jour de l'UI. Cela peut en effet entraîner des problèmes d'UI : le chargement se termine avant que les nouvelles données n'aient été ajoutées à l'interface utilisateur.

Pour cette raison, les accesseurs de commodité sont utiles pour afficher l'état de chargement dans un en-tête ou un pied de page. Pour d'autres cas d'utilisation, vous devrez peut-être accéder spécifiquement à l'état de chargement à partir de PagingSource ou RemoteMediator CombinedLoadStates fournit les propriétés source et mediator à cette fin. Ces propriétés exposent chacune un objet LoadStates contenant respectivement les objets LoadState pour PagingSource ou 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;
});

Opérateurs de chaîne sur LoadState

Étant donné que l'objet CombinedLoadStates fournit un accès à toutes les modifications de l'état de chargement, il est important de filtrer le flux de l'état de chargement en fonction d'événements spécifiques. Cela vous permet de mettre à jour votre UI au bon moment pour éviter le stuttering et les mises à jour inutiles.

Par exemple, supposons que vous souhaitiez afficher une vue vide, mais seulement une fois que le chargement initial des données est terminé. Ce cas d'utilisation nécessite de vérifier qu'une actualisation de données a commencé, puis d'attendre l'état NotLoading pour confirmer que l'actualisation est terminée. Vous devez exclure tous les signaux, sauf ceux dont vous avez besoin :

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

Dans cet exemple, le système attend que l'état du chargement de l'actualisation soit mis à jour, mais l'actualisation ne se déclenche que lorsque l'état est NotLoading. Cela garantit que l'actualisation à distance est entièrement terminée avant toute mise à jour de l'interface utilisateur.

Les API de flux permettent ce type d'opération. Votre appli peut spécifier les événements de chargement dont elle a besoin et gérer les nouvelles données lorsque les critères appropriés sont respectés.