Flutter, Bloc and Firestore Stream, the perfect match, if used the right way!

Version disponible en Français: Flutter, Bloc and Firestore Stream, la combinaison parfaite, si utilisée de la bonne manière!

Esta story tambien esta disponible en español: Flutter, Bloc y Firestore Stream, ¡la combinación perfecta, si se usa de la manera correcta!

My very first Flutter Experience

Back in 2018, the day after being hired in a company as an Angular developer, they finally told me that my job won’t be to dev in Angular, but to develop apps using Flutter. I was like, WTF! that was not the deal and i had never heard a thing about Flutter before!. But then i thought, hey, why not, i enjoy programming and learning new stuff.

So they gave me 10 days of training, by myself, starting with the flutter cookbook (https://flutter.dev/docs/cookbook).

After 2 days of training, honestly i was already in love with Flutter, one code for Android and iOS, and desktop and web in the future (which is now, Flutter 2.0.0…YEAY!!!!), that’s just amazing.

So at the end of my training, ready to start, they asked me if AWS would be a good choice for the backend, and yeah, it certainly is, but i told them, “have you been also considering Firebase?” I mean, it’s a google platform, so compatibility with Flutter are probably like 100%. And so guess what, after that, i finally start developing my first app with Flutter and Firebase. Happy me!

The way from dev to prod with my first app, was not an easy path, i have been struggling a lot with some big mistakes that almost cost the company some money with google billing.

So i am going to try to list some of them so i hope that it won’t happen to you.

Let’s begin

Ok, first things first, configure your firebase project (there is a lot of documentation for that) and at first, keep the Spark plan, which is the fully free one, no credit card asked and you almost have every features in Firebase available. But if you or your company want to switch to the Blaze plan from the start, then the next thing you have to do is to go to https://console.cloud.google.com/billing/ , select your project and on the left menu click on Budget and alert, follow the instructions and set the monthly budget to 1$ and the alerts. If you don’t do that and if you are using StreamBuilder with some firebase snapshot improperly, you may ending having a bad day. Why is that, you may ask, well here it comes:

The StreamBuilder thing

So you are building this nice app, and you have been asked to build this screen where the content has to be updated automatically when the data changes in the DB, and guess what, Firebase snapshots and StreamBuilders are perfect for this job and if you are new to Flutter, you will probably code something like that inside a StatefullWidget:

StreamBuilder<DocumentSnapshot>(
stream: FirebaseFirestore.instance.collection('myCollection').doc(item.id).snapshots(),
builder: (context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if(snapshot.hasData) {
//Parsing your data, using setState(), etc...
} else {
//CircularProgressIndicator, handling error, etc...
}
});

In those few line of code, there are many things wrong. The first one is calling each time FirebaseFirestore.instance, FirebaseFirestore should be instantiated as a Singleton and for that you can use the injectable plugin (https://pub.dev/packages/injectable) and use something like

getIt<FirebaseFirestore>()

to call it.

Next, the DocumentSnapshot HAS to be declared in your initState if you are using a StatefullWidget. If you are not doing so, each time your widget is being rebuilt, your stream will be initiated again until disposing the widget. Imagine it’s a QuerySnapshot with hundreds of documents, you will reach the free 50,000 documents reading in no time and it can be even worth if you are using setState inside the builder as it will cause an infinite loop of rebuild and you don’t want that.

Now if you really want to do it correctly, write your repository using injectable again, use the decorator @lazySingleton, take profit of dependency injection to inject the FirebaseFirestore.instance to your class, it will be much more cleaner. Here is an example of how i am normally doing it (kind of):

Normally, error should be handled inside the repository as well, i am using Either from the package Dartz (https://pub.dev/packages/dartz) for that, but it’s for another story.

Using Stream with Bloc

So this is one of the biggest first thing i have learned from my first project, but the other big problem that you may have is that widgets are being rebuilt everywhere and it’s totally out of your control, so your app is being lagy, to heavy and the reason is that we are usually using StatefullWidget everywhere at the beginning. The thing with StatefullWidget is that if the state changes, a.k.a when using setState(), the whole method build() is being rebuilt. So if you want a more fine grain control over the rebuild, you can split as much as you can your widget with other widgets, or you can use Bloc with the package flutter_bloc (https://pub.dev/packages/flutter_bloc) for example, and start using StatelessWidgets.

And with that, comes listening to Stream using the bloc pattern.

Actually this all come from a question i found on slack the other day and i have been helping him to implement this. I think it’s a legit question and the answer doesn’t come straight forward to your mind.

To be able to do this, you will need to have two events, one to call your repository stream method and listen to it and another one to call when new data are being received.

To keep the same example as earlier, let’s say we want to listen to this getLogs() method, i will need to create those two events:

const factory LogEvent.watchLogsAsked() = WatchLogsAsked;
const factory LogEvent.logsReceived(List<Log> data) = LogsReceived;

now, in our bloc file, we can implement the logic to yield the state we want, but first we need to subscribe on the events of the stream and Stream.listen return a StreamSubscription so we have:

StreamSubscription<List<Log>> _logsStreamSubscription;

Now we can keep going and yield our state based on the events:

yield* event.map(
watchLogsAsked: (WatchLogsAsked _) async* {
yield state.copyWith(logsAreLoading: true);
await _logStreamSubscription?.cancel();
_logsStreamSubscription = _repository.getLogs().listen((data) => add(LogEvent.logsReceived(data)));
},
logsReceived: (LogsReceived data) async* {
//Do your stuff here and yield your state
}
);

And that’s basically it, now you can call BlocBuilder in your StatelessWidget and do everything you want with your data, knowing that it will be the only part being rebuild when new data are coming;

And don’t forget to cancel your subscription as well, just override the Bloc close() method, and cancel the subscription in there.

Here is a full example of how i am using it:

You can see that i am using freezed for the code generation and Either for the error handling, if you want to know more about that, there are a lot of tutorials around here who will explain how it works better than me.

Well that was quite a story, and it is just my point of view, we all know that there are many and i am not pretending that this is the best one. This is just one possibility and there are many more, a that’s one the greatest thing we have, we can always learn new stuff, never bored.

Feel free to let me know what you think, if you like or dislike and why.

Happy coding to everyone!