State in layman’s term: “whatever data you need in order to rebuild your UI at any moment in time” — Flutter.dev
Leading a mobile engineering team at SoluteLabs, I often come across questions like which architecture or state management techniques we follow in the context of Flutter. These questions are the primary inspiration for me to write this article.
There are a plethora of options to manage the state of the application in Flutter. We can majorly classify them into Ephemeral State and Application State which is explained in great detail here.
As an Ephemeral State represents the single widget’s local state, so it’s very straight forward to achieve using StatefulWidget and setState() method. The real confusion starts when we want to implement the Application State as there are many ways to achieve the same.
In this article, we will try to understand the key differences of the opinionated state management options for the application state in the context of the MVVM design pattern.
A brief introduction to MVVM
Writing maintainable and clean code requires strategies like segregating responsibilities into different layers. There are various software architectural patterns that streamline the code organization into separate layers like MVC, MVP, MVVM, etc. MVVM is quite a common design pattern in writing UI based applications in general.
A Model-View-ViewModel mainly concerns these layers:
- View: Responsible for visuals like UI elements on the screen
- Model: It can be a Repository that takes care of managing data across the services like Database, Network, etc
- ViewModel: It acts as a mediator to View & Model. The key responsibilities of this layer are in-taking actions, processing data and give back the new state to the View layer.
It requires some mechanism to communicate between View (Widget) and ViewModel.
The state management techniques try to provide this communication mechanism in different ways.
ChangeNotifier is the built-in class that provides the notifyListeners() method to inform the changes to the listeners. The ViewModel either extends or mixes this class so that it can notify the property changes to the View layer. You can refer to this example for understanding how to implement it.
This is a very simple solution when you have a small number of listeners. Adding or removing a listener is O(N) operation and dispatching notifications is O(N²) operation.
Also, whenever notifyListeners() is called, all its listener widgets get rebuilt even though the property being used in the widget isn’t changed. One solution for this is to use the subclass of it named ValueNotifier. We can wrap all the properties of the ViewModel which are exposed for the View layer with the ValueNotifier. But there are better solutions out there 😉.
To overcome the problem of property agnostic change notifications, a package named property_change_notifier is published by the Very Good Ventures. This package gives a drop-in replacement class for ChangeNotifier named PropertyChangeNotifier.
When a property changes in the ViewModel, the name of the property is also included in the notification. Listener widgets can then choose to observe only one or many properties.
Indeed this is a simple solution with no extra effort of adding reactive bindings between the two layers. But rather than manually managing what is changed, what if we can automate the change tracking and have some more powerful features 😀?
3. The BLoC pattern — ViewModel + Reactiveness ️⚡️
We can use pure dart class as ViewModel and keep the properties as reactive streams. The view layer can add an action in the ViewModel using StreamController’s Sink or Subject from the RxDart package. Those actions can be processed and converted to the View representable property streams in the ViewModel. From the View layer, widgets can react to the ViewModel streams using the StreamBuilder widget.
This is the purest reactive form of state management where we can rebuild the necessary widgets by using specific property stream in the StreamBuilder. In case you want to understand this technique by tutorial, I would recommend this article. BLoC and ViewModel are identical to me and the subtle difference between them is explained here.
Following this pattern has additional challenges though. As there will be multiple Sinks for accepting different actions and multiple streams exposed for UI reactivity, it’s very easy to lost tracking the data flow. Let’s see what our rising community has done to solve this problem 😄.
4. The BLoC package
A Bloc (Business Logic Component) is a component which converts a
States — BLoC Library
The only way to send the event in the Bloc is add(Event event) method. This input event is mapped to the stream of new states using the mapEventToState method. At any given time we can access the state using state property of the Bloc. For each Event or State, there needs to be a separate class extending Event or State.
With this piped uni-directional data flow, Bloc has introduced powerful features like Transitions, Delegates and Supervisor. You can follow this in-depth guide to understand the Bloc anatomy.
BlocBuilder is the widget that can listen to the stream of States and rebuilds whenever a new value is added in that stream. We can refine the rebuilds with the additional functional argument named condition as well 🤟.
Bloc is one of the most opinionated architectures in Flutter. But what if we want the flexibility in the ViewModel and still want automated change tracking in a much simpler way 😄?
MobX is a state-management library that makes it simple to connect the reactive data of your application with the UI (or any observer). This wiring is completely automatic and feels very natural.
MobX supports the uni-directional data flow. From the View layer, Actions are dispatched to ViewModel. ViewModel processes the action and mutates the state of its properties which are denoted as Observables. The changes in the observable properties are automatically detected by the View layer and widgets get rebuild only if necessary! The power of selective rebuilds lies in the Reactions. Reaction automatically tracks the observable properties used in it and only rebuilds the widget if one of those properties gets changed in the ViewModel. The in-depth guide to understanding the MobX in the context of Flutter can be found here.
Honorable Mention: Provider
A Swiss Army Knife, Provider is the perfect blender of Dependency Injection and State Management. It is mostly the syntactic sugar around the InheritedWidget but has to offer a lot more than that. It’s been developed by Remi Rousselet, another must-follow Flutter enthusiastic. In fact, he has contributed other interesting packages as well which you may want to check out here.
I find this package as the LCD (Least Common Denominator) in every Flutter app we have developed so far.
In all of the above techniques, we discussed the communication mechanism between View and ViewModel layers. The provider comes into the picture even before that. The key responsibilities of the Provider are:
- Create the instance of the ViewModel somewhere in the widget tree underneath which the widgets need access (shared or direct)
- Persist the ViewModel instance across the rebuilds
- Provide the instance of the ViewModel from the tree when needed beneath
We have seen a few of the many state management options available for Flutter, in which we discussed:
- A simplest Ephemeral state can be controlled using StatefulWidget and setState() method.
- A built-in class like ChangeNotifier from Flutter foundation can help us in managing state when there are relatively few listeners.
- Fine-grain the unnecessary rebuilds with PropertyChangeNotifier.
- Reactive streams benefit us from manually managing the property changes using the BLoC pattern.
- Reducer kind of pattern gives us control over input and output of the ViewModel such as the Bloc package.
- Completely automatic, reactive from the ground; MobX can help us write ViewModels which feels natural and tracks changes with no extra effort!
“The rule of thumb is: Do whatever is less awkward.”