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 objetLoadState.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 objetLoadState.Loading
. - En cas d'erreur,
LoadState
est un objetLoadState.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 :
// 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
}
}
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);
});
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 :
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
}
}
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);
}
}
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é.
// 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)
}
// 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);
}
}
// 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);
}
}
Afficher l'état de chargement sous la forme d'un en-tête ou d'un pied de page
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
:
pagingAdapter
.withLoadStateHeaderAndFooter(
header = ExampleLoadStateAdapter(adapter::retry),
footer = ExampleLoadStateAdapter(adapter::retry)
)
pagingAdapter
.withLoadStateHeaderAndFooter(
new ExampleLoadStateAdapter(pagingAdapter::retry),
new ExampleLoadStateAdapter(pagingAdapter::retry));
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
.
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
}
}
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;
});
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
:
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
}
}
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;
});
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 :
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) }
}
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);
});
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.
Aucune recommandation pour l'instant.
Connectez-vous à votre compte Google.