Flutter: How to dramatically decrease reads from Firestore using Local Storage

Tarun Thummala
PressW
6 min readFeb 7, 2022

--

At Gathr, www.magical.app, we’ve been building our primary application in Flutter for over a year and a half now and we love it. Flutter allows us to quickly deploy our application to Android and iOS, and by utilizing Firebase as our BaaS (backend-as-a-service), we’ve been able to quickly scale up our product and feature set, thanks to Firebase + Flutter’s easy integrations.

Firebase is also exceptionally powerful for our specific use case as Gathr is a social-media-esq app, and we can utilize the Stream based Firestore queries to keep our app real-time for our events and chat features specifically.

The Issue

While incredibly powerful, performant, and scalable, Firebase’s Firestore comes with its own set of issues. Firstly came the problem of $$$. Firebase is a Pay-as-you-Go service, meaning that you are charged for your usage of their services past a certain free tier. Firestore in particular charges for every read and write that you perform. This means for a read-intensive feature, such as chat, the number of reads that you need to do, increases dramatically for every chat your user is apart of. Across scale, costs will run rampant as you now need to do document reads for each individual chat (group chats), as well as each chat message itself.

Without any caching, paging, or optimizations, let’s run through an example of the number of reads a user will trigger when loading their chat (Based on Gathr).

First some architecture details. Each individual chat in Gathr (group chats, DMs, etc) are stored as a document in a larger collection. Each one of these “chat” documents then has a sub-collection called “messages”, which stores any number of documents representing each individual message.

Firestore Chat Architecture

As you can see, for every chat that a user is apart of and opens, the number of reads goes up tremendously.

Let’s say a user is apart of 25 different chats, with each chat having on average 30 messages. Now because we have no local storage, just by opening the app the user triggers a read of at minimum 25 documents for each of their different chats, and then every chat they open costs an additional 30 reads. Just for opening one chat and reading the latest message, Gathr is billed for 55 reads! Now imagine that scaled across 100,000 users, with multiple chats receiving updates every day. The number of reads would be astronomical.

Obviously fetching every chat and every message in each chat is inefficient and can be pared down with pagination, we wanted to come up with a solution that allowed us to cut our reads down from total messages, to just new messages/updates.

We can do better.

Enter Drift

Drift is a SQLite DB written in Dart for Flutter applications, otherwise often termed as a form of “local storage”, an on-device database. Drift is going to help ease the burden of reads off Firestore by locally caching all of the chats and messages a user has.

Here lies an important question you may be wondering: if we’re storing everything locally, how will we know what messages and chats we need to get from Firestore and how will we update messages in Drift? Well okay maybe you weren’t wondering that, but you should be!

The answer lies in a DateTime timestamp. For every chat and chat message in our database we store a field called lastTouched. LastTouched is a timestamp that represents the last time any edits were made to that document. This is going to be incredibly helpful as we merge Firestore and Drift together.

We are going to setup our Drift database to mimic our Firestore database. We will create a table that stores a row for every chat that a user is apart of, and then another table that has a row for every chat message, where every message has a reference to the id of the chat it is apart of, so we can query later.

For a more in-depth tutorial on how to use Drift, refer to their docs here!

The Dawn of Lower Read Costs

So now that we have our Drift database setup to mimic Firestore, let’s go over how to save that $$$.

Our new chat system will work through Streams from our Drift DB. First we must establish a “Set Point”, which is the lastTouched timestamp of the last message in the chat (ordered by lastTouched). Then we can begin to setup our streams.

The first one will listen to all “older” messages, that have a lastTouched value that is less than (before) the Set Point. The second will listen to all messages in the database that have a lastTouched value greater than our Set Point. A separate Firestore stream can then be setup to fetch all new chat messages/updates to older messages (such as liking a message, editing the text, etc).

Drift + Firestore streams setup

This may be a bit confusing to process so I’ll try to break it down a bit simpler. I’ll walk through a user experience on the app and cross reference what’s going on behind the scene.

A user boots their app and goes to a specific chat. First to note, all the chats are now locally stored so we have eliminated the 25 initial reads we were doing before to get all the chats they are apart of, woot! But we’re only halfway there.

Now when they load into a chat, they should see all their previous 30 messages (reading for free since it’s from Drift!). This is because as soon as they load into a chat, we fetched the latest message in that chat’s lastTouched field, and used that as our set point. We then opened two streams from our Drift DB. One that fetches all messages that have a lastTouched field before our set point (this contains the 30 messages they had prior) and one that fetches all after. One additional stream is setup from Firestore, which listens to the messages sub-collection and reports back any message documents that have a lastTouched field after the set point.

Now a new user sends a message, our Firestore stream dumps the new message doc into our database, and our second database streams reports it back out, showing the new message to our user!

AND BOOM!

Just like that,

we’ve cut the number of reads from 55 for a single chat, to just 1

Your mind right now

Thanks!

And that concludes my crash-course on how to integrate Cloud Firestore with Drift DB to dramatically reduce the number of reads in an application. In our case we were looking at a chat application example, but this concept of using a Set Point + stream based setup should be able to work for any local storage system working with Firestore.

I hope you all got something out of this, please feel free to drop a question below, or reach out to me personally at tarun@gathr.team, and I’ll be happy to give you more insight or answer any burning questions.

Also check out www.magical.app to completely change the way you organize your social life :)

--

--