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.

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

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 typeLoadResult.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 PagingDataAdapter
that 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