Paging Library for Android With Kotlin: Creating Infinite Lists
In this tutorial, you’ll build a simple Reddit clone that loads pages of information gradually into an infinite list using Paging 3.0 and Room. By Harun Wangereka.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Paging Library for Android With Kotlin: Creating Infinite Lists
30 mins
- Getting Started
- Defining a PagingSource
- Fetching Reddit Posts From the Network
- Fetching Posts From the PagingSource
- Configuring Your ViewModel to Fetch Data
- Using a PagingData Adapter
- Displaying Data in Your UI
- Enabling Key Reuse
- Displaying the Loading State
- Breaking Free of Network Dependency
- Creating a RemoteMediator
- Adding RemoteMediator to Your Repository
- Fetching Previous and Next Pages With RemoteMediator
- Paging 3.0 Remarks
- Where to Go From Here?
Configuring Your ViewModel to Fetch Data
Navigate to the ui/RedditViewModel.kt. Add the following to class:
// 1
private val redditRepo = RedditRepo(application)
// 2
fun fetchPosts(): Flow<PagingData<RedditPost>> {
return redditRepo.fetchPosts().cachedIn(viewModelScope)
}
Here you’re:
- Creating an instance of
RedditRepo
. You’ll use it to fetch data from the Reddit API. - Calling
fetchPosts
, which you created inRedditRepo
in the previous section. You use thecachedIn
call to cache the data in a scope. In this case, you’re usingviewModelScope
.
Great! Now your ViewModel is ready to provide data to the view. :]
In the next sections, you’re going to add the logic to display this data to the user.
Using a PagingData Adapter
Open ui/RedditAdapter.kt. You’ll see that RedditAdapter
extends PagingDataAdapter
. The first type parameter is RedditPost
. That’s the model this adapter uses, which is the same RedditPagingSource
class you created earlier produces. The second type parameter is RedditViewHolder
, as in RecyclerView.Adapter
.
In order to handle logic around updating the list, you’ll need make use of DiffUtil
class. DiffUtil
is a utility class which streamlines the process of sending a new list to RecyclerView.Adapter
. It has callbacks that communicate with the adapter’s notifyItemChanged
and notifyItemInserted
methods to update its items efficiently. Great news — this means you don’t have to deal with that complex logic!
A util class named DiffUtilCallBack
is already created that extends from DiffUtil.ItemCallback
and it overrides the below two methods:
- areItemsTheSame Check whether two items represent the same, equal object. This is usually determined by comparing the unique IDs of the two items.
-
areContentsTheSame Check whether the content of the item are the same. You call
areContentsTheSame
only ifareItemsTheSame
returnstrue
.
If the adapter determines that the content has changed for the item at position in the list, then it re-renders the item.
Next, while inside the ui/RedditAdapter.kt, replace the TODO()
inside onBindViewHolder()
with below:
getItem(position)?.let { redditPost ->
holder.bindPost(redditPost)
}
Here notice that you are using the getItem
method provided by PagingDataAdapter
to get RedditPost
. After getting the item, you call bindPost(redditPost)
with the post at that position. This method handles displaying the data for a post in a single RecyclerView
item.
Now your RedditAdapter
is ready! Next, you’ll use this adapter to display the Reddit posts in the UI.
Displaying Data in Your UI
Navigate to ui/RedditPostsActivity.kt. Add the following code to class:
private val redditViewModel: RedditViewModel by lazy {
ViewModelProvider(this).get(RedditViewModel::class.java)
}
Here you’re initializing RedditViewModel
using lazy
keyword, which means the initialization will occur only after the first call. Then consecutive calls will return the same instance of the ViewModel.
Next, add this method at the bottom of RedditPostsActivity
, below the setupViews
method:
private fun fetchPosts() {
lifecycleScope.launch {
redditViewModel.fetchPosts().collectLatest { pagingData ->
redditAdapter.submitData(pagingData)
}
}
}
This code fetches the posts from RedditViewModel
. Since the ViewModel returns a Flow
, you use collectLatest
to access the stream. Once you have the results, you send the list to the adapter by calling submitData
.
In order to wire everything up, you need to head over to the onCreate()
replace //TODO: Replace with fetchPosts()
with below:
fetchPosts()
All done! Now, build and run. You’ll see a screen that looks like below (Of course, your content will be different):
Woohoo! You have your list! Try scrolling down, and you’ll see that new content loads as you approach the bottom of the list.
Enabling Key Reuse
If you continue scrolling, you’ll notice that the app crashes. Take a look at your logcat:
Reddit API reuses the keys in some instances to fetch the posts. Unfortunately, PagingSource
does not support this behavior.
To solve this issue, navigate to RedditPagingSource.kt. Add the following code to the class:
override val keyReuseSupported: Boolean = true
keyReuseSupported
defaults to false
. Here you’re overriding the default setting to true
. This enables PagingSource
to reuse keys in fetching the posts.
Build and run. Now everything works as it should. :]
Wow, that took quite a few steps, but now you’re able to fetch an infinite list of items from the network!
Next, you’ll add a loading header and footer to show the user the status of the load.
Displaying the Loading State
In this section, you’re going to add a ProgressBar
while you’re fetching new items after you reach the end of a page. You’ll also display an error message to the user in case it fails.
First, open ui/RedditLoadingAdapter.kt. Note how RedditLoadingAdapter
extends LoadStateAdapter
. LoadStateAdapter
is a special list adapter that has the loading state of the PagingSource
. You can use it with a RecyclerView
to present the loading state on the screen.
Replace the //TODO: not implemented
inside LoadingStateViewHolder
with below:
// 1
private val tvErrorMessage: TextView = itemView.tvErrorMessage
private val progressBar: ProgressBar = itemView.progress_bar
private val btnRetry: Button = itemView.btnRetry
// 2
init {
btnRetry.setOnClickListener {
retry()
}
}
// 3
fun bindState(loadState: LoadState) {
if (loadState is LoadState.Error) {
tvErrorMessage.text = loadState.error.localizedMessage
}
// 4
progressBar.isVisible = loadState is LoadState.Loading
tvErrorMessage.isVisible = loadState !is LoadState.Loading
btnRetry.isVisible = loadState !is LoadState.Loading
}
There are a couple of things to explain here:
-
- You wire up the view as local properties:
- tvErrorMessage will display an error message.
- progressBar will display the loading state.
- btnRetry will retry the network call if it fails.
- You set the click listener for
btnRetry
to invoke the retry action in thePagingSource
- You create a function named
bindState
that takes inLoadState
as an argument.
LoadState
is a sealed class that can have any of the following states:- NotLoading: No loading of data happening, and no error.
- Loading: Data is loading.
- Error: Fetching data ends with an error.
-
The value of
LoadState
is used to toggle visibility of views.
Next, replace the TODO()
inside onBindViewHolder()
with the following code:
holder.bindState(loadState)
Here you’re calling bindState()
with the state that onBindViewHolder()
method provides.
Next, you’ll wire up the RedditLoadingAdapter
to your RecyclerView
so as to start handling the loading state.
Navigate to ui/RedditPostsActivity.kt. Append the following code inside the setupViews()
:
rvPosts.adapter = redditAdapter.withLoadStateHeaderAndFooter(
header = RedditLoadingAdapter { redditAdapter.retry() },
footer = RedditLoadingAdapter { redditAdapter.retry() }
)
Here, you add another adapter to your RecyclerView
. You use withLoadStateHeaderAndFooter
, which takes two parameters: header
and footer
. For both, you use the RedditLoadingAdapter
you created earlier. Notice how redditAdapter.retry()
is used to retry network calls.
Build and run, and you’ll see the ProgressBar
when loading new pages.
To display the error text in the footer, set your phone to airplane mode and try scrolling to the end of the list. Now you’ll see an error message: