An Easy and Effective State Management Solution for Flutter to achieve 100 % separation of concerns

MELLATI Meftah
Oct 21 · 8 min read

Every programmer wishes to achieve a complete separation between the hidden business logic and the visual user interface (UI). Complete separation means that the logic classes don’t know and don’t care about which UI framework is using them. Even for notification, logic classes don’t notify UI views about any change in their state. It’s the responsibility of the UI views to track any change in the logic state and update themselves. How to manage the state in this kind of situation? knowing that business classes can have Futures and UI classes need to be notified when Futures complete?

This article addresses this question by introducing the new version of the States_rebuilder package in which the responsibility of notifying changes is moved from the model classes to the user interface views, thus, leaving the logical classes simple, modular, testable, maintainable and portable.

Counter App:

To see how things work differently with states_rebuilder, let’s start with the simplest app in the world; The Counter App.

This is the model class:

There is no simpler than this; No inheritance, no notification (as in provider and similar packages), no streams (as in bloc pattern), no annotation and code generation (as in mobX). The model is a simple dart class that is totally separated from the UI framework (Flutter in our case). No matter how big and complex the class model is, in the states_rebuilder solution, it is only a simple dart class.

How will be the UI part? Will it be as simple as the model is? How it can be notified when the counter value changes? Let’s see.

Here is the UI. See if you can understand it! I will explain it!

After important states_rebuilder dependency:

The UI consists of simple flutter widgets. It is only the lines indicated by arrows that make the difference and make what looks like stateless widgets to be reactive.

(1) — Meet the Injector widget: The Injector is a widget that makes models available to its child widgets that need it. The inject parameter of the Injector takes a list of Inject classes. The Inject constructor takes what is called the creation function which is of the type T function() . You can inject (or provide) many models at the same time; for example:

Injector(
inject: [
Inject<Counter>(() => Counter()),
Inject<HomeService>(()=>HomeService()),
Inject<LoginService>(()=>LoginService),
],
builder: (_, __) { ........
.....
.....

Child widgets that need to access one of the injected (provided) model uses the static method Injector.getAsModel<T>().

If you have noticed in the UI code above, Injector.getAsModel is called without the context (line 15) and with the context in (line 32). context in states_rebuilder is optional. You use context with widgets you want to rebuild when the state of the model changed. On line 15, we get the model without context because we didn’t want the App widget to rebuild when the state of the model changes. In contrast, we want the MyHome widget to rebuild after incrementing the count and for this reason, the context is provided on line 32.

If you want any child widget to listen to changes of a provided model, get the model with the context parameter.
Injector.getAsModel<Counter>(context:context);

if you want only to get the model without listening to changes, get it without the context parameter
Injector.getAsModel<Counter>();

(2) — To notify subscribed widgets after any change in the state of a particular injected model use setState method:

onPressed: () => counter.setState((Counter state) => state.increment())

(3) — To get the state of any injected model, use the getter state:

Text("${counter.state.count}"),

This how a reactive counter app looks like. And there is even more!

Future counter app:

It is very common to deal with Futures in your app, such as when fetching data from another service or database. In these cases, we want our app to rebuild when the future completes reproducing the newly received state.

This is an example:

I changed the increment method to be asynchronous and wait for one second before incrementing the count. Again, nothing special; it is a simple dart class.

For the UI part, you change nothing and believe it or not it will work. After tapping on the FAB button, the counter changes after one second.

To complicate the situation a little bit, I want to show a circular progress bar while waiting for the future.

This is the model:

Here is the UI part. Changes from the simple counter app are highlighted:

In the onPressed method, I used the to: first, set the isActive to false and then call the increment method.

This is the GIF of the app:

Update:

After posting the article, I noticed that I added the isActive boolean variable to help in the conditional display of CircularPrograssIndicator. This can be considered to be a violation of the separation of concerns’ principle because the utility of the isActive variable is in the UI rather than in the model. So let’s remove the isActive variable from the model, and let’s see how to track the future, and show the CircularPrograssIndicator accordingly:
The model returns back to its simplest form:

The model is written for the business logic, not to serve the UI!.

The UI becomes:

I removed the cascade operator and use setSate for incrementing the counter as in the first example of the simple counter.

The second part is what does the job. The counter has a snapshot of type AsyncSnapshot<counter>. It holds the connection state :(waiting: for future; done: after future completes and, active: before first calling the future); similar to what we found in StreamBuilder widget. I took advantage of this property and display the CircularPrograssIndicator while waiting for the Future to complete. Yet another level of separation of concerns!

Stream and future injection:

With states_rebuilder you can inject stream<T> and/or future<T> and exposes a snapshot<T> to the child widget tree.

(1) — Named constructor is used to inject streams and futures. In the future case, an initial value is defined.

(2) — To get the emitted value we use
* Injector.getAsModel<int>().snapshot for the stream because the stream emits an int value.
* Injector.getAsModel<bool>().snapshot
for future because the future completes with a bool value.

(3) — The emitted value is of type AsyncSnapshot<T> from which you can get data or error and check if it has data or error

It is important to know that streams are automatically unsubscribed when the Injector widget is removed from the widget tree.

The resultant GIF is:

Remarks:

1- To make the distinction between two provided values with the same type, you can use the name parameter in the Inject constructor.

to get the injected value with a name, you use:

You can use the enumeration to name injected values:

2- To get the injected model, we used Injector.getAsModel<T>(). The return type of Injector.getAsModel<T>() is ModelStatesRebuilder<T>. This is useful when we want to make the model reactive. Reactive here means that we can register widgets using the context and get model state and mutate it using the state getter and stateState method as shown in the examples above.

In case we want to get the model without reactivity, we can simply use Injector.get<T>(). The return type of Injector.get<T>() is T.

3- Injected models are injected lazily. This means that model classes are not instantiated and Future and Stream are not started until the first use. To instantiate an injected model at the time of registration, set the parameter isLazy of the Inject contractor to false.

BloC pattern:

If you like working with streams and rxDart library, you can use it with states_rebuilder to simplify the UI part of your app.

Here is an example of a login screen that has two text fields; one for the user email and the other for the user password, and one submit button. The submit button is disabled until the two text fields are validated.
NB: The example is inspired from this .

Here is the model:

If you are used to the BloC pattern, you see that there is nothing special in the LoginFromModel class.

The UI part is:

Here

(line 7)__ Injecting the LoginFromModel class
(line 9–10) —
* getting the injected instance of LoginFromModel using the get method and not the getAsModel.
*
injecting the email stream with the custom the name of “emailStream”
(lines 13–14) —
* getting the injected instance of LoginFromModel using the get method and not the getAsModel.
*
injecting the password stream with the custom the name of “passwordStream
(line 17) — because we are using StreamControllers, we have to dispose them to release resources. For this purpose, we set the disposeModels parameter to true. The dispose method of the injected model (if they have one) will be called when the Injector widget is removed from the widget tree.
(line 19 — ) getting the injected instance of LoginFromModel using the get method because we do not want the LoginFromModel to be reactive.
(lines 20 -24) — getting the snapshot of the streams using getAsModel with the context to register it so when the stream emits a value the widget will rebuild.
The remaining part of the code is easy to understand.

This is the GIF of the resulting APP:

This is the end of this part. states_rebuiler offers more features that I will talk about in the next post.

To get the code, it’s in the example folder.

Thanks for your time.

Hope you see the simplicity of the solution and like it, if yes clap and share

Flutter Community

Articles and Stories from the Flutter Community

MELLATI Meftah

Written by

@MELLATIM

Flutter Community

Articles and Stories from the Flutter Community

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade