Maestro: A Flutter experiment for orchestrating your app state

Romain Rastel
5 min readMay 29, 2020

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:

Application layers

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).

A repository which returns a data stored locally if present, otherwise fetches it from the remote data source, stores it in local and returns the data.

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.

--

--