When working with Cloud Firestore, pagination is particularly useful for handling large data sets. Firestore collections can contain thousands of documents, and retrieving all of them at once can lead to significant performance issues and excessive bandwidth consumption.
And since Firestore operates on a pay-as-you-go pricing model, we have to pay for every document read above the free quota (charged at $0.06 per 100,000 documents).
Furthermore, as we can only view a limited number of items at once (especially on mobile devices), it is sensible to implement pagination and fetch only the data we need.
But how can we enable pagination in our Flutter app when loading data from Cloud Firestore?
A quick search on the official docs reveals that we can paginate data with query cursors.
However, by following this approach, we still need to write all the pagination code by hand, including:
- fetching the first page
- managing the start and end cursors
- retrieving each subsequent page when the user scrolls down
Wouldn't it be convenient to have an easy-to-use, drop-in widget that handles pagination for us?
Luckily, the Firebase UI for Firestore package offers a FirestoreListView
widget that does exactly that.
And in this article, we're going to learn how to use it.
Pagination with FirestoreListView: Overview
The documentation of the Firebase UI for Firestore package already contains a simple example we can use to get started:
final usersQuery = FirebaseFirestore.instance.collection('users').orderBy('name');
FirestoreListView<Map<String, dynamic>>(
query: usersQuery,
itemBuilder: (context, snapshot) {
Map<String, dynamic> user = snapshot.data();
return Text('User name is ${user['name']}');
},
);
Since FirestoreListView
is built upon Flutter's own ListView
widget, it accepts the same arguments.
By default, it retrieves 10 items at once, but we can modify this using the pageSize
parameter:
FirestoreListView<Map<String, dynamic>>(
pageSize: 20,
// ...
);
This is all easy enough.
However, for a production-ready app, there are more things to consider:
- Can our widgets use type-safe model classes rather than
Map<String, dynamic>
? - How to deal with empty, loading, and error states?
To answer these questions, let's examine some sample code from my Time Tracker app on GitHub and see how everything fits together. 👇
Using FirestoreListView with type-safe model classes
To display a list of jobs with name
and ratePerHour
properties, we can create a Job
model class, complete with fromMap
and toMap
methods for data serialization:
class Job {
const Job({required this.name, required this.ratePerHour});
final String name;
final int ratePerHour;
factory Job.fromMap(Map<String, dynamic> data) {
return Job(
name: data['name'] as String,
ratePerHour: data['ratePerHour'] as int,
);
}
Map<String, dynamic> toMap() => {
'name': name,
'ratePerHour': ratePerHour,
};
}
To learn more about data serialization, read: How to Parse JSON in Dart/Flutter: The Essential Guide.
Next, we can use the withConverter
method to obtain a Query<Job>
object (rather than Query<Map<String, dynamic>>
) when reading data from Firestore:
final jobsQuery = FirebaseFirestore.instance.collection('jobs').withConverter(
fromFirestore: (snapshot, _) => Job.fromMap(snapshot.data()!),
toFirestore: (job, _) => job.toMap(),
);
In this example, I have declared the
jobsQuery
as a global variable and usedFirebaseFirestore.instance
directly as a singleton. This is not recommended for production apps. For a better alternative, read: Flutter App Architecture: The Repository Pattern.
As a result, we can pass our jobsQuery
to the FirestoreListView<Job>
and access the job.name
and job.ratePerHour
properties in a type-safe manner:
FirestoreListView<Job>(
query: jobsQuery,
pageSize: 20,
itemBuilder: (context, doc) {
final job = doc.data();
return ListTile(
title: Text(job.name),
subtitle: Text(job.ratePerHour.toString()),
);
},
)
Voilà! We can now display paginated data from a Firestore collection. And if the data changes on the backend, our UI will automatically rebuild. 🔥
However, we still need to deal with the remaining UI states.
Handling empty, error, and loading states
Fortunately, addressing these states is simply a matter of adding three more arguments: emptyBuilder
, errorBuilder
, and loadingBuilder
:
return FirestoreListView<Job>(
query: jobsQuery,
pageSize: 20,
emptyBuilder: (context) => const Text('No data'),
errorBuilder: (context, error, stackTrace) => Text(error.toString()),
loadingBuilder: (context) => const CircularProgressIndicator(),
itemBuilder: (context, doc) {
final job = doc.data();
return ListTile(
title: Text(job.name),
subtitle: Text(job.ratePerHour.toString()),
);
},
);
It's that simple! 🚀
Conclusion
As demonstrated, FirestoreListView
greatly simplifies data pagination when reading from Cloud Firestore.
The primary advantage of FirestoreListView
is its completely declarative API, which eliminates the need for a "pagination controller" or manual fetching of the next page.
Moreover, since FirestoreListView
uses real-time listeners internally, it automatically updates the UI when data changes. 👍
If you have attempted to implement your own pagination logic before, this code should make you smile:
FirestoreListView<Job>(
query: jobsQuery,
pageSize: 20,
itemBuilder: (context, doc) {
final job = doc.data();
return ListTile(
title: Text(job.name),
subtitle: Text(job.ratePerHour.toString()),
);
},
)
But while the FirestoreListView
API is easy to use, it's not highly configurable.
For situations requiring more control, consider using the FirestoreQueryBuilder
class. The advanced configuration section of the package readme offers detailed guidance on its usage.
In summary, the FirestoreListView
and FirestoreQueryBuilder
widgets make pagination with Cloud Firestore a breeze, allowing you to focus your time and resources elsewhere.
So I hope you’ll find these widgets valuable in your projects.
Happy coding!
New Flutter & Firebase Course
If you want to ship your Flutter apps faster, Firebase is a great choice. And in this new course, I cover all the most important features, including Firebase Auth, Cloud Firestore, Firebase Storage, Cloud Functions, and Firebase Extensions. 👇