State management is an ever-evolving aspect of any software project. It's not a "one and done" thing, instead, you must continually ensure that you follow best practices to keep things maintainable.

Using Mobx with Flutter effectively follows these principles:

  1. We have access to an Observable selection of state (i.e. variables that change over the course of our application).
  2. We can display these state items within our view and respond to Action intents.
  3. We can modify the state and thus, update our Observable and corresponding view(s).

The best part? The code to make this all work with Mobx is super simple! We can take advantage of the codegen to do most of the hard work for us.

Project Setup

Let's get started by creating a new Flutter project:

# New Flutter project
$ flutter create f_mobx && cd f_mobx

# Open in VS Code
$ code .

Next up, we'll need to install some dependencies and dev_dependencies inside of pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter

  mobx:
  flutter_mobx:

dev_dependencies:
  flutter_test:
    sdk: flutter

  build_runner: ^1.3.1
  mobx_codegen:

We can then go ahead and create a new MaterialApp inside of main.dart to house our CounterPage.

import 'package:f_mobx/pages/counter_page.dart';
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: CounterPage(),
    );
  }
}

We'll then need to create the CounterPage with the user interface that contains an Add and Minus button at lib/pages/counter_page.dart

import 'package:flutter/material.dart';

class CounterPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: AppBar(
          title: Text('Flutter and MobX'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'Counter',
                style: TextStyle(fontSize: 30.0),
              ),
              Text(
                '0', 
                style: TextStyle(fontSize: 42.0),
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  FlatButton.icon(
                    icon: Icon(Icons.add),
                    label: Text('Add'),
                    onPressed: () {},
                  ),
                  FlatButton.icon(
                    icon: Icon(Icons.remove),
                    label: Text('Remove'),
                    onPressed: () {},
                    ),
                  ),
                ],
              )
            ],
          ),
        ));
  }
}

Creating the Counter State

Awesome! We're now ready to create our Counter over at lib/store/counter/counter.dart. Let's take a look at the code and then go through it piece by piece:

import 'package:mobx/mobx.dart';

// This is our generated file (we'll see this soon!)
part 'counter.g.dart';

// We expose this to be used throughout our project
class Counter = _Counter with _$Counter;

// Our store class
abstract class _Counter with Store {
  @observable
  int value = 1;

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

  @action
  void decrement() {
    value--;
  }
}
  1. We're importing mobx.dart which gives us access to Store and other features.
  2. We're then using part to combine the generated version of this class. We haven't used the generator yet, and we'll be doing that next - so don't worry!
  3. Next up, we're exposing a Counter class that uses the generated _$Counter with our MobX bindings. This is what we'll be binding to inside of our application.
  4. Finally, we're creating a _Counter with Store class and we're defining @observable properties and @actions to define areas of our Store that we can interact with.

MobX handles most of the heavy lifting for us, so we don't have to be concerned about how this works under the hood.

Now that we've got our Counter class, lets run the build_runner and mobx_codegen by running the following in our terminal from within the project:

$ flutter packages pub run build_runner watch

We should now be able to see our generated counter.g.dart. Here's an example of what it may look like:

part of 'counter.dart';

mixin _$Counter on _Counter, Store {
  final _$valueAtom = Atom(name: '_Counter.value');

  @override
  int get value {
    _$valueAtom.reportObserved();
    return super.value;
  }

  @override
  set value(int value) {
    _$valueAtom.context.checkIfStateModificationsAreAllowed(_$valueAtom);
    super.value = value;
    _$valueAtom.reportChanged();
  }

  final _$_CounterActionController = ActionController(name: '_Counter');

  @override
  void increment() {
    final _$actionInfo = _$_CounterActionController.startAction();
    try {
      return super.increment();
    } finally {
      _$_CounterActionController.endAction(_$actionInfo);
    }
  }

  @override
  void decrement() {
    final _$actionInfo = _$_CounterActionController.startAction();
    try {
      return super.decrement();
    } finally {
      _$_CounterActionController.endAction(_$actionInfo);
    }
  }
}

Isn't that awesome? Look at all the work we don't have to do!

Binding to the Store

Next up, we'll be binding our counter_page.dart to the Counter store. Once again, let's take a look at how this looks and go into it after:

import 'package:flut_mobx/store/counter/counter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';

class CounterPage extends StatelessWidget {
  final Counter counter = Counter();

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: AppBar(
          title: Text('Flutter and MobX'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'Counter',
                style: TextStyle(fontSize: 30.0),
              ),
              Observer(
                builder: (_) =>
                    Text('${counter.value}', style: TextStyle(fontSize: 42.0)),
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  FlatButton.icon(
                    icon: Icon(Icons.add),
                    label: Text('Add'),
                    onPressed: counter.increment,
                  ),
                  FlatButton.icon(
                    icon: Icon(Icons.remove),
                    label: Text('Remove'),
                    onPressed: counter.decrement,
                  ),
                ],
              )
            ],
          ),
        ));
  }
}

Let's dive in:

  1. We're importing flutter_mobx and our Counter store so that we can reference it later.
  2. Next up, we're initialising a new Counter named counter which we can either listen to observable values with Observer or fire actions: final Counter counter = Counter();
  3. We're using the Observer to listen to the value of counter.value.
  4. We've bound the onPressed events to counter.increment and counter.decrement which fires an action to our Store.

All of this combined gives us our lovely counter application!

Summary

I hope you've found this introduction to MobX with Flutter useful. I'm still learning the best practices for state management within Flutter, so I'll be excited to bring further updates to this series in the future.