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.

4.8 (15) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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:

  1. Creating an instance of RedditRepo. You’ll use it to fetch data from the Reddit API.
  2. Calling fetchPosts, which you created in RedditRepo in the previous section. You use the cachedIn call to cache the data in a scope. In this case, you’re using viewModelScope.

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 if areItemsTheSame returns true.

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):

List of Reddit Posts

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:

Logcat Key Reuse Error

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.
  • NotLoading: No loading of data happening, and no error.
  • Loading: Data is loading.
  • Error: Fetching data ends with an error.
  1. You set the click listener for btnRetry to invoke the retry action in the PagingSource
  2. You create a function named bindState that takes in LoadState as an argument.
    LoadState is a sealed class that can have any of the following states:
  3. 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.

Header Loading Indicator

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:

Footer Error Text Message and Retry Button