maestro 0.8.0

  • Readme
  • Changelog
  • Example
  • Installing
  • 69

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.

[0.8.0] #

Modified #

  • MaestroInspector renamed to Inspector.

Added #

  • A specifc Maestro called MaestroInspector which always expose an Inspector.
  • A specifc Maestro called MaestroMemento used for undo/redo actions.
  • Extensions methods undo<T> and redo<T> on BuildContext for undoing/redoing actions on type T.
  • Methods undo<T> and redo<T> on Composer and Maestro static methods.

[0.7.1] #

Added #

  • An update method for Composer.

[0.7.0] #

Added #

  • A onWrite argument for Maestro.readOnly called when a descendants wants to write a new value.

Changed #

  • readAndWrite methods renamed to update.
  • Maestro.initialValue renamed to value.

[0.6.0] #

Added #

  • A readOnly constructor on Maestro allowing a Maestro to be read but not updated by its descendants, and only updated by its parent.

[0.5.0] #

Changed #

  • Revert the possibility to update Maestro value without setting a new key.

[0.4.0] #

Added #

  • A remix method on Performer called when a Performer instance changed and the new one need to be updated from the old one.
  • The value held by a Maestro is now updated when the value changed, whether it's a Performer or not.

Changed #

  • The play method is now on the Performer.
  • initialValue of Maestro is renamed value.

[0.3.0] #

Changed #

  • MaestroInspector can be used multiple times and can decide to bubble action to further ancestors.
  • OnAction return type to bool, in order to continue/cancel bubbling.

[0.2.0] #

Changed #

  • The value held by a Maestro is now updated when the initialValue changed and if it's not a Performer.

[0.1.1] #

Fixed #

  • Make the action an Object everywhere.

[0.1.0] #

Added #

  • An Object parameter to write and readAndWrite methods, in order to indicate what the action is.

Changed #

  • The method in MaestroInspector from onValueUpdated to onAction along with the signature method.

[0.0.1] #

  • Initial Open Source release.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:maestro/maestro.dart';

// This is a way to create the default Flutter application using Maestro.

void main() => runApp(const MyApp());

/// The application.
class MyApp extends StatelessWidget {
  /// Creates a [MyApp] widget.
  const MyApp({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    /// [Maestros] is a widget to easily add multiple [Maestro]s inside
    /// the widget tree.
    /// Each [Maestro] hold a single value. When this value changes, all
    /// dependents widgets are rebuilt.
    return Maestros(
      [
        // This is a special component used to listen any changes from
        // Maestro descendants.
        MaestroInspector(_onAction),

        // This is how we expose data.
        const Maestro(Counter(0)),

        // We separate the Maestros containing data from the Maestros acting on
        // these data. We call them composers.
        Maestro(CounterComposer()),
      ],
      child: MaterialApp(
        title: 'Maestro Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: const _HomePage(),
      ),
    );
  }
}

class _HomePage extends StatelessWidget {
  const _HomePage({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Maestro Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[
            Text('You have pushed the button this many times:'),
            _CounterText(),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // We get the composer to act on the data.
        onPressed: () => context.read<CounterComposer>().increment(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

class _CounterText extends StatelessWidget {
  const _CounterText({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // The call to `listen`, makes `CounterText` rebuild when `Counter`changes.
    final Counter counter = context.listen<Counter>();
    return Text(
      '${counter.value}',
      style: Theme.of(context).textTheme.headline4,
    );
  }
}

/// Data classes hold the state of your application in multiple places.
/// It's better to have immutable objects.
///
/// This is how you can create one by hand.
///
/// You can also use freezed: https://pub.dev/packages/freezed to easily
/// generate them.
@immutable
class Counter {
  /// Creates a [Counter].
  const Counter(this.value);

  /// The value of the counter.
  final int value;

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    } else if (other.runtimeType != runtimeType) {
      return false;
    } else {
      return other is Counter && other.value == value;
    }
  }

  @override
  int get hashCode {
    return value.hashCode;
  }

  @override
  String toString() {
    return 'Counter: $value';
  }
}

/// A composer is a object which mutate the values held by ancestors [Maestro]s.
class CounterComposer with Composer {
  /// We create a getter to read the nearest [Counter] in the tree in order to
  /// see quickly on what this composer depends on.
  /// This is just a convention.
  Counter get _counter => read<Counter>();

  /// Increments the value of the current [Counter].
  void increment() {
    // We read the nearest Counter in order to increment its value.
    final Counter incrementedCounter = Counter(_counter.value + 1);

    // We write the new value.
    write(incrementedCounter, 'counter.increment');
  }
}

bool _onAction<T>(T oldValue, T value, Object action) {
  debugPrint('$action made a transition from $oldValue to $value');
  return true;
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  maestro: ^0.8.0

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:maestro/maestro.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
38
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
69
Learn more about scoring.

We analyzed this package on Jul 2, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.8.4
  • pana: 0.13.13
  • Flutter: 1.17.5

Analysis suggestions

Package not compatible with SDK dart

because of import path [maestro] that is in a package requiring null.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.7.0 <3.0.0
collection ^1.14.0 1.14.12 1.14.13
flutter 0.0.0
Transitive dependencies
meta 1.1.8
sky_engine 0.0.99
typed_data 1.1.6 1.2.0
vector_math 2.0.8
Dev dependencies
flutter_test