maestro 0.8.0

Flutter Android iOS web

A way to orchestrate your app's state and to expose your data across your entire Flutter application.

Maestro #

Pub

A simple way for orchestrating the state of an entire Flutter application.

This is an experimentation around InheritedWidgets and StatefulWidgets for managing the state of an entire Flutter application.

The main philosophy behind this package is to have immutable data representing your app state and separate objects able to manipulate that data.

Exposing your app state #

We often have different types of data, which when gathered together, form the app state. For this purpose we have one widget responsible for storing and exposing the app data to its entire subtree. This widget is called Maestro.

A Maestro is created with an initial value which can evolves during the execution of the application.

The following example shows how to expose a String object with an initial value of 'Hello World!':

Maestro(
  'Hello World!',
  child: SubTree(),
)

You can nest multiple Maestros in order to expose different types of data:

Maestro(
  'Hello World!',
  child: Maestro(
    42,
    child: SubTree(),
  ),
)

To simplify the syntax of such code, we can use another widget called Maestros which is responsible to create a nested tree of Maestros. The previous code can be rewritten like this:

Maestros(
  [
    Maestro('Hello World!'),
    Maestro(42),
  ],
  child: SubTree(),
)

Manipulating your app state #

There are many ways to manipulate the data exposed with Maestro depending on what you want.

I want to read the value and rebuild my widget every time the value changes. #

You can either use Maestro.listen<T>(...); inside the build method of your widget, or the extension method on BuildContext called listen<T>();

If you don't want to create your own widget, you can use the MaestroListener<T> widget and provide a builder.

I want to read the value, but only one time because I know the value never change. #

You can either use Maestro.read<T>(...); inside the build method of your widget, or the extension method on BuildContext called read<T>();

I want to read a part of the value and rebuild my widget every time that part changes. #

You can either use Maestro.select<T, R>(...); inside the build method of your widget, or the extension method on BuildContext called select<T, R>(...);

If you don't want to create your own widget, you can use the MaestroSelector<T, R> widget and provide a builder and a selector.

I want to update the value #

You can either use Maestro.write<T>(...); inside an event handler, or the extension method on BuildContext called write<T>(...);

The value you pass to write<T> will update the value held by the nearest Maestro<T> ancestor. A build with the new value will be triggered and all listeners will be rebuilt.

I want to separate the logic from the widget tree #

This package promotes the clear separation between the app business logic and the widgets by introducing a concept of Composer.

A Composer is an object which is responsible for managing a part of the state of your app. It's able to read and write data declared in a Maestro higher than itself in the widget tree.

To create a Composer you'll need to create a class which will apply the mixin Composer. With this mixin, your object will also be able to execute some code when the Composer is initialized and before it is no longer used.

For example, let's considering we have an immutable Counter class and we want to manipulate this counter in a Composer called CounterComposer. We would declare the Maestros like this:

Maestros(
  [
    // This is how we expose a [Counter] model with an initial value of 0.
    const Maestro(Counter(0)),

    // This is how we declare the [CounterComposer].
    Maestro(CounterComposer()),
  ],
  child: SubTree(),
)

The CounterComposer would be implemented like this:

class CounterComposer with Composer {
  /// Increments the value of the current [Counter].
  void increment() {
    // We read the nearest Counter in order to increment its value.
    final Counter counter = read<Counter>();
    final Counter incrementedCounter = Counter(counter.value + 1);

    // We write the new value.
    write(incrementedCounter);
  }
}

In a button somewhere in the SubTree we could call the increment method like this:

FloatingActionButton(
  onPressed: () => context.read<CounterComposer>().increment(),
  tooltip: 'Increment',
  child: Icon(Icons.add),
),

To execute some code when the Composer is initialized, you can override the play method.

Limitations #

The value passed to a Maestro is only used for its initial state. Therefore if you want to change the current value from a parent you need to use the Maestro.readOnly constructor. You'll have a onWrite argument allowing you to intercept any writing request to a read-only Maestro.

Advanced use #

Inspector #

All Maestros can report when their value changed to the nearest Maestro<Inspector>. It can be used to log all the intermediates states that lead to a bug for example.

You can declare an inspector through a specific Maestro like this:

bool onAction<T>(T oldValue, T newValue, Object action){
  print('$action initiated a change from $oldValue to $newValue');
  return false;
}
...
Maestros(
  [
    MaestroInspector(onAction),
    Maestro(Data())
    Maestro(Composer())
    ...
  ],
  child : SubTree(),
)

You can also implements your own inspector like this:

class _Memento<T> implements Inspector {
  @override
  bool onAction<X>(X oldValue, X newValue, Object action) {
    // Do what you want.
    return false;
  }
}
...
Maestros(
  [
    MaestroInspector.custom(onAction),
    Maestro(Data())
    Maestro(Composer())
    ...
  ],
  child : SubTree(),
)

Then when you use write or update you can pass an optional object called action. This action will be provided to the inspector so that you can log the action along with the previous and current values.

You can have multiple Inspector in the tree. The action is bubbling up until there is one Inspector which returns true.

Undo/Redo #

This packages helps you to implement a undo/redo feature within your app. To do so, you'll have to use another specific Maestro called MaestroMemento before the maestro holding your state:

Maestros(
  const [
    MaestroMemento<int>(),
    Maestro<int>(0),
  ],
  child: SubTree(),
),

Then within your sub tree, you will be able to call undo<T> and redo<T> methods on the BuildContext or in a Composer.

By default, a memento can remember up to 16 entries, but you can define this value for matching your needs. To do so, you need to set the maxCapacity argument:

const MaestroMemento<int>(maxCapacity: 256)

Changelog #

Please see the Changelog page to know what's recently changed.

Contributions #

Feel free to contribute to this project.

If you find a bug or want a feature, but don't know how to fix/implement it, please fill an issue.
If you fixed a bug or implemented a feature, please send a pull request.

7
likes
110
pub points
32%
popularity

Publisher

romainrastel.com

A way to orchestrate your app's state and to expose your data across your entire Flutter application.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (LICENSE)

Dependencies

collection, flutter

More

Packages that depend on maestro