Maestro: A Flutter experiment for orchestrating your app state
We already have a huge bunch of options for managing our app state in Flutter, so you could wonder why spend some time on another one?
The reason is simple: I love creating things, I can’t stop. Sometimes I find myself thinking about what I could make when I rock my daughter to sleep. Sometimes it can be useful, other times it’s not but I still have learned something in the end.
Today I want to tell you the story of Maestro and you will decide whether it can help you or not. If it does, it will be music to my ears 🎵!
All of this started after using two packages I like a lot: Provider and Freezed (kudos to Remi Rousselet if you’re reading this 😉). I use Freezed to generate the classes of the data coming from the APIs and Provider + ChangeNotifier
for managing my app state. It’s great but the mutability of ChangeNotifier
bother me. So I started to think about a clear separation between the app state and the components that perform actions on it, but I didn’t want to end up with something too verbose either. One night before going to bed, I opened VSCode created a new Flutter project and started to prototype something. In only 300 lines of code I ended up with something I liked and found pleasant to use. I like to share, so instead of keeping this to myself I decided to create a new package I called Maestro and here we are 😅!
🏗 A global app architecture
Before going further on the package let me show you how I separate an application in three layers, each one built upon the previous one:
Many of you may already be familiar with this kind of separation, if not this is a little explanation of each layer:
Data layer
This layer is responsible for fetching and manipulate the data from one or more sources.
There are two main components in this block:
Data sources which are providers of raw data (like a web service client)
Repositories that use one or more data sources to provide data understandable by the rest of the app (we can call them models).
Logic Layer
This layer is the glue between the UI layer and the Data layer. It takes orders given by the user input and then communicates with repositories in order to modify a part of the app state the UI will display.
UI Layer
This layer is responsible for providing something to the user so that they can interact with the data the app produces and consumes.
🗂 App state management with Maestro
In Flutter, you structure your app with widgets. A widget is a declaration of something in your app, it can be a visual element, a way to access your data, just a log writer and all that jazz. Remember: Everything is a widget! Therefore in my vision, the data sources, the repositories and the objects in the logic layer should be encapsulated in widgets. In Maestro the widget responsible for storing these objects is called… Maestro
!
Let’s say you want to expose the current user to a subtree, with Maestro you’ll just have to write this:
Maestro(
User(),
child: SubTree(),
)
If you want to expose the current user and the current cart, you can nest the Maestro
widgets:
Maestro(
User(),
child: Maestro(
Cart(),
child: SubTree(),
),
)
If you have a lot of elements to expose, it’s more convenient to use another widget called Maestros
. This widget takes a list as parameter and recreate a nested tree of Maestro
widgets:
Maestros(
[
Maestro(User()),
Maestro(Cart()),
],
child: SubTree(),
)
In any widget built by SubTree
you will be able to read and write the current user and the current cart. For example you may use Maestro.read<Cart>(context);
to read the current cart and Maestro.write(context, User(firstName: ‘Anakin’, lastName: ‘Skywalker’));
to write the current user. Like Provider, you need to provide a BuildContext
to these methods.
With Maestro, the logic layer is composed essentially by objects I called Composers
. The composers know how to read and write the models declared before them.
Let’s see a simple Composer
:
A composer is a class which apply the mixin called Composer
. By applying this mixin, you’ll get access to the methods read
and write
that you can use to get and set objects exposed before the composer. As you can see, you don’t need to pass any BuildContext
here, Maestro will do that for you 😉.
This composer depends on a UserRepository
and gets it with a call to the read
method.
In the authenticate
method we delegate the authentication to the repository and our job is to handle errors (we don’t do it in the example) and to update the app state with a call to the write
method.
These are the few components you need to know to use Maestro for managing the app state.
🎼 Let’s play with Maestro!
This was the theory, now I will show you how to do the Flutter Counter app with Maestro:
We encapsulate the MaterialApp
with a Maestros
widget in which we expose the Counter
with an initial value of 0 and the CounterComposer
. By doing so, the entire app can access the current counter and the composer.
In the _HomePage
you can see a specific widget _CounterText
(we will see it in a few seconds) and the call to the increment
method of the composer when the user taps on the floating action button. You can see that instead of Maestro.read<CounterComposer>(context)
we used an extension method on BuildContext
provided by Maestro.
This widget use another method of Maestro called listen
. This method is able to read the value of the nearest Maestro
ancestor of the given type and tells the Flutter to rebuild this widget when the value changes.
We have other options than creating a dedicated widget in order to listen for changes but this is how I like to do.
Here we have the implementation of the model called Counter
, and its composer called CounterComposer
.
The model should be immutable, for doing it, we use another great package called Equatable. You can also use Freezed to achieve the same result.
🎉 Conclusion
Maestro is for now an experiment. I don’t know yet if there are cases where it does not fit, but I find it very promising. The Fluture will tell 😜!
You can download it on pub.dev, the source code is on this GitHub repository along with more information on the package.
Let me know what you think about it.