Organizing Stores

The primary purpose of MobX is to simplify the management of Reactive State in your application. As your application scales, the amount of state you manage will also increase. This requires some techniques to break down your application state and divvy it up across a set of stores. Of course, putting everything in one Store is not prudent, so we apply divide-and-conquer instead.

General approach to managing Stores

When you look at the UI on the screen, all you see is a flattened graphic. But what escapes your eye is the Object Model that brought it to life on the screen. That Object Model is a tree of Widgets, a hierarchy. Just like the UI, think of the Reactive State of your application as a hierarchy of Stores.

When you are starting out, a single store is enough to get going. As it grows in size, you will see that it is accumulating a lot of additional fields and actions that don't all belong together. It is then time to break it down and move the related observable state and actions to its own Store. For example, consider the store below:

abstract class _MainStore with Store {
@observable
String title;
@observable
String name;
@observable
String email;
@observable
String phone;
@action
void setTitle(String value) => title = value;
@action
void setName(String value) => name = value;
@action
void setEmail(String value) => email = value;
@action
void setPhone(String value) => phone = value;
}

The above Store is for an app that has the home page that shows the title. There is a form on the second page that accepts the name, email and phone values. Even though it's easier to keep the fields together, it's a good idea to move the form fields to its own Store. That forms a nice conceptual boundary and helps in breaking the MainStore down.

Think in terms of conceptual boundaries for your stores, aka Cohesiveness.

Let's create a sub-store called PersonDetails and move the fields into it:

class PersonDetails = _PersonDetails with _$PersonDetails;
abstract class _PersonDetails with Store {
@observable
String name;
@observable
String email;
@observable
String phone;
@action
void setName(String value) => name = value;
@action
void setEmail(String value) => email = value;
@action
void setPhone(String value) => phone = value;
}

The _MainStore now looks much simpler to read:

abstract class _MainStore with Store {
@observable
String title;
final details = PersonDetails();
@action
void setTitle(String value) => title = value;
}

Note that we are not making the details field into an @observable as its value is not going to change. The reactive-state that matters is inside the PersonDetails, which is already marked @observbale.

Store co-ordination

Stores should be made as independent as possible with all of their inputs clearly defined. All their dependencies should be fed in via constructor parameters or through fields. Any external communication that needs to happen from the Store should be done via callbacks. Adopting this approach improves the portability and also simplifies testing.

The triad of Widget - Store - Service

The responsibilities of State management can be clearly demarcated across three entities:

  1. Widget: this paints the visual representation of the reactive-state
  2. Store: holds the reactive state of the application
  3. Service: performs work, which can result in a change to the state. Typically this abstracts the CRUD operations to an API or hides details of any stateless worker process.

The dependency order is always top-down from the Widget, through the Store, down to the Service.

This breakdown has a few advantages:

  • The Widget layer is primarily stateless and contains a good dose of Observer widgets sprinkled wherever the reactive-state is being rendered. You can be as generous as you need to be to with the Observers in selectively observing the state.
  • The Store layer is primarily composed of the @observable and @computed fields. Additionally, you will have the @action methods that mutate the observable state. This layer is primarily meant to add the reactivity you need to render the UI. There is no actual work being performed here besides keeping track of the reactive-state.
  • The Service layer is where you do all the heavy lifting with data. This will include API calls to fetch and send data, format according to the needs of the UI, do data transformations, apply rule-based validation and error checking and so on. All the inputs needed are clearly sent in, making this layer completely stateless!

In short, we have taken cues from the SOLID principles and applied them here :-).

Single Responsibility is the most important attribute of this triad pattern.

Linking Stores

If you organize stores in a hierarchy, you will have a root-Store and then a set of child-Stores that cater to specific features of your application. If the feature is complex, it is possible to have a subtree of Stores. In such a scenario, you will want some kind of communication from the parent to the child stores. There are two common ways of doing this:

  1. Pass the Parent into the Child: This way you have direct access to the public interface of the Parent and you can use that to observe certain fields or just invoke parent level functionality for certain operations. It is better to pass the parent into the Child's constructor, rather than setting it as a field.
// parent.dart
class Parent {
Parent(){
child = Child(parent: this);
}
Child child;
}
// child.dart
class Child {
Child({this.parent});
Parent parent;
void dispose() {
parent = null;
}
}

Service Locator

If the dependencies between Stores is much more complicated, let's say each Store needs a shared instance of the ThemeStore, PreferencesStore, AuthenticationStore, etc., a better way to manage this is to use the Service Locator pattern. The pub package makes this possible and is worth checking out.

  1. Using callbacks from Child: Rather than tightly coupling the Parent with the Child, the other option is to have an explicit set of callbacks for the child to communicate with the parent. This is useful when the direction of communication is mostly from Child to Parent.
class Parent {
/* ... */
void addChild() {
final child = Child();
child.onChange = (value) {
print(value);
};
}
}
class Child {
/* ... */
void Function(String) onChange;
void perform() {
if (onChange != null) {
onChange('value changed');
}
}
}

Store lifetimes

Most stores should be created before rendering the UI. This includes your core application stores, preferences, translations, themes, etc. However, there are cases where a store should exist only for the lifetime of a screen. For example, a FormWidget that only needs a store for validation and for loading some dynamic data. Once the FormWidget is disposed, the store also goes away.

For such cases, you should create the store in the initState() of the State<FormWidget> and dispose inside the State<FormWidget>'s dispose().

class FormWidget extends StatefulWidget {
const FormWidget();
@override
_FormWidgetState createState() => _FormWidgetState();
}
class _FormWidgetState extends State<FormWidget> {
FormStore store;
@override
void initState() {
super.initState();
store = FormStore();
store.setupValidations(); // setup some reactions
}
@override
void dispose() {
store.dispose(); // dispose the reactions
super.dispose();
}
@override
Widget build(BuildContext context) { /* ... */ }
}

Using Providers to avoid Singletons

The provider-pattern has become a standard way of making the top level stores available across the entire app. It avoids creating a singleton-Store, making it easier to use the Store as a plain class instance.

Using the pub package on pub, you can setup a repository of stores at the App level. Then using the Provider.of<T>() API, you can read this value inside your Widget.build() methods.

In the example below, we are setting up a store at the app-level:

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<MultiCounterStore>(builder: (_) => MultiCounterStore())
],
child: MaterialApp(
initialRoute: '/',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: {
'/': (_) => ExampleList(),
}..addEntries(
examples.map((ex) => MapEntry(ex.path, ex.widgetBuilder))),
),
);
}
}

Then inside a nested widget, deep inside, you can retrieve the store with the Provider.of<T>(BuildContext context) API. You will do this in the build method of the Widget, where the context is available.

class CounterListPage extends StatelessWidget {
const CounterListPage();
@override
Widget build(BuildContext context) {
final store = Provider.of<MultiCounterStore>(context);
return Observer(
builder: (_) => Column(children: <Widget>[
RaisedButton(
onPressed: store.addCounter,
child: const Text('Add Counter'),
),
]));
}
}

Using ProxyProvider with a service

For handling when a store depends on a service, the ProxyProvider API can be used

class MyApp extends StatelessWidget {
const MyApp(this._sharedPreferences);
final SharedPreferences _sharedPreferences;
@override
Widget build(BuildContext context) => MultiProvider(
providers: [
Provider<PreferencesService>(
builder: (_) => PreferencesService(_sharedPreferences),
),
ProxyProvider<PreferencesService, SettingsStore>(
builder: (_, preferencesService, __) =>
SettingsStore(preferencesService)),
],
child: Consumer<SettingsStore>(
builder: (_, store, __) => Observer(
builder: (_) => MaterialApp(
initialRoute: '/',
theme: ThemeData(
primarySwatch: Colors.blue,
brightness:
store.useDarkMode ? Brightness.dark : Brightness.light,
),
routes: {
'/': (_) => ExampleList(),
}..addEntries(
examples.map((ex) => MapEntry(ex.path, ex.widgetBuilder))),
),
),
));
}

Here, the ProxyProvider API allows us to declare that SettingsStore depends on an instance of the PreferencesService class and will ensure the objects are constructed in the appropriate order based on the chain of dependencies. Note that in this particular example, we have used the Consumer widget that is an alternative to using Provider.of<T> for retrieving a value. Whilst this adds an additional level of indentation, some may prefer this approach as it's declarative and assists in building a mental model where a value provided so that can be consumed further down in the widget tree. Furthermore, it facilitates in writing code where dependencies can be more explicit. The following is a widget that depends on the SettingsStore

class SettingsExample extends StatelessWidget {
const SettingsExample(this.store);
final SettingsStore store;
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Settings'),
),
body: Observer(
builder: (context) => SwitchListTile(
value: store.useDarkMode,
title: const Text('Use dark mode'),
onChanged: (value) {
store.setDarkMode(value: value);
},
),
));
}

Using a non-default constructor here allows us to explicitly declare that the SettingsExample class depends on the SettingsStore for it to function. Some developers may already be familiar with structuring code this way when working with applications/frameworks that support constructor injection. An instance of the SettingsStore is passed through the Consumer widget as per the example code below

...
Example(
title: 'Settings',
description: 'Settings for toggling dark mode',
path: '/settings',
widgetBuilder: (_) => Consumer<SettingsStore>(
builder: (_, store, __) => SettingsExample(store),
),
)
...

A side effect of this is that writing widget tests become easier as well. Since the widget's dependencies have been made explicit, we could pass mocks (e.g. using the pub package) of said dependencies if required.

A variety of providers

There are multiple types of Providers available in the pub package. The ones most relevant and useful to MobX are:

  • Provider for constructing and disposing objects on the fly. This is good when the object has a fixed lifetime.
  • Provider.value, a Provider constructor for single values, known before hand
  • ProxyProvider for constructing objects that depend on other objects e.g. a store depending on a service
  • MultiProvider for passing multiple values