Widget-Perfect State Management in Flutter! Is it Possible?
If it happened that you read an introductory section about Flutter, you might have faced statements like “Flutter is a toolkit offered by Google to build cross-platform mobile, desktop and web applications from one source codebase”. Most likely, you have not missed the statement either that it is all widgets. It probably hit you too, that Flutter owns every pixel on the screen which allows it to be “Pixel Perfect”.
That is all nice and good but when it comes to state management in Flutter opinions diverge. Can any of the available state management techniques be qualified as the most optimal? I say optimal in the sense of easiness of implementation without losing the ability to surgically rebuild the widget tree whenever you want. Let’s find out together!
In the Google I/O 2019 event, the Flutter team took a choice and went with the Provider package as the recommended approach for state management. The good thing about this announcement is that Provider is a community-driven package. This reflects how open the Flutter team towards community contributions is. This announcement is not only a recognition to the creator of the Provide alone, yet also a message to all the community members to encourage them to try to found their solutions. In this article, I will present one contributional effort to make state management in Flutter as easy and as effective as possible.
states_rebuilder:
The states_rebuilder package is based on the principle of separation of concerns. It is a design principle for separating your application into distinct sections so that each section addresses a separate concern. In this case, you define a logic class to hold all of your application logic and provide this class to your User Interface (UI) to make it accessible from all your widgets.
Providing BloCs:
The first Widget offered by states_rebuilder package is BlocProvider
whose sole role is to make an instance of MainBloc, for our example, available to the RoutPage
and all its child widgets so that they can share data using the InheritedWidget
behind the scene.
Prepare the scene:
The UI consists of a simple grid of 12 items. In the BloC, there is a counter
variable and an increment()
method to increment the counter.
Our app is stateless and it is not reactive. Let’s use the strength of states_rebuilder to add reactivity.
Rebuild all:
The second widget offered by the states_rebuilder package is the StateBuilder
.
Wrap what you want to make reactive with StateBuilder and define at least two required parameters:
1- blocs: a list of instances of your logic classes you want the StateBuilder
to subscribe to the list of listeners;
2- builder: a closure with two parameters, the first is of BuildContext type that holds the context of this widget, and the second of String type and it holds the identity (or the key or the address) of this widget. As we do not need them now,we just used underscores.We will use them late on in this article.
In the Bloc we use rebuildStates()
to notify listeners subscribed in this particular BLoC to rebuild.
That all, when you tap on any of the boxes all of them rebuild.
If you’re familiar with other libraries such as scoped_model and provider, we probably noticed that they achieve a similar behavior using notifyListeners
. What makes states_rebuilder different is the fact that it gives you more capabilities to surgically rebuild the widget tree. Continue reading this article and I think you’ll be pleasantly surprised.
Rebuild one:
If you want to be widget perfect, you want to rebuild only the tapped box. This easily achieved with states_rebuilder:
All the change you have to do on the UI are highlighted. Not much!
The second parameter of the builder
closure is of type String. It stores the unique identity of the state (the address of the state). I choose to name it tagID
, but it can be named tagMe
, thisTag
or any other name you think it is more descriptive. Just like the first parameter of the builder closure which you can name it context, or simple “c” or just an underscore if not used.
the logic class becomes:
This is how the app looks like after these tiny changes:
Please note that all boxes hold the same value of the counter, but only the tapped one is rebuilt to reflect the updated value.
You see who easy it is, how efficient it is, how widget perfect it is!
Rebuild a set:
Let’s say you want to rebuild a set of boxes that have common criteria. In our example,let’s suppose that we want that all widgets that have an even index to rebuild all together. Also, we want the same thing to widgets that have an odd index.
The fact that we want to rebuild a specific set of boxes leads us to distinguish them form other boxes. This is achieved by giving them a name or tagging them so that when reBuildStates
is called with the chosen tag, those widgets that have this tag will rebuild. The tag parameter in the states_rebuilder is dedicated to such a use.
This is how the UI looks like:
the bloc part remains the same.
the parameter tag is dynamic so that it accepts any type of data.
Rebuild remote widget:
To show more functionality, let’s say we want to add a widget to show if the tapped widget has even or odd index.
This is one possible implementation:
The UI part:
The bloc part:
The solution is to use a StateBuilder
with a tag. We can choose a simple String to name our tag, but for a big project, it is preferred to use an enums so that we can make full profit of our preferred IDE helping functionality such as auto-completion, peek and show definition and references. The conventional place to declare your enums is in the same file where your BloC classes are. For a bigger project, it is even more helpful if you declare your enums in one file and import it when needed.
A full functional statefulWidget:
StateBuilder
offers many other parameters to mimic all the functionality of a StatefullWidget:
StateBuilder(
{Key key,
dynamic tag,
List<StatesRebuilder> blocs,
(BuildContext, String) → Widget builder,
(BuildContext, String) → void initState,
(BuildContext, String) → void dispose,
(BuildContext, String) → void didChangeDependencies,
(BuildContext, String, _StateBuilder) → void didUpdateWidget
})
StateBuilder with mixins:
Are we saying goodbye to StatefulWidget? NO, because the state class of a StatefulWidget can be mixin with other class to support animation, tabView and many other things.
states_rebuilder offers the StateWithMixinBuilder
widget to handle some of the most commonly used mixins.
StateWithMixinBuilder<T>(
{Key key,
dynamic tag,
List<StatesRebuilder> blocs,
(BuildContext, String) → Widget builder,
(BuildContext, String, T) → void initState,
(BuildContext, String, T) → void dispose,
(BuildContext, String, T) → void didChangeDependencies,
(BuildContext,String, _StateBuilder, T) → void didUpdateWidget,
(String, AppLifecycleState) → void didChangeAppLifecycleState
MixinWith mixinWith
})
Mixin supported until now are singleTickerProviderStateMixin, tickerProviderStateMixin, AutomaticKeepAliveClientMixin and WidgetsBindingObserver.
Animation Example:
I won’t go through the details on how to use animation in Flutter. The aim of the example is to show you a practical case of using StateWithMixinBuilder
.
If you want more details on how to set your animation, this is a good article about animation written by Raouf Rahiche. (link)
The UI is:
The bloc is:
From the initState()
of the StateWithMixinBuilder
we call the initAnimation
from the bloc with TickerProvider
type parameter. It is here where all the conventional animation setup occurs.
In the triggerAnimation()
of the bloc we added rebuildStates
() as a listener, which means that all the boxes will rotate when any of them is tapped.
To rotate only one box, we follow the same steps mentioned above.
The change in UI part.
The change in the bloc part
the result.
The last tapped box will continue to animate because listeners are not removed. Here is the bloc with removing listeners:
The result:
To animation the even and the odd indexed boxes, there is no special thing, as mentioned in the steps above:
The change in the UI are:
The result is:
Based on the states_rebuilder, I created a library dedicated to the animation called the animator.
This is an article explaining some of its functionalities.
AppLifecycleState Example:
This is an example using AppLifecycleState to track the state of the app (onPause(), onResume() in Android).
The UI part:
The Bloc part, we add lifecycleState
method:
The result is:
Performance consideration:
As states_rebuilder rely on the speed of fetching data form Maps where listeners are stored, performance questions may arise.
I try to compare the time taken to execute the rebuildState() function with that taken by a simple print statement.
I followed this approach:
This is the result in the debug mode:
The states_rebuild average is 50 microseconds and the print statement 20 microseconds in the debug mode.
I increased the number of grids to 500, the results are
This means that the average time taken to mark 250 widgets as to need to rebuild is 150 microseconds in debug mode. IT is important to note that states_rebuilder stores only listeners which are mounted. When a state is disposed, its listener is removed. In most common situation states_rebuilder stores about tenths of listeners.
I used the same technique with notifyListener() used in scoped_model and provider, I found:
This is one contribution from the community to manage the states in Flutter. I hope we like it.
states_rebuilderr for the web:
The good news is that states_rebuilder works in the web just as well as it does in the mobile. This GIF shows all the above example with the same code in web browser:
The full code of this article is here.
The page of the states_rebuilder is here.
Happy to hear your claps, to read your comments, to react to your critics, to see your stars in GitHub and above all happy to be part of the great Flutter community.