Manage and present loading states
Stay organized with collections Save and categorize content based on your preferences.

The Paging library tracks the state of load requests for paged data and exposes it through the LoadState class. Your app can register a listener with the PagingDataAdapter to receive information about the current state and update the UI accordingly. These states are provided from the adapter because they are synchronous with the UI. This means that your listener receives updates when the page load has been applied to the UI.

A separate LoadState signal is provided for each LoadType and data source type (either PagingSource or RemoteMediator). The CombinedLoadStates object provided by the listener provides information about the loading state from all of these signals. You can use this detailed information to display the appropriate loading indicators to your users.

Loading states

The Paging library exposes the loading state for use in the UI through the LoadState object. LoadState objects take one of three forms depending on the current loading state:

  • If there is no active load operation and no error, then LoadState is a LoadState.NotLoading object. This subclass also includes the endOfPaginationReached property, which indicates whether the end of pagination has been reached.
  • If there is an active load operation, then LoadState is a LoadState.Loading object.
  • If there is an error, then LoadState is a LoadState.Error object.

There are two ways to use LoadState in your UI: using a listener, or using a special list adapter to present the loading state directly in the RecyclerView list.

Access the loading state with a listener

To get the loading state for general use in your UI, use the loadStateFlow stream or the addLoadStateListener() method provided by your PagingDataAdapter. These mechanisms provide access to a CombinedLoadStates object that includes information about the LoadState behavior for each load type.

In the following example, the PagingDataAdapter displays different UI components depending on the current state of the refresh load:

Kotlin

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

Java

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

Java

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

For more information on CombinedLoadStates, see Access additional loading state information.

Present the loading state with an adapter

The Paging library provides another list adapter called LoadStateAdapter for the purpose of presenting the loading state directly in the displayed list of paged data. This adapter provides access to the current load state of the list, which you can pass to a custom view holder that displays the information.

First, create a view holder class that keeps references to the loading and error views on your screen. Create a bind() function that accepts a LoadState as a parameter. This function should toggle the view visibility based on the load state parameter:

Kotlin

classLoadStateViewHolder(
parent:ViewGroup,
retry:()->Unit
):RecyclerView.ViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.load_state_item,parent,false)
){
privatevalbinding=LoadStateItemBinding.bind(itemView)
privatevalprogressBar:ProgressBar=binding.progressBar
privatevalerrorMsg:TextView=binding.errorMsg
privatevalretry:Button=binding.retryButton
.also{
it.setOnClickListener{retry()}
}
funbind(loadState:LoadState){
if(loadStateisLoadState.Error){
errorMsg.text=loadState.error.localizedMessage
}
progressBar.isVisible=loadStateisLoadState.Loading
retry.isVisible=loadStateisLoadState.Error
errorMsg.isVisible=loadStateisLoadState.Error
}
}

Java

class LoadStateViewHolderextendsRecyclerView.ViewHolder{
privateProgressBarmProgressBar;
privateTextViewmErrorMsg;
privateButtonmRetry;
LoadStateViewHolder(
@NonNullViewGroupparent,
@NonNullView.OnClickListenerretryCallback){
super(LayoutInflater.from(parent.getContext())
.inflate(R.layout.load_state_item,parent,false));
LoadStateItemBindingbinding=LoadStateItemBinding.bind(itemView);
mProgressBar=binding.progressBar;
mErrorMsg=binding.errorMsg;
mRetry=binding.retryButton;
}
publicvoidbind(LoadStateloadState){
if(loadStateinstanceofLoadState.Error){
LoadState.ErrorloadStateError=(LoadState.Error)loadState;
mErrorMsg.setText(loadStateError.getError().getLocalizedMessage());
}
mProgressBar.setVisibility(loadStateinstanceofLoadState.Loading
?View.VISIBLE:View.GONE);
mRetry.setVisibility(loadStateinstanceofLoadState.Error
?View.VISIBLE:View.GONE);
mErrorMsg.setVisibility(loadStateinstanceofLoadState.Error
?View.VISIBLE:View.GONE);
}
}

Java

class LoadStateViewHolderextendsRecyclerView.ViewHolder{
privateProgressBarmProgressBar;
privateTextViewmErrorMsg;
privateButtonmRetry;
LoadStateViewHolder(
@NonNullViewGroupparent,
@NonNullView.OnClickListenerretryCallback){
super(LayoutInflater.from(parent.getContext())
.inflate(R.layout.load_state_item,parent,false));
LoadStateItemBindingbinding=LoadStateItemBinding.bind(itemView);
mProgressBar=binding.progressBar;
mErrorMsg=binding.errorMsg;
mRetry=binding.retryButton;
}
publicvoidbind(LoadStateloadState){
if(loadStateinstanceofLoadState.Error){
LoadState.ErrorloadStateError=(LoadState.Error)loadState;
mErrorMsg.setText(loadStateError.getError().getLocalizedMessage());
}
mProgressBar.setVisibility(loadStateinstanceofLoadState.Loading
?View.VISIBLE:View.GONE);
mRetry.setVisibility(loadStateinstanceofLoadState.Error
?View.VISIBLE:View.GONE);
mErrorMsg.setVisibility(loadStateinstanceofLoadState.Error
?View.VISIBLE:View.GONE);
}
}

Next, create a class that implements LoadStateAdapter, and define the onCreateViewHolder() and onBindViewHolder() methods. These methods create an instance of your custom view holder and bind the associated load state.

Kotlin

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is LoadState.Error.
classExampleLoadStateAdapter(
privatevalretry:()->Unit
):LoadStateAdapter<LoadStateViewHolder>(){
overridefunonCreateViewHolder(
parent:ViewGroup,
loadState:LoadState
)=LoadStateViewHolder(parent,retry)
overridefunonBindViewHolder(
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 ExampleLoadStateAdapterextendsLoadStateAdapter<LoadStateViewHolder>{
privateView.OnClickListenermRetryCallback;
ExampleLoadStateAdapter(View.OnClickListenerretryCallback){
mRetryCallback=retryCallback;
}
@NotNull
@Override
publicLoadStateViewHolderonCreateViewHolder(@NotNullViewGroupparent,
@NotNullLoadStateloadState){
returnnewLoadStateViewHolder(parent,mRetryCallback);
}
@Override
publicvoidonBindViewHolder(@NotNullLoadStateViewHolderholder,
@NotNullLoadStateloadState){
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 ExampleLoadStateAdapterextendsLoadStateAdapter<LoadStateViewHolder>{
privateView.OnClickListenermRetryCallback;
ExampleLoadStateAdapter(View.OnClickListenerretryCallback){
mRetryCallback=retryCallback;
}
@NotNull
@Override
publicLoadStateViewHolderonCreateViewHolder(@NotNullViewGroupparent,
@NotNullLoadStateloadState){
returnnewLoadStateViewHolder(parent,mRetryCallback);
}
@Override
publicvoidonBindViewHolder(@NotNullLoadStateViewHolderholder,
@NotNullLoadStateloadState){
holder.bind(loadState);
}
}

To display the loading progress in a header and a footer, call the withLoadStateHeaderAndFooter() method from your PagingDataAdapter object:

Kotlin

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

Java

pagingAdapter
.withLoadStateHeaderAndFooter(
newExampleLoadStateAdapter(pagingAdapter::retry),
newExampleLoadStateAdapter(pagingAdapter::retry));

Java

pagingAdapter
.withLoadStateHeaderAndFooter(
newExampleLoadStateAdapter(pagingAdapter::retry),
newExampleLoadStateAdapter(pagingAdapter::retry));

You can instead call withLoadStateHeader() or withLoadStateFooter() if you want the RecyclerView list to display the loading state only in the header or only in the footer.

Access additional loading state information

The CombinedLoadStates object from PagingDataAdapter provides information on the load states for your PagingSource implementation and also for your RemoteMediator implementation, if one exists.

For convenience, you can use the refresh, append, and prepend properties from CombinedLoadStates to access a LoadState object for the appropriate load type. These properties generally defer to the load state from the RemoteMediator implementation if one exists; otherwise, they contain the appropriate load state from the PagingSource implementation. For more detailed information on the underlying logic, see the reference documentation for 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.
LoadStaterefreshLoadState=loadStates.refresh;
// Observe prepend load state from RemoteMediator if present, or
// from PagingSource otherwise.
LoadStateprependLoadState=loadStates.prepend;
// Observe append load state from RemoteMediator if present, or
// from PagingSource otherwise.
LoadStateappendLoadState=loadStates.append;
});

Java

pagingAdapter.addLoadStateListener(loadStates->{
// Observe refresh load state from RemoteMediator if present, or
// from PagingSource otherwise.
LoadStaterefreshLoadState=loadStates.refresh;
// Observe prepend load state from RemoteMediator if present, or
// from PagingSource otherwise.
LoadStateprependLoadState=loadStates.prepend;
// Observe append load state from RemoteMediator if present, or
// from PagingSource otherwise.
LoadStateappendLoadState=loadStates.append;
});

However, it is important to remember that only the PagingSource load states are guaranteed to be synchronous with UI updates. Because the refresh, append, and prepend properties can potentially take the load state from either PagingSource or RemoteMediator, they are not guaranteed to be synchronous with UI updates. This can cause UI issues where the loading appears to finish before any of the new data has been added to the UI.

For this reason, the convenience accessors work well for displaying the load state in a header or footer, but for other use cases you might need to specifically access the load state from either PagingSource or RemoteMediator. CombinedLoadStates provides the source and mediator properties for this purpose. These properties each expose a LoadStates object that contains the LoadState objects for PagingSource or RemoteMediator respectively:

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.
LoadStatemediatorRefreshLoadState=loadStates.mediator.refresh;
// Directly access the RemoteMediator append load state.
LoadStatemediatorAppendLoadState=loadStates.mediator.append;
// Directly access the RemoteMediator prepend load state.
LoadStatemediatorPrependLoadState=loadStates.mediator.prepend;
// Directly access the PagingSource refresh load state.
LoadStatesourceRefreshLoadState=loadStates.source.refresh;
// Directly access the PagingSource append load state.
LoadStatesourceAppendLoadState=loadStates.source.append;
// Directly access the PagingSource prepend load state.
LoadStatesourcePrependLoadState=loadStates.source.prepend;
});

Java

pagingAdapter.addLoadStateListener(loadStates->{
// Directly access the RemoteMediator refresh load state.
LoadStatemediatorRefreshLoadState=loadStates.mediator.refresh;
// Directly access the RemoteMediator append load state.
LoadStatemediatorAppendLoadState=loadStates.mediator.append;
// Directly access the RemoteMediator prepend load state.
LoadStatemediatorPrependLoadState=loadStates.mediator.prepend;
// Directly access the PagingSource refresh load state.
LoadStatesourceRefreshLoadState=loadStates.source.refresh;
// Directly access the PagingSource append load state.
LoadStatesourceAppendLoadState=loadStates.source.append;
// Directly access the PagingSource prepend load state.
LoadStatesourcePrependLoadState=loadStates.source.prepend;
});

Chain operators on LoadState

Because the CombinedLoadStates object provides access to all changes in load state, it is important to filter the load state stream based on specific events. This ensures that you update your UI at the appropriate time to avoid stutters and unnecessary UI updates.

For example, suppose that you want to display an empty view, but only after the initial data load completes. This use case requires that you verify that a data refresh load has started, then wait for the NotLoading state to confirm that the refresh has completed. You must filter out all signals except for the ones you need:

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.refreshisLoadState.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();
Disposabledisposable=
subject.distinctUntilChanged(CombinedLoadStates::getRefresh)
.filter(
combinedLoadStates->combinedLoadStates.getRefresh()instanceofLoadState.NotLoading)
.subscribe(combinedLoadStates->binding.list.scrollToPosition(0));
pagingAdapter.addLoadStateListener(loadStates->{
subject.onNext(loadStates);
});

Java

LiveData<CombinedLoadStates>liveData=newMutableLiveData<>();
LiveData<LoadState>refreshLiveData=
Transformations.map(liveData,CombinedLoadStates::getRefresh);
LiveData<LoadState>distinctLiveData=
Transformations.distinctUntilChanged(refreshLiveData);
distinctLiveData.observeForever(loadState->{
if(loadStateinstanceofLoadState.NotLoading){
binding.list.scrollToPosition(0);
}
});

This example waits until the refresh load state is updated, but only triggers when the state is NotLoading. This ensures that the remote refresh has fully finished before any UI updates happen.

Stream APIs make this type of operation possible. Your app can specify the load events it needs and handle the new data when the appropriate criteria are met.

Content and code samples on this page are subject to the licenses described in the Content License. Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.

Last updated 2025年02月10日 UTC.