Firestore Pagination Made Easy with FirestoreListView in Flutter

Source code on GitHub

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 emptyloading, 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 used FirebaseFirestore.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: emptyBuildererrorBuilder, 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. 👇

Want More?

Invest in yourself with my high-quality Flutter courses.

Flutter In Production

Flutter In Production

Learn about flavors, environments, error monitoring, analytics, release management, CI/CD, and finally ship your Flutter apps to the stores. 🚀

Flutter Foundations Course

Flutter Foundations Course

Learn about State Management, App Architecture, Navigation, Testing, and much more by building a Flutter eCommerce app on iOS, Android, and web.

The Complete Dart Developer Guide

The Complete Dart Developer Guide

Learn Dart Programming in depth. Includes: basic to advanced topics, exercises, and projects. Last updated to Dart 2.15.

Flutter Animations Masterclass

Flutter Animations Masterclass

Master Flutter animations and build a completely custom habit tracking application.