Handling Loading states of Android Paging 3
A guide to manage and present load states of paging library
Previously I wrote on “Paging 3 on Android” and “Testing Android PagingSource”. If you have missed those, please check it out first.
In this article, I will write about the loading states of the paging library.
Paging library tracks data request states. LoadState
class represents the states of a request.
Loading states
LoadState
objects can have three forms:
LoadState.NotLoading
indicates no active load operation and no error.LoadState.Loading
indicates there is an active load operation ongoingLoadState.Error
indicates there is an error in the data request
There are two ways you can use LoadState
to show the state in UI.
- Registering a listener with the
PagingDataAdapter
. In this approach, you have to present a state outside of theRecyclerView
- Using
LoadStateAdapter
to present the state directly in theRecyclerView
Before using LoadState
you need to know about LoadType
and CombinedLoadStates
.
LoadType
is an Enum
and it has 3 type
- APPEND : used to load at the end of a
PagingData
- PREPEND : used load at the start of a
PagingData
- REFRESH : used to refresh or initial load of a
PagingData
CombinedLoadStates
is a collection of pagination LoadState
s for both a PagingSource
, and RemoteMediator
. It contains LoadState
for LoadType
refresh, prepend, append, and data source type (PagingSource
, RemoteMediator
).
For better understanding check the official doc
Let’s check how can we show the loading state.
Loading state with a listener
To get the loading state for general use in your UI, you can use the loadStateFlow
stream or the addLoadStateListener()
method provided by your PagingDataAdapter
. loadStateFlow.collectLatest
will give you CombinedLoadStates
so you can easily update your UI
Below is the code for updating UI with a listener
lifecycleScope.launch {
newsAdapter.loadStateFlow.collectLatest {
binding.progressLayout.isVisible = (it.refresh is LoadState.Loading) || (it.append is LoadState.Loading)
binding.layoutError.isVisible = (it.refresh is LoadState.Error) || (it.append is LoadState.Error)
}
}
Here to show progress I checked both refresh
and append
LoadState
is Loading
because progress may need to show(actually depends on your requirement) for both refresh
, append
load type.
Similarly, to show an error, I checked LoadState
is Error
So it’s easy and simple
Loading state with LoadStateAdapter
The purpose of LoadStateAdapter
is to present the loading state directly in RecyclerView
. So how does this work? Let's see.
LoadStateAdapter
provides access to the current load state of paged data, which you can pass to a custom view holder that displays the information.
First, create a view holder that is responsible for showing loading and error views in UI.
Second, create a class that implements LoadStateAdapter
. In onBindViewHolder()
you will get LoadState
which you can pass to the method of view holder that will display information in UI.
Let’s see the implementation
class LoadStateViewHolder(
private val binding: LayoutLoadStateBinding,
val retry: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(loadState : LoadState) {
binding.apply {
layoutError.isVisible = loadState is LoadState.Error
progressBar.isVisible = loadState is LoadState.Loading
}
}
}
Here parameter retry
can be used to retry failed load requests. bind
will update UI.
Display the loading state as a header or footer
To display the loading progress in a header and a footer, you can use withLoadStateHeaderAndFooter()
method from your PagingDataAdapter
object
val headerStateAdapter = NewsLoadStateAdapter(newsAdapter::retry)
val footerStateAdapter = NewsLoadStateAdapter(newsAdapter::retry)
newsAdapter.withLoadStateHeaderAndFooter(
header = headerStateAdapter,
footer = footerStateAdapter
)
If you need to display the loading state with only the header then use withLoadStateHeader()
and only the footer use withLoadStateFooter()
. Now you can see loading states as header and footer. If you want to show progress or error for the initial data load then this approach will not work. Let's find out why it will not work for the initial data load.
Let’s check the implementation. Below is the implementation of the method withLoadStateHeaderAndFooter()
fun withLoadStateHeaderAndFooter(
header: LoadStateAdapter<*>,
footer: LoadStateAdapter<*>
): ConcatAdapter {
addLoadStateListener { loadStates ->
header.loadState = loadStates.prepend
footer.loadState = loadStates.append
}
return ConcatAdapter(header, this, footer)
}
Here a load state listener is added, prepend LoadState
is assigned to the header LoadState
and append LoadState
is assigned to the footer LoadState
. So the header will work for only prepend LoadState
and the footer for only append LoadState
. But to show initial progress we need refresh
LoadState
. Hope now you understand why it will not work for the initial loading state.
Display the initial loading state
I am sure now you know the solution of showing the initial load state in UI.
The solution is to use refresh
LoadState
instead of prepend
LoadState
.
Check the code below.
newsAdapter.addLoadStateListener { loadStates ->
headerStateAdapter.loadState = loadStates.refresh
footerStateAdapter.loadState = loadStates.append
}
val concatAdapter = ConcatAdapter(headerStateAdapter, newsAdapter, footerStateAdapter)
binding.recyclerView.adapter = concatAdapter
Thanks for reading. I hope if you are new to the Android paging library this article will help you to handle loading states.
Happy coding. :)