Using Firebase Firestore for a Flutter application is an excellent choice - but it bears some challenges. In this series, we introduce some best practices to help you ship your flutter app.
In the last blog post, we explored possibilities to retrieve documents from Firestore, and we also took a thorough look at writing maintainable converter classes to handle serialization and deserialization. If you missed that post for some unimaginable reason, now it is still not too late to dive deep into it.
You can find all the code created for this post in the companion repository.
Interacting with the database layer should be completely isolated from the UI logic, and specific to the model class that is being queried. We can achieve this by using a Data access object, or a DAO for short.
Finally, we can declare our last abstract class for today: FirestoreDao. It defines the signature of the functions to be used to retrieve data from the UI layer. Then we can create concrete implementations for our in-app models.
These DAO implementations look trivial because they really are trivial. They are literally introducing more code without any added functionality, and questioning the need for this layer might be justified. However, this is because of the simplicity of the demo application. In a real application with a more complex database schema, these DAO classes might prove to be very useful as a place where you can hide the logic of your database service layer.
To access a DAO instance of a model class more conveniently, we can modify FirestoreDao to act as a facade, similar to Converters.
This way, from an outer class in the UI layer, the only call to be made to access a real-time stream of the Firestore recipe data is: FirestoreDao.of<Recipe>().modelStream().
This approach also comes with several advantages:
- The UI layer is entirely decoupled from the database service layer
- By using a common DAO interface, changes made in the concrete implementations will not break the signature of the calls made from the UI layer
- The DAO can be easily extended to support reading single documents, deleting documents, writing new documents, querying for specific documents, etc.
- The code is a lot easier to read (at least from the UI layer, for sure)
Some additional reflection on the readability of the code, let's consider our very first approach! Before introducing these layers of abstractions, we could access a Stream<List<Recipe>> as follows:
Which solution would you choose?
Okay, I must admit that my last question was not only rhetorical; it was also posed in a slightly biased context which possibly conveyed the ultimate supremacy of the architecture introduced in this series of posts.
To be fair, we need to reflect on both the advantages and disadvantages of this design. It would be reasonable to do so by battle-testing it by seeing how easily we can extend our app. I invite you to tune in for the final post of this series, where we will do precisely that!