mobx 0.3.5+1

mobx.dart #

pub package CircleCI Coverage Status

MobX for the Dart language.

Supercharge the state-management in your Dart apps with Transparent Functional Reactive Programming (TFRP)

Introduction #

MobX is a state-management library that makes it simple to connect the reactive data of your application with the UI. This wiring is completely automatic and feels very natural. As the application-developer, you focus purely on what reactive-data needs to be consumed in the UI (and elsewhere) without worrying about keeping the two in sync.

It's not really magic but it does have some smarts around what is being consumed (observables) and where (reactions), and automatically tracks it for you. When the observables change, all reactions are re-run. What's interesting is that these reactions can be anything from a simple console log, a network call to re-rendering the UI.

MobX has been a very effective library for the JavaScript apps and this port to the Dart language aims to bring the same levels of productivity.

Go deep #

For a deeper coverage of MobX, do check out MobX Quick Start Guide. Although the book uses the JavaScript version of MobX, the concepts are 100% applicable to Dart and Flutter.

Core Concepts #

MobX Triad

At the heart of MobX are three important concepts: Observables, Actions and Reactions.

Observables #

Observables represent the reactive-state of your application. They can be simple scalars to complex object trees. By defining the state of the application as a tree of observables, you can expose a reactive-state-tree that the UI (or other observers in the app) consume.

A simple reactive-counter is represented by the following observable:

import 'package:mobx/mobx.dart';

final counter = Observable(0);

More complex observables, such as classes, can be created as well.

class Counter {
  Counter() {
    increment = Action(_increment);
  }

  final _value = Observable(0);
  int get value => _value.value;

  set value(int newValue) => _value.value = newValue;
  Action increment;

  void _increment() {
    _value.value++;
  }
}

On first sight, this does look like some boilerplate code which can quickly go out of hand! This is why we added mobx_codegen to the mix that allows you to replace the above code with the following:

import 'package:mobx/mobx.dart';

part 'counter.g.dart';

class Counter = CounterBase with _$Counter;

abstract class CounterBase with Store {
  @observable
  int value = 0;

  @action
  void increment() {
    value++;
  }
}

Note the use of annotations to mark the observable properties of the class. Yes, there is some header boilerplate here but its fixed for any class. As you build more complex classes this boilerplate will fade away and you will mostly focus on the code within the braces.

Note: Annotations are available via the mobx_codegen package.

Computed Observables #

What can be derived, should be derived. Automatically.

The state of your application consists of core-state and derived-state. The core-state is state inherent to the domain you are dealing with. For example, if you have a Contact entity, the firstName and lastName form the core-state of Contact. However, fullName is derived-state, obtained by combining firstName and lastName.

Such derived state, that depends on core-state or other derived-state is called a Computed Observable. It is automatically kept in sync when its underlying observables change.

State in MobX = Core-State + Derived-State

import 'package:mobx/mobx.dart';

part 'counter.g.dart';

class Contact = ContactBase with _$Contact;

abstract class ContactBase with Store {
  @observable
  String firstName;

  @observable
  String lastName;

  @computed
  String get fullName => '$firstName, $lastName';

}

In the example above fullName is automatically kept in sync if either firstName and/or lastName changes.

Actions #

Actions are how you mutate the observables. Rather than mutating them directly, actions add a semantic meaning to the mutations. For example, instead of just doing value++, firing an increment() action carries more meaning. Besides, actions also batch up all the notifications and ensure the changes are notified only after they complete. Thus the observers are notified only upon the atomic completion of the action.

Note that actions can also be nested, in which case the notifications go out when the top-most action has completed.

final counter = Observable(0);

final increment = Action((){
  counter.value++;
});

When creating actions inside a class, you can take advantage of annotations!

import 'package:mobx/mobx.dart';

part 'counter.g.dart';

class Counter = CounterBase with _$Counter;

abstract class CounterBase with Store {
  @observable
  int value = 0;

  @action
  void increment() {
    value++;
  }
}

Reactions #

Reactions complete the MobX triad of observables, actions and reactions. They are the observers of the reactive-system and get notified whenever an observable they track is changed. Reactions come in few flavors as listed below. All of them return a ReactionDisposer, a function that can be called to dispose the reaction.

One striking feature of reactions is that they automatically track all the observables without any explicit wiring. The act of reading an observable within a reaction is enough to track it!

The code you write with MobX appears to be literally ceremony-free!

ReactionDisposer autorun(Function(Reaction) fn)

Runs the reaction immediately and also on any change in the observables used inside fn.

import 'package:mobx/mobx.dart';

String greeting = Observable('Hello World');

final dispose = autorun((_){
  print(greeting.value);
});

greeting.value = 'Hello MobX';

// Done with the autorun()
dispose();


// Prints:
// Hello World
// Hello MobX

ReactionDisposer reaction<T>(T Function(Reaction) predicate, void Function(T) effect)

Monitors the observables used inside the predicate() function and runs the effect() when the predicate returns a different value. Only the observables inside predicate() are tracked.

import 'package:mobx/mobx.dart';

String greeting = Observable('Hello World');

final dispose = reaction((_) => greeting.value, (msg) => print(msg));

greeting.value = 'Hello MobX'; // Cause a change

// Done with the reaction()
dispose();


// Prints:
// Hello MobX

ReactionDisposer when(bool Function(Reaction) predicate, void Function() effect)

Monitors the observables used inside predicate() and runs the effect() when it returns true. After the effect() is run, when automatically disposes itself. So you can think of when as a one-time reaction. You can also dispose when() pre-maturely.

import 'package:mobx/mobx.dart';

String greeting = Observable('Hello World');

final dispose = when((_) => greeting.value == 'Hello MobX', () => print('Someone greeted MobX'));

greeting.value = 'Hello MobX'; // Causes a change, runs effect and disposes


// Prints:
// Someone greeted MobX

Future<void> asyncWhen(bool Function(Reaction) predicate)

Similar to when but returns a Future, which is fulfilled when the predicate() returns true. This is a convenient way of waiting for the predicate() to turn true.

final completed = Observable(false);

void waitForCompletion() async {
  await asyncWhen(() => _completed.value == true);

  print('Completed');
}

Observer

One of the most visual reactions in the app is the UI. The Observer widget (which is part of the flutter_mobx package), provides a granular observer of the observables used in its builder function. Whenever these observables change, Observer rebuilds and renders.

Below is the Counter example in its entirety.

import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';

part 'counter.g.dart';

class Counter = CounterBase with _$Counter;

abstract class CounterBase with Store {
  @observable
  int value = 0;

  @action
  void increment() {
    value++;
  }
}

class CounterExample extends StatefulWidget {
  const CounterExample({Key key}) : super(key: key);

  @override
  _CounterExampleState createState() => _CounterExampleState();
}

class _CounterExampleState extends State<CounterExample> {
  final _counter = Counter();

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: const Text('Counter'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                'You have pushed the button this many times:',
              ),
              Observer(
                  builder: (_) => Text(
                        '${_counter.value}',
                        style: const TextStyle(fontSize: 20),
                      )),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: _counter.increment,
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        ),
      );
}

Roadmap #

Observables #

  • [x] Create Observable<T> via Observable<T>()
  • [x] Create ObservableList<T>
    • [x] observe hook
    • [ ] intercept hook
  • [ ] Create ObservableMap<K, T>
    • [ ] observe hook
    • [ ] intercept hook
  • [ ] Create ObservableSet<T>
    • [ ] observe hook
    • [ ] intercept hook
  • [x] Atoms with createAtom()

Computed Observables #

  • [x] Create Computed<T> via Computed<T>()
  • [x] 2-phase change propagation

Reactions #

  • [x] Create Reaction with autorun()
    • [x] with delay
  • [x] Create Reaction with reaction()
    • [x] with delay
    • [x] with fireImmediately
  • [x] Create Reaction with when()
  • [x] Create Reaction with asyncWhen() returning Future<T>

Actions #

  • [x] Create Action
  • [x] Create untracked-action with untracked<T>()
  • [x] Create transaction with transaction<T>()

Cross cutting features #

  • [x] Use of a ReactiveContext and ReactiveConfig to isolate the reactivity. This is an advanced feature and useful in case you are running multiple independent reactive systems without causing any interference between them. This is possible if a library chooses to use MobX internally and the library gets consumed by an app that also uses MobX. In that scenario, you want the reactivity of the library NOT to interfere with the reactivity within the app. For most cases, you don't have to worry about this. MobX will default to using the singleton mainContext, which is at the app level.
  • [x] Observability API for Observable and Computed
    • [x] observe
    • [x] intercept
    • [x] onBecomeObserved
    • [x] onBecomeUnobserved
  • [ ] Spying and Tracing
  • [x] Global configuration
  • [x] Exception handling and Error recovery
    • [x] Error boundary
    • [x] Disabling Error boundary in global config
  • [ ] Debuggability

Public facing #

  • [x] Logo
  • [ ] Documentation
  • [ ] Website (published on github.io)

Contributing #

If you have read up till here, then 🎉🎉🎉. There are couple of ways in which you can contribute to the growing community of MobX.dart.

0.3.5 - 0.3.5+1 #

  • Fixed a bug where the ObservableFuture<T> would not show the correct status. This was happening because of the lazy evaluation strategy. We are now being eager in creating the status and monitoring the inner Future<T> immediately.
  • Upgraded test_coverage

0.3.3+1 - 0.3.4 #

  • Removed the @experimental annotations for Observable{Future,Stream} and reaction.
  • Removed the dependency on the meta package.
  • Some formatting changes
  • Reporting update or change only when the new value is different than the old value. This is mostly for observable collections like list, set, map.

0.3.3 #

  • Wrapping all collection setters in conditional action wrappers. This removes the need to wrap collection mutating methods in explicit actions.

0.3.0 - 0.3.2+3 #

  • API changes introduced to the enforceActions setting of ReactiveConfig. It is now called writePolicy and the enum EnforceActions has been renamed to ReactiveWritePolicy.
  • Also introducing a readPolicy setting on ReactiveConfig. It is an enumeration with two values:
  • Removing the "strict-mode" text in the exception message when the ReactiveWritePolicy is violated. This was a vestige from the mobx.js world.
  • Exposing a boolean isWithinBatch on ReactiveContext, which tells if the current code is running inside a batch. This is used to conditionally apply an action-wrapper for mobx_codegen-generated setters.
  • Introduced an action-wrapper called conditionallyRunInAction() that runs the given function in an action only when outside a batch.
  • Increasing test coverage
enum ReactiveReadPolicy { always, never }

0.2.1+2 #

  • Improving test coverage

0.2.1+1 #

  • README updates

0.2.1 #

0.2.0 #

  • A breaking change has been introduced to the use of the Store type. Previously it was meant to be used as an interface, which has now changed to a mixin. Instead of doing:
abstract class UserBase implements Store {}

You now do:

abstract class UserBase with Store {}

This allows us to add more convenience methods to the Store mixin without causing any breaking change in the future. With the current use of the interface, this was not possible and was limiting the purpose. Store was just a marker interface without any core functionality. With a mixin, it opens up some flexibility in adding more functionality later.

  • All the docs and example code have been updated to the use of the Store mixin.

example/README.md

Examples #

Annotations #

Annotations drastically simplify the usage of MobX by tastefully tucking away all the boilerplate in the generated files (*.g.dart).

These examples use annotations, which is supported by the mobx_codegen package. To run the code-generator, we are using the following command:

$> cd $PATH_TO_MOBX_DART/mobx/example
$> flutter packages pub run build_runner build

Counter (counter.dart)

A really simple Counter. After all this is how you start off a Dart/Flutter project :-)

import 'package:mobx/mobx.dart';

part 'counter.g.dart';

class Counter = CounterBase with _$Counter;

abstract class CounterBase with Store {
  @observable
  int value = 0;

  @action
  void increment() {
    value++;
  }
}

Notice the @observable value property and the @action increment()that mutates it.

Todos (todos.dart)

This example showcases some of the core features of MobX such as Observable, Computed and Action. We have intentionally left out Reaction to keep it simple.

The Todos example is a classic way of showcasing a framework or a library. Here, you can see how MobX can simplify your code.

Todo #

A Todo entity is at the heart of the Todos example. A bit of boilerplate is needed to make the code-generator do the rest for you.

import 'package:mobx/mobx.dart';

part 'example.g.dart';

class Todo = TodoBase with _$Todo;

abstract class TodoBase with Store {
  TodoBase(this.description);

  @observable
  String description = '';

  @observable
  bool done = false;
}

Observables #

The TodoList manages the list of Todo. Notice the use of annotations to make the code more readable. MobX follows the princple:

What can be derived, should be derived. Automatically.

This is achieved with a small core-state (the @observable properties) and the derived-state (the @computed properties).

enum VisibilityFilter { all, pending, completed }

class TodoList = TodoListBase with _$TodoList;

abstract class TodoListBase with Store {
  @observable
  ObservableList<Todo> todos = ObservableList<Todo>();

  @observable
  VisibilityFilter filter = VisibilityFilter.all;

  @observable
  String currentDescription = '';

  @computed
  ObservableList<Todo> get pendingTodos =>
      ObservableList.of(todos.where((todo) => todo.done != true));

  @computed
  ObservableList<Todo> get completedTodos =>
      ObservableList.of(todos.where((todo) => todo.done == true));

  @computed
  bool get hasCompletedTodos => completedTodos.isNotEmpty;

  @computed
  bool get hasPendingTodos => pendingTodos.isNotEmpty;

  @computed
  String get itemsDescription =>
      '${pendingTodos.length} pending, ${completedTodos.length} completed';

  @computed
  ObservableList<Todo> get visibleTodos {
    switch (filter) {
      case VisibilityFilter.pending:
        return pendingTodos;
      case VisibilityFilter.completed:
        return completedTodos;
      default:
        return todos;
    }
  }

  // ...
}

Notice how you can have derived-state that depends on other derived-state. For example, itemsDescription depends on pendingTodos and completedTodos, which are also computed (aka derived) properties.

Actions #

The operations that can be performed on the TodoList are marked with @action. These are simple functions, which can be invoked like normal dart functions! There is no extra ceremony to invoke an action.

class TodoList = TodoListBase with _$TodoList;

abstract class TodoListBase with Store {
  // ...

  @action
  void addTodo(String description) {
    final todo = Todo(description);
    todos.add(todo);
    currentDescription = '';
  }

  @action
  void removeTodo(Todo todo) {
    todos.removeWhere((x) => x == todo);
  }

  @action
  void changeDescription(String description) =>
      currentDescription = description;

  @action
  void changeFilter(VisibilityFilter filter) => this.filter = filter;

  @action
  void removeCompleted() {
    todos.removeWhere((todo) => todo.done);
  }

  @action
  void markAllAsCompleted() {
    for (final todo in todos) {
      todo.done = true;
    }
  }
}

Use this package as a library

1. Depend on it

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


dependencies:
  mobx: ^0.3.5+1

2. Install it

You can install packages from the command line:

with pub:


$ pub get

with Flutter:


$ flutter pub get

Alternatively, your editor might support pub get or 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:mobx/mobx.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
95
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]
98
Learn more about scoring.

We analyzed this package on Aug 18, 2019, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.4.0
  • pana: 0.12.19

Platforms

Detected platforms: Flutter, web, other

No platform restriction found in primary library package:mobx/mobx.dart.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.2.0 <3.0.0
meta ^1.1.6 1.1.7
Dev dependencies
fake_async ^1.0.1
mockito ^4.0.0
test ^1.6.3
test_coverage ^0.3.0