Firestore Pagination with Paging 3

Firestore Pagination with Paging 3

Firestore Pagination with Paging 3

Introduction

The Paging library helps you load and display pages of data from a larger dataset from local storage or over network. This approach allows your app to use both network bandwidth and system resources more efficiently. The components of the Paging library are designed to fit into the recommended Android app architecture, integrate cleanly with other Jetpack components, and provide first-class Kotlin support.

In this post we will see how to gradually load data from Firebase Firestore using the Paging 3 library.

Image for post

Figure 1

Why not Firebase UI ?

There is already a library to paginate the data from Firebase Firestore, in my opinion this library can be used for simple cases and personally I had difficulty fitting the Firebase UI code with my existing architecture and the advantage of paging library is that the paging library integrates directly into the recommended Android app architecture.

You can learn more about Firebase UI here.

Setup

Before using the Paging 3 library in your project, you need to add some dependencies in your Gradle configuration file.

build.gradle

Library Components

The Paging 3 library comes with a certain amount of element intervening in each part of your architecture as shown in the following figure

Image for post

Figure 2

1. Paging Source

The primary Paging library component in the repository layer is PagingSource. Each PagingSource object defines a source of data and how to retrieve data from that source. A PagingSource object can load data from any single source, including network sources and local databases.

2. RemoteMediator

A RemoteMediator object handles paging from a layered data source, such as a network data source with a local database cache.

3. Pager

The Pager component provides a public API for constructing instances of PagingData that are exposed in reactive streams, based on a PagingSource object and a PagingConfig configuration object.

4. Paging Data

The component that connects the ViewModel layer to the UI is PagingData. A PagingData object is a container for a snapshot of paginated data. It queries a PagingSource object and stores the result.

5. PagingDataAdapter

The primary Paging library component in the UI layer is PagingDataAdapter, a RecyclerView adapter that handles paginated data.

Paging 3 in Practice

To illustrate this, we will try to develop the application shown in figure 1, which retrieves data from Firestore before displaying them in a list.

Step 1: Model

If you have noticed figure 1, you will notice that the application only displays a list of countries and their codes, and a country is represented by the following model

data class Country (
    val identifier: String = "",
    val lastUpdate: Date? = null,
    val name: String = "",
    val uid: String = ""
)

Note : All properties have a default value so you can create an instance without having to pass anything to the constructor. All models that we need to deserialize from Firebase Firestore must have a default constructor.

Step 2 : Define a PagingSource

A PagingSource is a class that inherits from the generic PagingSource class and has two type parameters, the first one in our case QuerySnapshot represents the type of the page key, and the second one represents the data to retrieve in our case Country

And finally we have to redefine the load method that returns a LoadingResult object.

class FirestorePagingSource(
    private val db: FirebaseFirestore
) : PagingSource<QuerySnapshot, Country>() {

    override suspend fun load(params: LoadParams<QuerySnapshot>): LoadResult<QuerySnapshot, Country> {
        return try {
            // Step 1
            val currentPage = params.key ?: db.collection("countries")
                .limit(10)
                .get()
                .await()

            // Step 2
            val lastDocumentSnapshot = currentPage.documents[currentPage.size() - 1]

            // Step 3
            val nextPage = db.collection("countries").limit(10).startAfter(lastDocumentSnapshot)
                .get()
                .await()

            // Step 4
            LoadResult.Page(
                data = currentPage.toObjects(Country::class.java),
                prevKey = null,
                nextKey = nextPage
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
}
  • Line 8 : At the first call of the load method, it is necessary to create a key as the params.key line will return null, and in our case the key corresponds to a QuerySnapshot object pointing to the first 10 elements of the countries collection in Firestore.
  • Line 14 : After having obtained the reference of the first 10 elements of the collection, we recover the reference of the penultimate element of these 10 elements.
  • Line 17 : Once we have the reference of the penultimate element of the current page, we use this reference to have the key of the next portion of data to load, in other words the reference of the next 10 elements coming after the document retrieved at line 14 in the collection.
  • Line 22 : Finally we have two objects of type QuerySnapshot, the first one created at line 8 which contains the data of the current page, and the second one created at line 17 which contains the data of the next page, so we have everything we need to return a result represented by an object of type LoadResult.Page to which we pass the data (List<Country>) of the current page, the reference to the previous page in our case null, and the reference to the next page.

Step 3 : PagingDataAdapter

After setting up our PagingSource that will retrieve its data from the Firestore, we have to implement a PagingDataAdapterthat is not different from the other adapters we are used to using with the RecyclerView.

class CountryAdapter : PagingDataAdapter<Country, CountryViewHolder>(Companion) {
    companion object : DiffUtil.ItemCallback<Country>() {
        override fun areItemsTheSame(oldItem: Country, newItem: Country): Boolean {
            return oldItem.uid == newItem.uid
        }

        override fun areContentsTheSame(oldItem: Country, newItem: Country): Boolean {
            return oldItem == newItem
        }
    }

    override fun onBindViewHolder(holder: CountryViewHolder, position: Int) {
        val binding = holder.binding as ItemCountryBinding
        binding.country = getItem(position) ?: return
        binding.executePendingBindings()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CountryViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding = ItemCountryBinding.inflate(layoutInflater, parent, false)
        return CountryViewHolder(binding)
    }
}

class CountryViewHolder(
    val binding: ViewDataBinding
) : RecyclerView.ViewHolder(binding.root)

CountryAdapter.kt

Step 4 : In The ViewModel

In the ViewModel we retrieve the information returned by the PagingSource in the form of a flow that we will collect in the activity or fragment.

Step 5 : Display data

In the MainActivity we instantiate the previously created adapter that we link to the RecyclerView and collect the data from the ViewModel.

ThePagingDataAdapter. give us the possibility to observe the loading state as you can see in the line 20, in our case I have used two ProgressBar the first one for the initial loading and the second when the user need more data.

Conclusion

The Paging 3 library is very powerful and can manage several sources of data to easily meet your needs, consult the official documentation to get an idea of the extent of the functionalities offered by the Paging 3 library.

You can get the source code of the application on the following link

Reference

Did you find this article valuable?

Support Eric Ampire by becoming a sponsor. Any amount is appreciated!