Black Friday Sale - Save on EverythingAll videos. All books. Now 50% off.

Build your mobile development skills and save! Stay ahead of the rest with an Ultimate book & video subscription. Starting at just $149/year for Black Friday.

Home Android & Kotlin Tutorials

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.

5/5 5 Ratings

Version

  • Kotlin 1.4, Android 5.0, Android Studio 4.0
Update note: Harun Wangereka updated this tutorial. Alex Sullivan wrote the original.

Lists are an essential part of mobile development. A list is a great option when you have a lot of information to display, and it contains items of similar type.

Some lists don’t have a fixed size. There’s often too much information to display all at once. For example, think about your Facebook feed. If Facebook loaded and displayed every post, image or Like every time you opened it, the list would never finish loading.

Instead, Facebook uses infinitely loading lists, as do most apps with large datasets. In an infinitely loading list, you download only as much data as the user will immediately consume. Then you load more data as the user scrolls farther in the list.

Implementing infinitely loading lists is an arduous process. To make it easier, Google provides a Paging library. It includes several core classes to help you create progressively loading lists. These classes abstract the logic that determines when to load new items. The Paging library also provides tie-ins to other core architecture components. These include the LiveData streaming library and the Room storage library.

In this tutorial, you’ll build a simple Reddit clone. It uses the Android Paging library to load information into an infinite list, page by page. During this process you’ll learn several important skills:

  • Implement the Paging 3.0 library.
  • Add headers and footers in infinitely loading lists.
  • Learn how to use Room with the Paging Library.

Now it’s time to get to work!

Getting Started

Start by downloading the materials for this tutorial using the Download Materials button at the top or bottom of this tutorial. Fire up Android Studio 4 or later, and import the starter project.

You’ll see that there’s a networking package with several model classes. It also contains a RedditService class that provides the Reddit API.The project also includes several empty classes that you’ll flesh out as you work through the tutorial.

Now, build and run. You’ll see an empty screen.

Paging Library Reddit Clone

To begin populating the screen, you’ll use the Reddit API which returns pages of Reddit posts.

Defining a PagingSource

Your first task is to create a PagingSource that will pull down Reddit information. A PagingSource is a class that manages the loading of data in small sets/pages for your list. In the data from the Reddit API, you receive a list of Reddit posts corresponding to a single page via the dist key, which is known as the a one-page key.

Head over to repositories/RedditPagingSource.kt:

class RedditPagingSource(private val redditService: RedditService) :
    // 1 & 2
    PagingSource<String, RedditPost>() {

    override suspend fun load(params: LoadParams<String>): 
        LoadResult<String, RedditPost> {
            TODO("not implemented")
    }
}

As you can see above, PagingSource has two parameters:

  1. String: Reddit API provides before and after keys, which tell the API how it is going to fetch the previous and next pages of data.
  2. RedditPost: The information received from the Reddit API i.e a list of Reddit posts of type RedditPost.

At the same time, notice that the load method is not yet implemented. It accepts LoadParams as an argument, which is a sealed Kotlin class that keeps information related to the load operation. It does so via its below class properties:

  • loadSize: Number of items you’re requesting.
  • placeholdersEnabled: Boolean value that indicates whether or not you’re using a placeholder.

In the next section, you’ll add the logic to fetch data from the Reddit API.

Fetching Reddit Posts From the Network

While still inside repositories/RedditPagingSource.kt, replace the TODO() with the below code:

return try {
    // 1 
    val response = redditService.fetchPosts(loadSize = params.loadSize)
    // 2
    val listing = response.body()?.data
    val redditPosts = listing?.children?.map { it.data }
    // 3
    LoadResult.Page(
        // 4
        redditPosts ?: listOf(),
        // 5
        listing?.before,
        listing?.after
    )
} catch (exception: IOException) { // 6
    return LoadResult.Error(exception)
} catch (exception: HttpException) {
    return LoadResult.Error(exception)
}

Here’s what is happening here:

  1. RedditService is used to fetch the list of posts from the Reddit API, passing loadSize as a parameter, via LoadParams.
  2. Get the list of posts from the body of the response.
  3. Create an instance of LoadResult.Page by providing all arguments.
  4. Pass in the list of reddit posts. If the case when API call didn’t return any RedditPosts, you pass an empty list.
  5. Pass in the before and after keys you received in the response body from the Reddit API.
  6. Finally, two catch blocks handle exceptions and return LoadResult.Error.

The code block above returns LoadResult, which is another sealed class that can take the following forms:

  • When the network call executes successfully, load() returns LoadResult.Page.
  • If an error occurred when executing the network call, load() returns LoadResult.Error.

When the IDE prompts you, make sure you add the imports to resolve the errors.

Next, you’ll use this RedditPagingSource to get list of posts.

Fetching Posts From the PagingSource

An advantage of the Paging 3.0 library is that its components conform to the Model-View-ViewModel (MVVM) architecture. RedditPagingSource acts as a data source, corresponding to the Model layer in the MVVM.

Navigate to repositories/RedditRepo.kt. Add the following code block to the class:

// 1
private val redditService = RedditClient.getClient().create(RedditService::class.java)

// 2
fun fetchPosts(): Flow<PagingData<RedditPost>> {
    // 3
    return Pager(
        PagingConfig(pageSize = 40, enablePlaceholders = false)
    ) {
        RedditPagingSource(redditService)
    }.flow
}

Here,

  1. You create a reference to RedditService to download list of posts from the Reddit API
  2. You return an instance of Pager class, which is used to fetch a stream of data from PagingSource. The Paging library supports several return types. These include Flow, LiveData, and the RxJava Flowable and Observable. In this tutorial, you’ll use Flow from Kotlin coroutines.
  3. Pager takes two parameters:
    • PagingConfig, defines how PagingSource should load the data. pageSize specifies how many items to load in a single page. enablePlaceholders indicates whether you want to display placeholders when loading the data. Here you set it to false.
    • The second parameter is a trailing lambda that returns a PagingSource. In this case, you use RedditPagingSource with the redditService you created earlier.
Note: If you’re not familiar with Flow, take a look at this Kotlin Flow for Android: Getting Started tutorial.

Finally, when the IDE prompts you, make sure you add the imports to resolve the errors. In case of importing Flow, choose the one from Kotlin Coroutines.

In the next section, you’ll make use of fetchPosts() from the ViewModel to trigger the API call for fetching list of posts from Reddit API.

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.
  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:
    • NotLoading: No loading of data happening, and no error.
    • Loading: Data is loading.
    • Error: Fetching data ends with an error.
  3. The value of LoadState is used to toggle visibility of views.

Next, replace the TODO() inside onBindViewHolder() with the below 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 below 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

Breaking Free of Network Dependency

At this point, the app is firing on all cylinders. But there’s one problem — it’s dependent on the network to run. If you have no internet access, the app is doomed!

Next up, you are going to use the Room database library for persisting/caching the list so it is functional even when offline.

Navigate to database/dao package and create a new interface name RedditPostsDao.kt with the following code:

import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy.REPLACE
import androidx.room.Query
import com.raywenderlich.android.redditclone.models.RedditPost

@Dao
interface RedditPostsDao {

    @Insert(onConflict = REPLACE)
    suspend fun savePosts(redditPosts: List<RedditPost>)

    @Query("SELECT * FROM redditPosts")
    fun getPosts(): PagingSource<Int, RedditPost>
}

For the most part, this is a standard Dao object in Room. But there’s one unique thing here: The return type for getPosts is a PagingSource. That’s right, Room can actually create the PagingSource for you! Room will generate one that uses an Int key to pull RedditPost objects from the database as you scroll.

Now, navigate to database/RedditDatabase.kt. Add the following code to the class:

abstract fun redditPostsDao(): RedditPostsDao

Here, you’re adding an abstract method to return RedditPostsDao.

At this point, you have methods for inserting and fetching posts. Now it’s time to add the logic to fetch posts from Reddit API and insert them into your Room database.

Creating a RemoteMediator

A RemoteMediator is like a PagingSource class. But RemoteMediator does not display data to a RecyclerView. Instead, it uses the database as a single source of truth. You fetch data from the network and save it to the database.

Navigate to repositories/RedditRemoteMediator.kt. You’ll see the following code:

@OptIn(ExperimentalPagingApi::class)
class RedditRemoteMediator(
    private val redditService: RedditService,
    private val redditDatabase: RedditDatabase
) : RemoteMediator<Int, RedditPost>() {
    override suspend fun load(
        // 1
        loadType: LoadType,
        // 2
        state: PagingState<Int, RedditPost>
    ): MediatorResult {
        TODO("not implemented")
    }
}

This class extends RemoteMediator and overrides the load(). You’re going to add the logic to fetch data from the Reddit API and save it to a database. The arguments for this method are different from those in PagingSource:

  1. LoadType, is an enum that represents the loading type. It can have any of these values:
    • REFRESH indicates it’s a new fetch.
    • PREPEND indicates that content is being added at the start of the PagingData.
    • APPEND indicates that content is being added at the end of the PagingData.
  2. PagingState. This takes a key-value pair, where the key has type Int and the value has type RedditPost.

Replace the TODO() with below code:

return try {
    // 1
    val response = redditService.fetchPosts(loadSize = state.config.pageSize)

    // 2
    val listing = response.body()?.data
    val redditPosts = listing?.children?.map { it.data }

    // 3
    if (redditPosts != null) {
        redditDatabase.redditPostsDao().savePosts(redditPosts)
    }

    // 4
    MediatorResult.Success(endOfPaginationReached = listing?.after == null)

    // 5
} catch (exception: IOException) {
    MediatorResult.Error(exception)
} catch (exception: HttpException) {
    MediatorResult.Error(exception)
}

Here is what is happening in above code block:

  1. This is a call to the Reddit API where you use the pageSize from the state parameter to fetch the data from the network.
  2. Get the list of posts from the response body
  3. If there are posts returned from the API, you save the list in the database.
  4. If the network response is successful, you set the return type of the method to MediatorResult.Success. You also pass endOfPaginationReached, a Boolean variable that indicates when you are at the end of the list. In this case, the list ends when after is null. Notice that RemoteMediator uses the sealed class MediatorResult to represent the state of the data fetch.
  5. Finally, you handle any exceptions that may occur during the loading operation and pass them to the Paging library.

Notice that RemoteMediator uses the sealed class MediatorResult to represent the state of the data fetch.

Next, you’ll modify your repository class to use RedditRemoteMediator in order to start using Room for persisting the list of posts.

Adding RemoteMediator to Your Repository

Navigate to repositories/RedditRepo.kt. Add the following code to the class body:

private val redditDatabase = RedditDatabase.create(context)

This creates an instance of the Room database using the create(). You’ll use it with RemoteMediator.

Next, replace the complete fetchPosts() with the following code:

@OptIn(ExperimentalPagingApi::class)
fun fetchPosts(): Flow<PagingData<RedditPost>> {
    return Pager(
        PagingConfig(
              pageSize = 40, 
              enablePlaceholders = false, 
              // 1
              prefetchDistance = 3),

        // 2
        remoteMediator = RedditRemoteMediator(redditService, redditDatabase),

        // 3
        pagingSourceFactory = { redditDatabase.redditPostsDao().getPosts() }
    ).flow
}

Here’s a breakdown of code above:

  1. As part of your paging configuration, you add prefetchDistance to PagingConfig. This parameter defines when to trigger the load of the next items within the loaded list.
  2. You set RedditRemoteMediator, which you created earlier. RedditRemoteMediator fetches the data from the network and saves it to the database.
  3. Finally, you set pagingSourceFactory, in which you call the Dao to get your posts. Now your database serves as a single source of truth for the posts you display, whether or not you have a network connection.

You don’t have to modify the ViewModel or the activity layer, since nothing has changed there! That’s the benefit of choosing a good architecture. You can swap the data source implementations without modifying other layers in your app.

Note: RemoteMediator API is currently experimental and needs to be marked as OptIn via the @OptIn(ExperimentalPagingApi::class) annotation in the classes using it.

Now, build and run. You'll see the following screen:

Paging With Room Database

As you scroll, you'll notice that your app only fetches one page. Why went wrong? Well, no worries! In the next section, you're going to fix it.

Fetching Previous and Next Pages With RemoteMediator

In PagingSource, you passed the before and after keys to LoadResult. But you're not doing this in RedditRemoteMediator. That's why the app currently fetches only one page.

To scroll continuously, you have to tell Room how to fetch the next and previous pages from the network when it reaches to the start or end of the current page. But how do you do this, when you're not passing the keys to MediatorResult?

To achieve this, you're going to save the keys in the Room database after every network fetch. Navigate to database/dao and create a new interface named RedditKeysDao.kt with below code:

@Dao
interface RedditKeysDao {

    @Insert(onConflict = REPLACE)
    suspend fun saveRedditKeys(redditKey: RedditKeys)

    @Query("SELECT * FROM redditKeys ORDER BY id DESC")
    suspend fun getRedditKeys(): List<RedditKeys>

}

This is a standard Dao object in Room with two methods for saving and retrieving the keys.

Next, go back to database/RedditDatabase.kt. In your RedditDatabase class, append the below line of code:

abstract fun redditKeysDao(): RedditKeysDao

redditKeysDao() is used to get access to the RedditKeysDao so that you can access the keys in the database easily.

Now navigate to repositories/RedditRemoteMediator.kt. Replace the body of load() with the following code:

 return try {
     // 1
     val loadKey = when (loadType) {
         LoadType.REFRESH -> null
         LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
         LoadType.APPEND -> {
             state.lastItemOrNull()
                 ?: return MediatorResult.Success(endOfPaginationReached = true)
             getRedditKeys()
         }
     }

     // 2
     val response = redditService.fetchPosts(
         loadSize = state.config.pageSize,
         after = loadKey?.after,
         before = loadKey?.before
     )
     val listing = response.body()?.data
     val redditPosts = listing?.children?.map { it.data }
     if (redditPosts != null) {
         // 3
         redditDatabase.withTransaction {
             redditDatabase.redditKeysDao()
                 .saveRedditKeys(RedditKeys(0, listing.after, listing.before))
             redditDatabase.redditPostsDao().savePosts(redditPosts)
         }
     }
     MediatorResult.Success(endOfPaginationReached = listing?.after == null)
 } catch (exception: IOException) {
     MediatorResult.Error(exception)
 } catch (exception: HttpException) {
     MediatorResult.Error(exception)
 }

Here is what is happening in above code block:

  1. You fetch the Reddit keys from the database when the LoadType is APPEND.
  2. You set the after and before keys in fetchPosts.
  3. Finally, you create a database transaction to save the keys and the posts you retrieved is there response returned a lits of posts

Next, add a new getRedditKeys() to the RedditRemoteMediator.kt class body:

private suspend fun getRedditKeys(): RedditKeys? {
    return redditDatabase.redditKeysDao().getRedditKeys().firstOrNull()
}

This is a suspend method that fetches RedditKeys from the database. Notice that you're using firstOrNull(). This will return the first items in the list. If there are no items in the database, it returns null.

Build and run, and voila! Your endless scrolling list is back.

Paging Infinite List with Room

You now have an endlessly scrolling app that works whether or not you have a network connection!

Paging 3.0 Remarks

Paging 3.0 is a complete re-write of the library. It uses Kotlin, and it has first-class support for Kotlin coroutines and flow. It also has some new features. For example, it supports headers and footers, and it has retry and refresh mechanisms.

Some of the classes have changed names, and some functionality has changed as well. Here are a few examples:

  • PagedListAdapter has been renamed to PagingDataAdapter. The implementation is still the same.
  • DataSource has changed to PagingSource. ItemKeyedDataSource, PageKeyedDataSource and PositionalDataSource have been merged to one PagingSource class. And you no longer have to override loadInitial, loadAfter and loadBefore. Instead, you have one load method, which is a suspend function. This means you don't need to use callbacks as before.
  • LivePagedListBuilder has changed to Pager. DataSource.Factory is no longer required when creating a Pager instance.
  • PagedList.Config has been renamed to PagingConfig, but its implementation remains the same.

This is a highlight of the major changes. You'll find a complete breakdown of changes, as well as information about backward compatibility, in the Paging 3 migration guide, Android Developers | Migrate to Paging 3.

Where to Go From Here?

Congratulations! You've learned how to use the Paging 3.0 library to manage infinitely scrolling lists. You've added headers and footers to your UI to make the list more accessible to the user. You've implemented caching to make your app usable when the network isn't available.

You can download the finished project by clicking the Download Materials button at the top or bottom of the tutorial.

In this tutorial, you've only scratched the surface of the Android Architecture Components and the Paging library. For more information, check out the official documentation at Android Developers | Library Overview.

We hope you enjoyed the tutorial. If you have any questions or comments, please join the forum discussion below.

Average Rating

5/5

Add a rating for this content

5 ratings

More like this

Contributors

Comments