Lazy Loading Data From Firestore in Real Time Using Flutter

Paginate through Firestore, and receive updates in real time

Jacobo Soffer
Sep 21 · 5 min read
Image for post
Image for post
Image credit: author

Fetching data from a database and displaying it on a list is a very common task in app development. One crucial task that’s often neglected when doing this, especially by new developers, is lazy loading the data to avoid fetching a whole collection, making your app slower and racking up your bill.

Paginating or lazy loading your data using Firestore is relatively simple — that is if you’re getting the documents once. When you attempt to lazy load your data using streams to keep your data updated in real time, it’s a whole other story.

In this article, I’m going to teach you a technique to lazy load your data from Firestore in real time using the Bloc pattern. If you don’t know anything about the Bloc pattern, I suggest you read their documentation to get an idea of how it works.

What We’re Building

We’ll build a simple app that lazy loads posts from a Firestore collection and displays them on a ListView.

Image for post
Image for post

Project Creation

Start by creating a new Flutter project:

$ flutter create lazy_loading_demo$ cd lazy_loading_demo

Add the following dependencies to your pubspec.yaml:

firebase_core: ^0.5.0
cloud_firestore: ^0.14.0+2
flutter_bloc: ^6.0.5
equatable: ^1.2.5

Next, go to the Firebase console, and create a new project. Once the project is created, enable Firestore, and follow the instructions to add it to your app.

Post Repository

We’re going to need to create a repository that our Bloc can use to fetch data from Firestore. Create a new folder named supplemental, and then create post_repository.dart inside of it. Add the following code:

Listing 1

Now we have a simple repository we can use to get our data. The code is relatively easy to understand: getPosts returns a stream containing the first 15 documents of the posts collection. getPostsPage returns a stream containing 15 documents from the collection, starting after the document we pass as an argument.

We created this repository as a singleton; however, you may want to use dependency injection or a lazy singleton in your app, depending on your use case.

Post Model

We’re going to need a model to represent each post retrieved from the database. For this example, the following class will do:

Listing 2

Create a file named models/post.dart and paste it into the previous code. Note that we’re extending Equatable so we (and our Bloc) can compare instances of Post.

Bloc Implementation

Create a new directory called data_bloc. There, create the following files:

  • data_bloc.dart
  • data_events.dart
  • data_state.dart

Lets start by creating the events for our Bloc. In the file data_events.dart, add the following lines:

Listing 3

Here, we’re creating a base class extending Equatable so we can compare our events. Now that we have the class we’ll base our events on, let’s define which events our Bloc should have:

  • DataEventStart: Used to configure our Bloc
  • DataEventLoad: Used to notify the Bloc new data is available
  • DataEventFetchMore: Used to get more data once we run out

Add the following lines to data_events.dart:

Listing 3.1

Now let’s define the states our Bloc will emit:

  • DataStateLoading: Used when the data isn’t loaded yet
  • DataStateEmpty: Used when there are no documents in the collection we pulled
  • DataStateLoadSuccess: Used when the data was pulled successfully and contains a list of posts

It may be a good idea to add a DataLoadFailure state to emit in case something goes wrong. Try to implement it yourself.

Add the following code to data_state.dart:

Listing 4

Here, we implemented the base class and the states we defined earlier.

Now it’s time to pull our Bloc together. Add the following to data_bloc.dart:

Listing 5

Most of this code is very simple. When the Bloc receives DataEventStart, it resets the class variables and cancels the stream subscriptions. It then creates the first subscription by requesting it from our PostRepository and letting handleStreamEvent (we’ll implement this method next) take care of the events received from the stream.

When our Bloc receives a DataEventLoad event, it first flattens the list of posts and then checks if it’s empty and returns either DataStateLoadSuccess or DataStateEmpty.

Our last event, DataEventFetchMore, requests a new page of data from our PostRepository and adds the stream subscription to our subscriptions list.

Now that we’ve finished most of our Bloc, let’s implement the handleStreamEvent method. Copy the following into your Bloc:

The handleStreamEvent method is simpler than it looks. It takes two arguments, whichever stream called it (passed as its index in the posts list), and the event received from the stream as a QuerySnapshot.

When called, the method first checks the length of the QuerySnapshot received. We request 15 docs at a time, so if we get less than that it means we’ve reached the end of the collection.

It then checks if it’s handling events from the last subscription in the list, and if that’s the case, it sets the lastDoc variable to the last document in the query snapshot.

Finally, it updates the posts list and adds DataEventLoad to our Bloc.

The UI

Now that we have a mechanism to lazy load our data, it’s time to create a UI to test it. Create a new directory for your UI with a file named lazy_list.dart. There, add the following:

Listing 6

The implementation of the UI is very straightforward. We return a Scaffold with a BlocBuilder, which handles all the states the Bloc can emit.

If the posts were loaded successfully, it returns a ListView, which has an extra item if hasMoreItems is true. In the list builder, if the current index is greater than or equal to the post-list length, it adds DataEventFetchMore to the Bloc and displays a CircularProgressIndicator in the meantime. Finally, we close the Bloc on the dispose() method.

One last thing, change your main.dart to contain the code below:

Listing 7

Here, we just initialize Firebase and set LazyListScreen as the home screen. We call WidgetsFlutterBinding.ensureInitialized() so we can make a call to the Firebase Core plugin in our main function.

Conclusion

We’ve successfully implemented a lazy-loading list. This is a very simple implementation you can build on; in a real app, you may want to inject the Bloc to the UI using BlocProvider or modify this example to suit your requirements. There are also different approaches you can take to implement this behavior — the one used here is just the simplest I could think of.

Better Programming

Advice for programmers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store