Cloud Firestore can be very powerful when coupled with Flutter, but also tricky for Production-Ready Apps where a good architecture is paramount.
Firebase Cloud Firestore’s Flutter integration is nothing short of amazing. It takes advantage of Dart’s language features, enabling fast development cycles with almost zero backend code. But as Uncle Ben once said: “With great power comes great responsibility” or in our case: All that flexibility can come at a price.
Production-Ready Apps need to be robust, so regular code maintenance or feature updates won’t easily bring along unnoticed bugs. The key to achieve it is following best practices. Adopting tried-and-true architectures and sound design patterns, will provide a much more pleasant experience to your users.
This article explores why most examples provided on Firestore’s “Tutorials” and “Quick Starts” don’t follow these best practices, so you shouldn’t strictly copy them, but instead extract the essence of what they are trying to teach.
Note: This article is meant for developers already familiarized with Flutter and Firebase Cloud Firestore. If you aren’t there yet, I suggest you hone your skills a bit more and then come back here later.
You have probably seen a piece of code like this before:
This code snippet comes straight out of Firestore’s own examples, as we can see the database query code is heavily intermingled with the UI code. While answering questions on StackOverflow I stumble upon this pattern all the time, but the funny thing is: I can’t even blame Firestore for this, as they have a pretty good reason for including it on their samples, as we will see below.
So, what’s the problem with that code? Why does Firestore use it on their samples? Can we make it better?
Writing high-quality software isn’t just about aiming for code that “works,” there’s more to be baked into your code to bring it to the next level.
Concepts like testability and maintainability are fundamental for projects expected to hit the market and deliver a continuous experience of robustness throughout future App updates. Of course there is much more to Software Quality than those two topics, but we will focus on those as they are essential to understand the root of our problem.
Keep in mind we won’t go deep into those two topics, otherwise we risk deviating too much from the original intent of this article, but a brief comprehension is essential to understand the problem we are accessing and why it is highly connected to software quality.
You want your code to be testable, this means being able to build automated tests (Unit Tests, Integration Tests, etc.) that can be triggered whenever you want to, this way you can be sure that new modifications won’t break functionality already present into your App.
You can also TDD and be sure that every new code you write will function as intended in every possible scenario, as described and covered by your previously written tests.
But writing testable software isn’t easy. How do you test code that needs to receive data from a Database? Or a REST API call from the internet? What if the network is down?
If you get a message saying that your tests failed, are you sure they failed because your code has a bug, or is it just the Internet connection that’s down?
Let’s say your tests execute flawlessly, how about running 1000 tests, each of them needing a round-trip to the server? How long will it take for your tests to complete?
What if your tests need to create or delete data? Can you risk your precious Database with code that hasn’t been tested yet?
This is a very broad concept but in a nutshell it’s the idea of making your code ready for future changes, as Heraclitus once said “The only constant in life is change,” and so it is in software as well.
Additional features will be requested, bugs would have to be fixed, you know the drill. Can your teammates easily understand what your code does? Can your future self, 3 years from now, read your code and understand it? How easy it is to adapt it to something else? These questions are closely related to code readability and coupling.
Many times maintainability and testability walk hand in hand. For example, if your project is heavily coupled with a bunch of code calling each other, all of that functionality mixed up together, not only it will be hard to maintain, it’s really hard to test as well. If a test fails, how do you know which of the countless inner parts of that heavily coupled code that’s actually broken? Can you easily identify and point out the culprit?
So how does testability and maintainability relate to our Firestore previous sample?
Well, can you test that code? No, you can’t as it will send a network request to your Firestore instance. What if one of your tests says a ListTile has wrong data? Is it because there’s wrong data on the database or is it because your build function doesn’t handle the data correctly, thus constructing the ListTile incorrectly? You can’t know because you can’t separate both parts to test them independently.
Can you easily maintain that code?
No, you can’t. As we just said, the build function is heavily coupled with Firestore. Not even mentioning readability, can you quickly identify what’s the purpose of the query Firestore.instance.collection(‘books’).snapshots()? How about if it was just getBooks() what’s easier to follow?
Right now you are probably thinking “Ok, if that’s so bad, then why would Firestore include this on their own samples?” and the answer is simple: Because they are meant to be just samples! Not an actual reference for future implementations.
They are there as a quick start, as something tiny where you can understand in a few seconds how easy it is to use Firestore and Flutter together, not that you actually should use it that way. Think of it as a TV ad for a new car, they show how fast you can go, or all the wild maneuvers you could do, but of course everyone knows that’s not how you are supposed to be driving.
Ok, so we already covered the problems and the reasons, let’s go to the solution.
There are many good ways to properly use Firestore with Flutter, specially if you are already using a State Management solution like Redux, BLoC, Provider with Change Notifier etc. but I want to show you a different one, a simple solution that I have been using for a while with very good results.
This solution is nothing new in the realm of software engineering, it’s an adaptation I made to work with Flutter.
It leverages Dependency Injection with the Repository Pattern. If you aren’t familiar with these concepts, don’t worry we will do a quick review on them, but I will still recommend you read more about at least Dependency Injection, as it’s a really powerful concept and deserves some special attention.
The Repository Pattern is just an abstraction layer between your business logic and your data access logic. Some code will make this clearer:
First lets make a simple Book class:
So now we can query the database for books, using our Repository:
With this Repository class we can now call getBooks and get a Stream of Books. The user of getBooks doesn’t have to know how it will return a Stream of Books, it just knows it will. That’s the contract getBooks offers to its users, the actual query implementation is encapsulated and abstracted, it could use Firestore or even another database. For the user of getBooks it doesn’t matter at all, it’s hidden and for a good reason.
Our Repository solution isn’t ready yet, even though it improves the testability of the build function, it uses Firestore.instance internally which makes testing the Repository class itself a lot harder, which is ironic at best. Also, using Firestore.instance will make our tests even harder since it’s a Singleton and we can’t easily control its behavior for each test case and isolate them, assuring that the result of one test won’t interfere with another.
Dependency Injection is a programming style where, instead of creating dependencies internally, we create those externally and then inject them into any code that needs it. This architectural change enables higher testability and flexibility, as it allows for transparently swapping implementations, changing our algorithms behavior without actually modifying its code.
If this sounds too abstract, fear not, Dependency Injection is a lot simpler to understand seeing actual code rather than reading definitions.
So in order to implement Dependency Injection to our sample, instead of calling Firestore.instance to get an instance of Firestore inside getBooks, we will inject that instance into the Repository class itself. The injection can be made by simply passing the instance to the class’s constructor and storing it for later usage under a field, but Dependency Injection libraries can be very handy as well, as we will see.
Once its dependencies are injected, the Repository class can’t know which instance of Firestore it’s dealing with, it could be a real one or a fake one, and that’s excellent for testing as all the network connections can be faked, with query results created on demand for each test case.
There are some ways of using Dependency Injection in Flutter, as of now (August 2020) Pub has a handful of packages but I recommend Provider as not only it helps with Dependency Injection but also integrates with Flutter as a “supercharged” InheritedWidget.
We will add Dependency Injection to our Repository by injecting a reference to a Firestore instance into the Repository’s constructor:
Then we add Provider to our top level Widget (which we don’t have for this sample, but let’s pretend we have one named SampleApp), injecting the Repository into the entire Widget Tree. But when creating the Repository instance itself, we “manually” inject Firestore.instance into the Repository’s constructor.
As you can see Dependency Injection is a programming pattern and you don’t need a Framework or a Library to apply it, but they do make it simpler and are very helpful.
With all this done, any Widgets can now use our brand new Repository. This includes the original sample that got us here, we just need to modify it a bit (and add a few performance optimizations as a bonus):
Let’s break down the modifications:
- BookList is now a StatefulWidget. This is a performance optimization for the StreamBuilder. In the original sample we were creating the books’ Stream directly inside our build function, this is bad and should be avoided as the build function can be called by the framework multiple times, each creating a new Stream, unsubscribing from the old one and subscribing to the new. Depending on how your code is structured this can even lead to memory leaks.
By making BookList a StatefulWidget we can create our Stream once at initState and doesn’t matter how many times the build function is called, the same Stream instance will be used.
Just keep in mind you might want to adapt this code if your App needs a more sophisticated Stream subscription logic, such as resubscribing in case of errors or if the Stream is closed.
- BookList doesn’t create an instance of Repository, we ask Provider to give us the one it holds internally using context.read<Repository>(). This way we can configure Provider with the instances we want to inject into BookList and all our other Widgets as well. They could be a real instance for our regular App or a fake instance for a Unit Test case.
- Injecting an instance of Repository into the Widget Tree root will enforce that all our Widgets use the same instance, this is something close to a Singleton, but without the drawbacks. Don’t worry, Provider doesn’t limit you to a Singleton-only injection. If you need to substitute that instance to another one you can.
- The Book class we used is nothing more than a Value Object. So I suggest you read more about them and use built_value to auto-generate code, saving you lots of time getting rid of boring boilerplate code. With built_value we can directly deserialize data from Firestore to a Book object instance.
- If you have been paying close attention, our code basically uses the Repository class for State Management. It’s composed of functions that return Stream and those Streams, paired with a StreamBuilder, automatically update our App’s State. If you are thinking “wait, isn’t this just a Simple BLoC”? Yep, it is. Not that we intended to but, it’s a great coincidence that Firestore’s automatic Stream updates can be encapsulated with the Repository Pattern, thus turning out to be a Simple BLoC!
(By Simple BLoC I mean the Pattern where we have functions returning Streams, instead of the traditional full-fledged BLoC where we have Sinks as inputs and Streams as outputs. This was an early definition of the BLoC Pattern, but some packages popularized BLoC in a slightly different form).
Keep in mind the Repository is only meant to be a data-access abstraction layer. For a simple CRUD implementation it can act as a State Management object as well, but if you need additional business logic, don’t do it in the Repository as you would in a BLoC.
- Our Repository is a very simple one, and this was intentional, as to not deviate from the main subject. Also the Repository Pattern can be found in many forms in the wild, like the ones more focused on DDD or as a simple DAO, so given the differences in complexity, I prefer leaving the implementation choice to you, as I can’t say what suits better your project.
- This architecture can be expanded so think about how it could be incorporated into your own App, for example, you could inject Firebase Auth into the Repository class as well so you could query by user ID, or even inject Firebase Auth into your entire App so you could make Authentication and Authorization logic highly testable!
- If you have lots of Firestore documents, your App may get too many updates to deal with, which might severely impact your performance. In that case, consider adding pagination or other network performance improvements to your Repository.
That’s what a data-access abstraction layer is all about, hiding implementation details and decoupling complex networking logic.
- Take a look at Mockito so you can easily create fake test instances (Mocks) and save yourself some precious time.
Firestore’s official sample is a great quick start, it succinctly shows how to get up and running and how amazing and powerful the library can be, but it’s architecture shouldn’t be followed by production-ready Apps. By leveraging Dependency Injection and the Repository Pattern we were able to build a much more maintainable and testable solution.