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 ChangeNotifier
s 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 {}
_$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 Future
s 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 pending, fulfilled 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;
}
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.
Matt thanks for this tutorial.
I updated provider to v4, then
final weatherStore = Provider.of(context);
not returning
final weatherStore = Provider.of(context, listen: false);
fix it
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.
Thanks for this tutorial.
But I would like to know how can I create test for StoreState?
I don’t think the title of your article matches the content lol. Just kidding, mainly because I had some doubts after reading the article.
I don’t think the title of your article matches the content lol. Just kidding, mainly because I had some doubts after reading the article.
Your point of view caught my eye and was very interesting. Thanks. I have a question for you.
Your point of view caught my eye and was very interesting. Thanks. I have a question for you.
Thank you for your sharing. I am worried that I lack creative ideas. It is your article that makes me full of hope. Thank you. But, I have a question, can you help me? https://www.binance.com/en-IN/register?ref=UM6SMJM3
Can you be more specific about the content of your article? After reading it, I still have some doubts. Hope you can help me.