Flutter State Management Gymnastics Show using states_rebuilder. Part 1.

MELLATI Fatah
Flutter Community
Published in
12 min readDec 15, 2019

Flutter state management solutions seek to achieve the same goals, but each follows its own path. The ultimate goal is to separate the business logic from the user interface logic.

The set of business logic objects is what is called the state. The user interface in Flutter is created by building a tree of nested widgets. The widget tree reproduces a visual representation of the state, and the state is the ultimate source of truth for the widget tree.

To achieve such a separation, state management solutions must find a way to:

1- Inform the widget tree of any state changes so that it rebuilds to reflect the new state. This is called notifying the widgets.

2- Make a single instance of the state available to the widget tree and make it accessible to any widget, regardless of how deep it is in the widget tree. This is called providing or injecting the state.

One of the most popular packages for managing the state in Flutter is the provider package. provider uses the observer pattern to notify the widget tree and relies on InheritedWidget to provide the state to the widget tree and rebuild the listening widgets. State (model) objects are the observable and InheritedWidgets are the observer. The latter register in an observable class and when the state is modified, the observable class notifies the observer InheritedWidgets that transmit the notification to their listening child widgets. Notification is made explicitly by calling notifyListener() method.

Another popular package is flutter_bloc that uses streams to notify the widget tree and InheritedWidget to provide the state (BloCs). In flutter_bloc, you must guess all possible states of your widget and all events that will change these states. Once the state sare mapped to the events, the widgets add events to the BloC, which in turn yields states to the widgets subscribed to the BloC stream and rebuilds them with the new state.

flutter_bloc uses InheritedWidget only to provide the BloC, while provider uses it for notification and providing.

states_rebuiler is yet another state management technique. It uses the observer pattern for notification and the service locator pattern to provide the state; this is done in a rather particular way.

Notification with states_rebuilder is special because it can be done implicitly and can be filtered, that is, there is no need to explicitly use a package-specific or flutter-specific method to notify widgets. Your logic, your models, the BloCs — name them as you wish — are Vanilla Dart classes regardless of their complexity.

In turn, providing a state with states_rebuilder is very specific. Although it uses the service locator pattern, it takes into account the life cycle of the widgets. This means that the models are added to the global container in the initState() method of the Injector widget (a StatefullWidget type) and removed in the dispose() method. This is very useful if you want to clean the models after they are destroyed. Using the service locator makes the process of getting an injected model independent of the BuildContext availability. This facilitates the management of the dependency injection in the application.

What is the added value of states_rebuilder?

states_rebuilder differs from other state management solutions by the following facts:

1. Business logic (bloc) is made 100% of simple dart classes.
2. Bloc’s methods can return any type (void, primitive, object, future or stream).
3. Refactor any method to return any type without affecting the user interface code.
4. Use immutable or mutable objects or mix them.
5. Refactor from mutability to immutability (or from immutability to mutability) without changing a single line in the UI code.
6. Easily mange side effects (onSetState, onData, onError and onRenuildState).
7. Inject dependencies asynchronously. (Even flavors can be injected asynchronously).

A line of code is worth a thousand words

In what follows, I will explain the concepts of states_rebuilder by building an application that will handle complex and tricky state management requirements. The views will be very basic, so as not to overload them with code not related to state management.

Although state_rebuilder models are pure Dart classes, you have complete control over when to send a notification and over which widgets the notification will be sent to.

NB: I assume basic knowledge of Dart and Flutter, so I will only explain what relates to state management.

Sénario one: Future counter with error:

This is a pure Dart class, which increments a counter after a second of waiting and throws an error at random. The error is a class defined by the user.

And what is a real application rather than dealing with synchronous and asynchronous methods and handling errors?

As mentioned above, the first thing to do is to make a single instance of the model object accessible throughout the widget tree. In other words, we have to feed, provide or inject the model into the widget tree. And it’s the Inject term that’s chosen in the states_rebuilder library.

The role of the Injector widget is to create a single instance of each model defined with the inject parameter and expose it to its child widgets so that it can be accessed from any child widget.

To get an instance of an injected model, we use the static method ‘Injector.get’:

final Foo foo = Injector.get<Foo>();//starting from 2.0.0 updatefinal Foo foo = IN.get<Foo>();

In the case where Dart can infer the type of the model, the generic type is optional.

final Foo foo = Injector.get();

Injected models are accessible even within the inject parameter. For example, if we want to inject two dependent classes, we can do it very simply like this:

The order of the injection is not mandatory.

Injector uses the type to get the injected instance. If you want to inject two instances of the same type, you can use the name parameter:

Injector offers you a set of practical methods to help you manage the widget’s life cycle as well as the application:

NB: Inject has three other parameters that we will see in this article and the following ones

Get with Reactivity:

Getting (or consuming) the model injected with the Injector.get method returns the registered singleton of the model.

For the injected model to be reactive, that is, widgets can register as listeners in the model, and the model can notify registered widgets, you must get the singleton wrapped in a reactive environment. To do so, use Injector.getAsReactive method :

final ReactiveModel<Foo> foo = Injector.getAsReactive<Foo>();//starting from 2.0.0 update
final ReactiveModel<Foo> foo = RM.get<Foo>();

Notice that the return type is ReactiveModel<Foo>. The following picture shows the difference between Injector.get and Injector.getAsReactive methods:

As you can see, Injector.get returns the raw registred singleton of the Foo class and the Injector.getAsReactive returns the same raw singleton decorated with a reactive environment. The reactive environment adds the following getters and methods:

The getters are :

state: returns the raw singleton model.

connectionState : It is of type ConnectionState (a Flutter defined enumeration). It takes three values:
1- ConnectionState.none: Before executing any method of the model. (Vergin model before calling any of its methods or mutating its state).
2- ConnectionState.waiting: While waiting for the end of an asynchronous task.
3- ConnectionState.done: After running a synchronous method or the end of a pending asynchronous task.

hasError: It’s of the bool type. it is true if the asynchronous task ends with an error.

error: Is of type dynamic. It holds the thrown error.

hasData: It is of type bool. It is true if the connectionState is done without any error.

customStateStatus: It is of type dynamic. It holds your custom-defined state status. For example, in a timer app, you can define custom states such as ‘plying’, ‘paused, ‘finished’. (This will be the subject of the third part of this article).

subscription: it is of type StreamSubscription<T>. It is not null if you inject streams using Inject.stream constructor. It is used to control the injected stream. (This will be the subject of the third part of this article).

and one method:

setState(T state): return a Future<void>. It takes the state as a parameter that corresponds to the singleton instance of the injected model. It is used to mutate the state and notify listeners after state mutation.

Let’s continue building the UI and see some of the reactive environment getters and method in action:

Notice that Injector.getAsReactive is invoked without any parameters. This is an important point and we will come back to it very soon.

The result of Injector.getAsReactive is a reactive singleton of the injected model.

states_rebuilder caches two singletons; the first is the raw singleton model (obtained by the Injector.get method) and and the second is the reactive singleton that wraps the raw singleton (obtained by Injector.getAsReactive method).

Any action or event that changes the state and notifies the listening widgets must be called inside the ReactiveModel.setState method. And that’s exactly what’s happening in the onPressed callback of FAB.

The state management wheel:

The following picture shows the cycle of state management

First, the state is your singleton instance of the model. From the user interface, you trigger an event or action that changes the state using the setState method. After state mutation, the UI will be notified to rebuild itself to reproduce the new state.

onPressed: (){
counterModel.setState(FutureCounterWithError state){
return state.increment();
}
}

Notice that I returned the result of state.increment() because states_rebuilder uses the return type to check for asynchronous methods. (the use of the arrow function is very convenient).

Often, we want to trigger side effects events after mutating the state.

side effects are what is not related to the UI build. Such as printing something in the console, showing snackbar and dialogs, opening drawer, calling external api.

That’s what we want in our demo application. We want to display an error dialog box that displays the thrown error.

First of all, we set the catchError property to true because we are certain that the method can end with an error and we want to silent it so as not to break the application.

onSetState takes BuildContext as a parameter and uses it to display AlertDialog after checking if the model throws an error using the hasError getter. The getter error returns the error object and it is used to display the error message.

From onSetState, you can call another setState and change the state, then notify the user interface. And from the last setState, you can use another onSetState to run other side effects, and so on. theoretically, without limit.

onSetState is called after the state is changed and notification is sent and just before rebuilding the widget. If you want to run the side effects after the rebuild process is complete, use the onRebuildState method.

To trigger side effects after the state change and just before rebuilding the listening widgets, use onSetState.
To trigger side effects after the rebuild process is completed, use the onRebuildState method.

onSetState vs onRebuildState

Registering widgets in the model.

states_rebuilder register widgets by two means:
1- Injector.getAsReactive(context : context); with context defined. (This feature is removed from update 2.0.0)
2- StateBuilder widget. (WhenRebuilder, WhenRebuilderOR, OnSetState are other wigets to register to a reactive model add from 2.0.0 update)

Let’s continue with our code

The difference between Injector.getAsReactive() without context and Injector.getAsReactive(context: context) with context is that the latter subscribe the widget that has the context to the obtained model.

With states_rebuilder, if you want to get the reactive singleton of the model without wanting to rebuild the widget use getAsReactive() without the context. Whereas if you want the widget to be notified by setState method use getAsReactive(context: context) with the context.

NOTE: Subscription with context feature is deprecated and removed from update 2.0.0

The remainder of the code is very simple to understand:

With a series of if statement blocks, we specify what to display in each case:

First, if connectionState is none, that is, before pressing FAB, we display a text to tell the user what to do.

Second, once the connectionState is waiting, we display a CircularPrgressIndicator

Third, if the connectionState is not none and not waiting, then it is done and we can safely get the counter value and display it.

NB: It happens that in certain practical situations, we want to trigger a method with and sometimes without wanting to notify the tree of the widgets. states_rebuilder gives you this choice.

In our example, If you want to notify listeners, use

counterModel.setState((state)=>state.increment());

and if you do not want to notify listeners after state mutation, use

counterModel.state.increment();

That’s it. the code of this example is available here. try it yourself.

You might say that the idea of ​​getting a raw model singleton and surrounding it with what’s called a reactive environment is very simple and works for simple models only and not for models with many independent asynchronous methods. Indeed, the environment of the reactive model is shared between all the methods of the model, if one of them throws, all listeners will be notified with the error, which will pollute the reactive environment for the other methods.

Let’s imitate this scenario with the following example:

The increment method takes an integer parameter to customize the time of waiting. Also, the error depends on the ‘seconds’ parameter.

I will use two FABs:
1- The blue one that will control the blue counter and display the blue CircularProgressIndicator for one second.
1- The green one that will control the green counter and display the green CircularProgressIndicator for two seconds.

The shared code for displaying AlertDialog is extracted in a local _showAlertDialog method.

The body of the scaffold that displays the green and blue counter is the following

Again, shared code is extracted to a local method to avoid repeating ourselves.

Whenever one of the FAB is tapped, both the blue and green counters are notified. You might say that this is very normal because both FABs when tapped, trigger the same method increment(). Can this be solved?

Yes!, and even with the same method, with states_rebuilder, you can have precise control over which part of the widget tree will be rebuilt after the setState call.

States_rebuilder gives you fine-tuned control over the widget tree. With the setState method, you can call the same method multiple times, but notifying each time different widgets of your choice.

For this purpose, states_rebuilder offers you the StateBuilder widget.

You wrap any part of the widget you want to control by the StateBuilder widget.

StateBuilder takes a list of models. Whenever one of the defined models sends a notification, the builder callback is called and what it contains will be rebuilt. The tag parameter is optional and, when set, it identifies this widget and the builder callback can be selectively called using this tag. In other words, reactive models can send a notification with a list of filter tags. Only widgets for which their tags are listed in the sent filter list will be rebuild.

tag is of dynamic type and can take any form of data. You can use enumeration instead of Strings to take advantage of your favorite IDE.

In our example, in the onPressed callback and in the setState method, we use the filterTags parameter to define a list of tags to send with the notification.

The resultant GIF is :

The code of this example is available here. try it yourself.

StatesBuilder offers many useful parameters that will discover in the next article:

This marks the end of this warm-up party. You have seen the main concepts of states_rebuider. In the next article, you’ll discover other important features that make it easy for you to manage complex and tricky state management situations.

  • Part two of the article is here.

--

--