Infinite lists with Paging3 and Room (without network)

Infinite lists with Paging3 and Room (without network)

Android Paging Basics

This guide aims at implementing simple pagination with Room without network calls. I experienced a hard time finding the right material for reference when I was learning. This article presents a specific use for huge amounts of local data. Here is a Github link to the final solution.

Prerequisites

Ensure you have the basic knowledge of Room, ViewModel, CoroutineScopes, Android Activities and presenting list data with List Adapter.

What we will not be doing

We will not build an application from scratch, we will assume you have a functional application with the data layer set up without Paging.

Overview

1. Dependencies

    implementation 'androidx.paging:paging-runtime-ktx:3.1.1'
    def room_version = "2.4.3" // room
    implementation "androidx.room:room-ktx:$room_version"
    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    testImplementation "androidx.room:room-testing:$room_version"
    implementation "androidx.room:room-paging:$room_version"

    def activity_version = "1.6.1" // enables by viewModels delegate property
    implementation "androidx.activity:activity-ktx:$activity_version"
// Check the following link for the latest version 
// https://developer.android.com/jetpack/androidx/versions

2. Paging Source in the DAO

// RandomTextDao.kt
@Dao
interface RandomTextDao {
    @get:Query("SELECT * FROM random_text")
    val allRandomTexts: PagingSource<Int, RandomText>
}

The paging source fetches chunks of data from your source (the database) based on your query. In a more advanced setup with network calls, you would create a class that overrides the PagingSource class for fine-grained control over loading and refresh events.

3. Pager in the ViewModel

// MainActivityViewModel.kt
class MainActivityViewModel(private val randomTextDao: RandomTextDao): ViewModel() {
    val allRandomTexts = Pager(
        config = PagingConfig(
            pageSize = 50,
            enablePlaceholders = false,
            maxSize = 200
        )
    ) {
        randomTextDao.allRandomTexts
    } .flow
        .cachedIn(viewModelScope)
}

The Pager produces the PagingData stream. The PagingData is a collection of data that will persist in the ViewModel even through configuration changes; in this case, it is as a result of cachedIn(viewModelScope) method. With each data request, a new PagingData instance is created from the PagingSource instance (for each PagingData instance, there is one PagingSource instance). Also, for each data insert, update or delete from the database, a new PagingData and PagingSource is generated (a PagingSource instance is never updated, a new one is usually created).

4. PagingConfig in the ViewModel

The PagingConfig influences the behavior of the Pager. We will concentrate on the configuration options that we have used in this sample application.

  • PageSize : controls the number of items loaded at once from the PagingSource. There are trade-offs when choosing this value. It is best to consider the number of items that fit your intended screen, then choose a number that is twice or thrice your screen's real estate.

  • enablePlaceholders : this one ensures there are null placeholders loaded in place of data that is yet to be fetched from the PagingSource . The PagingSource has to be aware of the count of items that are yet to be loaded for this feature to be effective. (experiment with this configuration option by setting it to true then false while observing the vertical scrollbar provided by the RecyclerView ; ensure you have android:scrollbars="vertical" inside RecyclerView tag for this experiment)

  • maxSize : ensures that there is a threshold for the number of items in the PagingData at one instance of time, above which, older data is dropped to control/limit the memory to a certain maximum size.

Find other configuration options here.

5. The List Adapter

// Change this
class RandomTextListAdapter(private val context: Context):
    ListAdapter<RandomText, RandomTextListAdapter.RandomTextViewHolder>(
    RANDOM_TEXT_COMPARATOR
) {
/* your lines of code here... */
}
// to this
class RandomTextListAdapter(private val context: Context):
    PagingDataAdapter<RandomText, RandomTextListAdapter.RandomTextViewHolder>(
    RANDOM_TEXT_COMPARATOR
) {
/* your lines of code here... */
}

There's nothing much here. Simply replace ListAdapter with PagingDataAdapter

The PagingDataAdapter listens for PagingData loading events as pages are loaded.

6. collectLatest inside the MainActivity

        val randomTextAdapter = RandomTextListAdapter(this)
        randomTextAdapter.setOnSelectedListener(this)
        recyclerView = binding.recyclerView

        lifecycleScope.launch {
            viewModel.allRandomTexts.collectLatest {
                randomTextAdapter.submitData(it)
            }

        }
        recyclerView.adapter = randomTextAdapter

collectLatest captures the PagingData stream and provides a means to add an action. The action in this case is handing over the data to the PagingDataAdapter for display.

Summary

When you scroll through an infinite list, the RecyclerView emits loading hints. The loading hints offers a clue for the Pager to send more data. In turn, the data is loaded through the PagingSource which gets its data from the data source (database). Walking backwards, the Pager creates PagingData based on an instance of PagingSource. The PagingDataAdapter listens for loading events of the PagingData in order to display content in the User Interface.

NOTE: Ignore the Repository Pattern from the image below (courtesy of developer.android.com) as there was no need for it in this example. Here's the code for more context. Happy coding!