State Management Gymnastics using states_rebuilder. Part 2.

MELLATI Fatah
Flutter Community
Published in
10 min readJan 3, 2020

This is the second part of a series of articles that aims to show the functionality of the states_rebuilder library by creating an application that has quite complex state management requirements.

It is mandatory to read the first article before continuing reading this article (link here).

What makes states_rebuilder different:

Write your business logic using pure dart classes and reactivity is implicitly added for you, in a way that gives you full control over which part of the widget tree to rebuild and when to notify it.

1. With states_rebuilder you can achieve a clear separation between UI and business logic;

2. Your business logic is made up of pure dart classes without the need to refer to external packages or frameworks (NO extension, NO notification, NO annotation);

class Foo {
//Vanilla dart class
//NO inheritance form external libraries
//NO notification
//No annotation
}

3. You make a singleton of your logical class available to the widget tree by injecting it using the Injector widget.

Injector(
inject : [Inject(()=>Foo())]
builder : (context) => MyChildWidget()
)

4. From any child widget of the Injector widget, you can get the registered raw singleton using the static method Injector.get<T>() method;

final Foo foo = Injector.get<Foo>();

5. To get the registered singleton wrapped with a reactive environment, you use the static method Injectro.getAsReactive<T>() method:

final ReactiveModel<Foo> foo = Injector.getAsReactive<Foo>();

In fact, for each injected model, states_rebuilder registers two singletons:
- The raw singleton of the model
- The reactive singleton of the model which is the raw singleton wrapped with a reactive environment:

The reactive environment adds getters, fields, and methods to modify the state, follow the state of the reactive environment and notify the widgets which are subscribed to it.

In the last article we saw the following getters: state, connectionState, hasError, error and the setState method.

6. The setState method is where actions that mutate the state and send notifications are defined:

What happens is that from the user interface, we use the setState method to mutate the state and notify subscribed widgets after the state mutation. In the setState, we can define a callback for all the side effects to be executed after the state change and just before rebuilding subscribed widgets using onSetState parameter(or onRebuild so that the code executes after the reconstruction). From inside onSetState, we can call another setState to mutate the state and notify the user interface with another call onSetState (onRebuild) and so on …

The following picture sketches the trigger-mutate-notify-side effects process:

In the last article, we started from this pure dart class :

and built this simple counter app that shows a CircularProgressIndicator while waiting for an asynchronous task to finished and increment a counter with the possibility of throwing an error.

In this article, we want to start from the same FutureWithCounter model and create an application with tricky state management requirements:

The UI of the app to build
  1. The app has Scaffold with an AppBar and its body contains four quarters (counters); each within its own Scaffold.
  2. We want each quarter (counter) to be independent (although they use the same instance of the FutureWithCounter model).
  3. If a quarter (counter) has an error, we want its own AppBar to be red and we want to show a SnackBar containing the error message (counter 2 in the above picture);
  4. We want the app’s AppBar to display the name of the counter that was tapped with its status (CircularProgressIndicator if waiting);
  5. We want the app’s AppBar to display a red error message if at least one counter has an error.
  6. If all counters are incremented and none of them has an error, we want the app’s AppBar to display a message successful message.

Here is the final GIF we want to get:

I want to emphasize that these state management requirements must be met using a simple pure dart model:

The FutureCounterWithError is the model, BloC, service or whatever you want to call. It is a vanilla dart class without any framework or library dependence.

As mentioned in the last article, any state management library must first provide (or inject) a single instance of the model in the widget tree and make it accessible to child widgets.

NOTE 1: We inject the FutureCounterWithError class in the constructor of the Inject class.
NOTE 2: We get the reactive singleton of the injected model without the context parameter because we don’t want to subscribe the BuildContext to the reactive model.
NOTE 3: CounterWidget is a custom Widget that we defined to be reusable :

As required by the state management requirements, we want the local AppBar for each counter to be red if the model has an error. For this reason, we wrapped the Theme widget with a StateBuilder widget.

NOTE 1: We used the StateBuilder widget to subscribe to the counterModel. This StateBuilder is subscribed with two tags so that each time the counterModel emits a notification with at least one of these tags, this StateBuilder will be notified to rebuild.

NOTE 2: Because we want to change the theme without rebuilding the entire scaffold, we use the builderWithChild parameter instead of the builder parameter. What is inside the builderWithChild will be rebuilt and what is held with the child parameter will not rebuild.

with states_rebuilder, you have to define either builder or builderWithChilde parameter:

StateBuilder<T>(
builder : (BuildContext context, ReactiveModel<T> model){
return WidgetToRebuild()
}
)
//OR
StateBuilder<T>(
builderWithChild : (BuildContext context, ReactiveModel<T> model, Widget child){
return WidgetToRebuild(child: child)
}
child: WidgetNotToRebuild()
)

NOTE 3: call setState Method to mutate the state and notify subscribed widgets with the provided tagFilter list.

NOTE 4: define the onError callback that will be called whenever a notification is sent with an error.

onError(BuildContext context, dynamic error){
// side effect code
}

onError is a shortcut to :

catchError : true;
onSetState(BuildContext context){
if (reactiveModel.hasError){
//side Effect code
}
}

NOTE 5: CounterApp is a widget that displays the value of the counters. If you followed the first part, you won’t find anything special:

This is the resultant GIF:

َAs you see in the GIF, this is not the behavior we want. Each time the state of a counter changes, the state of all other counters changes. Also, note the snackBar, it appears in counter 4 while the error comes from counter 1 and it is far from what the state management requirements impose.

In fact, this behavior is expected because the four counters share the same reactive environment. If one of them changes the status of the reactive environment, the other counters will be affected because they are surrounded by the same reactive environment.

To fill this gap, we must ensure that each counter has an independent reactive environment.

As we said, states_rebuilder caches two singletons (the raw singleton and the reactive singleton). Besides these two singletons, with states_rebuilder, you can create as many new reactive environments as you want.

New reactive instance is the same raw singleton decorated with a new reactive environment

The following picture shows how reactive instances interact:

The reactive singleton and reactive instances share the same raw singleton of the model. Each of them can invoke its own setState, change its own state and notify widgets that have already subscribed to it.

The idea is simple, you use pure dart classes and states_rebuilder adds reactivity by decorating them with a reactive environment. For each model, you can get many reactive instances.

To create a new reactive instance of an injected model use:

1- Injector.getAsReactive with the parameter asNewReactiveInstance equals true

ReactiveModel<T> model = Injector.getAsReactive<T>(asNewReactiveInstance: true);

2- StateBuilder with generic type and without models property.

StateBuilder<T>(
builder:(BuildContext context, ReactiveModel<T> newReactiveModel){
return YourWidget();
}
)

Let’s refactor the App class with the second approach:

I removed the Injector.getAsReactive() line because we do not need it [NOTE1], and I wrapped each CounterWidget with StateBuilder without the models parameter and with generic type FutureCounterWithError [NOTE2].

That all we to do to clean the reactive environment. The new GIF is :

At this stage, we have met the first, second and third requirements. We still have the last three requirements for the Appbar of the application:

4. We want the application AppBar to display the tapped quarter (counter) name with its status (waiting);
5. We want the application AppBar to display a red error message if at least one counter has and error.
6. If all counter are tried and none of them has an error, we want the application AppBar to display a message successful message.

To achieve this, I will introduce another concept of states_rebuilder: ‘The sharing of data and status between a reactive singleton and new reactive instances’.

With the exception of the raw singleton they share, the reactive singleton and the new reactive instances have an independent reactive environment; BUT:

New reactive instances can send data to the reactive singleton and share their state status with it.

If a new reactive instance changes its state and notifies its listeners, we can set it up to share its new state with the reactive singleton and notify the reactive singleton listeners. This can be done in two ways.

1. The reactive singleton will have the state status of the new reactive instance issuing the notification.

Injector(
inject: [
Inject(() => FutureCounterWithError(),
joinSingleton: JoinSingleton.withNewReactiveInstance,
)
],
builder: (BuildContext context) {
//
}
)

2. The reactive singleton will have a combined state status of all the new reactive instances.

Injector(
inject: [
Inject(() => FutureCounterWithError(),
joinSingleton: JoinSingleton.withCombinedReactiveInstances,
)
],
builder: (BuildContext context) {
//
}
)

In the language of states_rebuilder, we say that we join the reactive singleton with the new reactive instances. The place to define the wiring (joining) is in the Inject class where the model is injected by setting the joinSingleton parameter.

The combined state status has a similar meaning to that of RxDart for combined streams. The reactive singleton state:
1. has an error if at least one of the new reactive instance has error;
2. is waiting if at least one of the new reactive instances is waiting;
3. is idle if at least one of the new reactive instances is idle;
4. has data if all reactive instances have data.

In addition, a new reactive instance can send custom data to the reactive singleton with the notification. This can be done in the setState method

//From a new reactive instance
counterModel.setState(
(state) => state.increment(seconds: 1),
filterTags: ['blueCounter'],
joinSingletonToNewData: "Date to send to reactive singleton",
)

joinSingletonToNewData takes any type of date.

For its part, the reactive singleton receives data via its joinSingletonToNewData field

//From the reactive singletongprint(counterModelSingleton.joinSingletonToNewData);

Let’s apply these concepts and fulfill the remaining state management requirements

NOTE 1 : define joinSingleton to be JoinSingleton.withCombinedReactiveInstances.

NOTE 3: Subscribe the reactive singleton with ‘blueCounter’ and ‘greenCounter’ tags using StateBuilder widget. Whenever the reactive singleton emits a notification with one of these tags this StateBuilder will rebuild.

NOTE 4: Check the status of the reactive singleton and display the corresponding widgets. The reactive singleton holds a combined state of all the new reactive instances as defined above.

NOTE 5: Display the message sent by the new reactive instance which has just issued the notification. The data sent is kept in the joinSingletonToNewData fields.

Custom data is emitted from new reactive instances as defined in joinSingletonToNewData parameter of the setState callback:

This is what state_rebuilder offers you. You write your business logic with pure dart classes without losing control of which part of your widget tree to rebuild and when to rebuild it. Even better with the concept of reactive environments, you can perform very complicated state management tasks that can never be easily done with explicit reactivity as used in ChangeNotifier.

To see the combined reactive instances in action, see this example of a login form with validation and compare it with the way it is done using rxDart and combined streams. (link).

For the code source of this article check this link.

For a complete list of tutorials visite this link.

Try the states_rebuilder library, give your opinion, report issues and any contribution is welcome.

In the next article, I’ll show you how states_rebuilder makes clean architecture even cleaner.

--

--