Flutter MobX Tutorial – Transparent & Reactive State Management?

4  comments

Not a day goes by without a heated debate taking place somewhere in the comments about the best state management solution. MobX is one of them. Originating in the JavaScript world, it has found a way to Dart. Unlike most of the other state management libraries, MobX heavily relies on code generation which allows you to write really powerful, yet almost boilerplate free code.

The project we will build

State management is best learned on real(ish) projects. As is a bit of a tradition on Reso Coder, we're going to build a weather forecast app shown on the video below.

The foundations of the project together with the basic UI are already laid down in the starter project. It contains a Weather model class holding a city name and a temperature. There's also a fake WeatherRepository which simulates fetching the forecast over a network by simply generating a random temperature.

Adding dependencies

MobX is separated into a core, flutter-specific and a code gen package. In addition to them, we're also going to add provider to get state down the widget tree in an elegant way. Make sure to use the same package versions if you want to follow along with this tutorial.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  mobx: ^0.3.10
  flutter_mobx: ^0.3.4+1
  provider: ^3.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner:
  mobx_codegen: ^0.3.10+1

Principles of MobX

The closest match of MobX is, believe it or not, a simple ChangeNotifier which is built right into Flutter. A bunch of fields are stored inside a class called Store. The values held inside the fields, collectively known as state, are then mutated right inside the store. You can then update the UI whenever a field's value changes by observing it.

Of course, MobX stores are much more powerful than ChangeNotifiers but their principles are the same. That's unlike with BLoC or Redux which emit new states instead of mutating state in place.

Whereas you can create a hodgepodge of a code inside a ChangeNotifier, MobX brings in a bit more structure.

  • Fields which are mutated are marked with @observable.
  • You can run some logic within the store when an observable's value changes and put the result inside a property marked @computed.
  • User interface can trigger logic and change state by calling methods as usual. However, they must be annotated with @action to successfully mutate state.

This can be illustrated on a flowchart:

Yes, we haven't talked about reactions yet but we'll get there later on. Perhaps, the following picture may also be useful to get the concepts to sink in or you can also read the official docs.

Creating a WeatherStore

Reading a description of a state management solution is nice but things like this are really best demonstrated by writing code. Let's create a store responsible for storing state related to the weather forecast. Inside a new state folder, create weather_store.dart.

weather_store.dart

import 'package:mobx/mobx.dart';

part 'weather_store.g.dart';

class WeatherStore = _WeatherStore with _$WeatherStore;

abstract class _WeatherStore with Store {
  
}

This is about as much boilerplate as you get with MobX. If you're like me, you've never seen the weird syntax of setting a class equal to some thing else.  That line really means the following, only with a shortened syntax:

class WeatherStore extends _WeatherStore with _$WeatherStore {}
I prepared a VS Code snippet so that you don't have to type out the boilerplate yourself. You can get it here and then put it into the dart.json snippet file, which you can open from File -> Preferences -> User Snippets.

_$WeatherStore is a mixin generated by the library, so run the build command to kick off code gen.

terminal

flutter packages pub run build_runner watch

We need to add one dependency to the WeatherStore and that's the WeatherRepository class already present in the starter project. To allow for dependency injection, it's best to populate a field through the constructor. For this, we need to add constructors both to the package-private _WeatherStore and the public WeatherStore.

weather_store.dart

class WeatherStore extends _WeatherStore with _$WeatherStore {
  WeatherStore(WeatherRepository weatherRepository) : super(weatherRepository);
}

abstract class _WeatherStore with Store {
  final WeatherRepository _weatherRepository;

  _WeatherStore(this._weatherRepository);
}

Observable fields

Observables can be observed (?). This can happen from anywhere, for example, you can observe an observable from the UI, from a reaction or even within a store. We surely want to be able to observe a Weather instance to update the UI and also a possible error message.

weather_store.dart

abstract class _WeatherStore with Store {
  final WeatherRepository _weatherRepository;

  _WeatherStore(this._weatherRepository);

  @observable
  Weather weather;

  @observable
  String errorMessage;
}

Had we been working with synchronous code, these observables would really be all we need. Most of the time though, you work with asynchronous Futures and you probably want to display a progress indicator while the user is waiting. Let's introduce an ObservableFuture<Weather> which is a wrapper around a regular Future allowing it to be observed whenever its pendingfulfilled or rejected.

weather_store.dart

...
@observable
ObservableFuture<Weather> _weatherFuture;
@observable
Weather weather;

@observable
String errorMessage;
...

While you could replace the regular weather observable, we instead created only a complementary field that cannot be used from outside the WeatherStore, since it's private. In my opinion, it's better to create an enum to hold the current status of whether the weather forecast is currently being loaded.

weather_store.dart

enum StoreState { initial, loading, loaded }

Computed properties

The @computed annotation marks a special kind of an observable property which will be updated whenever another observable changes. This is just perfect for the StoreState enum. Naturally, it should change whenever the _weatherFuture observable's status changes. Then, our UI layer will not need to know anything about the intricacies of an ObservableFuture status but we will update the UI by observing the StoreState instance instead.

weather_store.dart

@observable
ObservableFuture<Weather> _weatherFuture;
...
@computed
StoreState get state {
  // If the user has not yet searched for a weather forecast or there has been an error
  if (_weatherFuture == null ||
      _weatherFuture.status == FutureStatus.rejected) {
    return StoreState.initial;
  }
  // Pending Future means "loading"
  // Fulfilled Future means "loaded"
  return _weatherFuture.status == FutureStatus.pending
      ? StoreState.loading
      : StoreState.loaded;
}
How does a computed property know when to compute its value anew? That's the magic of MobX! The generated code hides logic which triggers automatic observation. It's enough to "mention" an observable from within an Observer/@computed/reaction and it'll be triggered automatically when needed.

Action methods

If you're familiar with ChangeNotifier, you know you need to call notifyListeners() whenever you want to, well, notify listeners about a state change. For MobX, it's the same but instead of you manually notifying listeners, MobX does it for you when a method is annotated with @action.

Create a new method getWeather at the bottom of the store. Since this is the last bit of code we'll write inside weather_store.dart, here's its full code.

weather_store.dart

class WeatherStore extends _WeatherStore with _$WeatherStore {
  WeatherStore(WeatherRepository weatherRepository) : super(weatherRepository);
}

enum StoreState { initial, loading, loaded }

abstract class _WeatherStore with Store {
  final WeatherRepository _weatherRepository;

  _WeatherStore(this._weatherRepository);

  @observable
  ObservableFuture<Weather> _weatherFuture;
  @observable
  Weather weather;

  @observable
  String errorMessage;

  @computed
  StoreState get state {
    // If the user has not yet searched for a weather forecast or there has been an error
    if (_weatherFuture == null ||
        _weatherFuture.status == FutureStatus.rejected) {
      return StoreState.initial;
    }
    // Pending Future means "loading"
    // Fulfilled Future means "loaded"
    return _weatherFuture.status == FutureStatus.pending
        ? StoreState.loading
        : StoreState.loaded;
  }

  @action
  Future getWeather(String cityName) async {
    try {
      // Reset the possible previous error message.
      errorMessage = null;
      // Fetch weather from the repository and wrap the regular Future into an observable.
      // This _weatherFuture triggers updates to the computed state property.
      _weatherFuture =
          ObservableFuture(_weatherRepository.fetchWeather(cityName));
      // ObservableFuture extends Future - it can be awaited and exceptions will propagate as usual.
      weather = await _weatherFuture;
    } on NetworkError {
      errorMessage = "Couldn't fetch weather. Is the device online?";
    }
  }
}

User interface

When using MobX, the UI utilizes predominantly two things - an Observer widget for rebuilding the UI and reactions for running some UI related logic like showing a snack bar. First though, let's provide the WeatherStore to the WeatherSearchPage.

main.dart

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Material App',
      home: Provider(
        create: (context) => WeatherStore(FakeWeatherRepository()),
        child: WeatherSearchPage(),
      ),
    );
  }
}

Now, let's get hold of the store inside WeatherSearchPage. We'll want to store it inside a field to be able to use it inside a reaction as you'll see below.

weather_search_page.dart

class _WeatherSearchPageState extends State<WeatherSearchPage> {
  WeatherStore _weatherStore;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _weatherStore ??= Provider.of<WeatherStore>(context);
  }

  ...
}

Reaction to show a SnackBar

Reactions are a way to trigger a function whenever an observable is updated. They're used mostly in the UI to perform some side effect like showing a snack bar or an alert dialog. While there are multiple kinds of reactions, we will use the one simply called reaction.

As soon we get hold of a WeatherStore instance inside didChangeDependencies(), we'll create a reaction which will grant us a ReactionDisposer which we store inside a field _disposers. Then, don't forget to actually dispose of the reactions inside the State's overriden dispose() method.

Lastly, add _scaffoldKey to the actual Scaffold in the build method.

weather_search_page.dart

class _WeatherSearchPageState extends State<WeatherSearchPage> {
  WeatherStore _weatherStore;
  List<ReactionDisposer> _disposers;
  // For showing a SnackBar
  GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _weatherStore ??= Provider.of<WeatherStore>(context);
    _disposers ??= [
      reaction(
        // Tell the reaction which observable to observe
        (_) => _weatherStore.errorMessage,
        // Run some logic with the content of the observed field
        (String message) {
          _scaffoldKey.currentState.showSnackBar(
            SnackBar(
              content: Text(message),
            ),
          );
        },
      ),
    ];
  }

  @override
  void dispose() {
    _disposers.forEach((d) => d());
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(
        title: Text("Weather Search"),
      ),
      ...
    );
  }
...
}

Observer to rebuild a part of the UI

There's really not much to say about the Observer widget. It returns a Widget and runs whenever an observable "mentioned" inside of it is updated.

We'll observe the computed property state. When the state is StoreState.loaded, we know the weather observable is populated with the latest gotten forecast.

weather_search_page.dart

@override
Widget build(BuildContext context) {
  return Scaffold(
    key: _scaffoldKey,
    appBar: AppBar(
      title: Text("Weather Search"),
    ),
    body: Container(
      padding: EdgeInsets.symmetric(vertical: 16),
      alignment: Alignment.center,
      child: Observer(
        builder: (_) {
          switch (_weatherStore.state) {
            case StoreState.initial:
              return buildInitialInput();
            case StoreState.loading:
              return buildLoading();
            case StoreState.loaded:
              return buildColumnWithData(_weatherStore.weather);
          }
        },
      ),
    ),
  );
}

Lastly, we need to call an action when a new city name is submitted. From the outside, you really can't tell you're not calling just a regular method.

weather_search_page.dart

class CityInputField extends StatelessWidget {
  ...

  void submitCityName(BuildContext context, String cityName) {
    final weatherStore = Provider.of<WeatherStore>(context);
    weatherStore.getWeather(cityName);
  }
}

And with this, you have just built an app using MobX for state management. Everybody has different tastes and for me, I lean more toward the clean, unidirectional, state machine-like BLoC. However, if you want something with less boilerplate and you also want a step up from ChangeNotifier, then MobX may very well be your state management package of choice.

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a freelancer and most importantly developer educator, he is set on helping other people succeed in their Flutter app development career.

You may also like

  • Matt thanks for this tutorial.
    I updated provider to v4, then
    final weatherStore = Provider.of(context);
    not returning

  • Very good your tutorial. Do you intend to write a tutorial with MobX and TDD? I would love to see a complete example with ObservableList and ObservableFuture and unit tests.

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >