Are you facing state management challenges while developing UI framework? If yes, then flutter’s state management solutions can help you. Read this blog to know more.
State management is one of the major problems in every UI framework. Be it React in web world or Flutter in the cross-platform word or any other framework.
In simple apps, it is usually not a big deal to manage state which is bound to a single component or widget (in flutter terms) of the UI. The underlying framework usually does offer some built-in tools for managing such self-contained, local or ephemeral state.
The need for relying on a state management solution does arrive when a part of state is shared among multiple components. But, fret not! There are a gazillion solutions out there to help developers in dealing with state application management.
We’ve have been working with flutter since for over two years, and it has been an adventurous journey working with different flutter state management solutions.
In this article, we’ll walking you through some of top solutions present in flutter ecosystem including the best flutter state management solution we’ve experienced - Riverpod.
Here’s a list of all the Flutter state management solutions:
This list is taken directly from the Flutter documentation for state management.
So far, we have worked with some of the Flutter state management tools including StatefulWidget, provider, Riverpod, and Bloc.
If you have a simple state that only affects the behavior of a single widget, StatefulWidget is all you need.
In flutter, while proposing any state management solution, the main target is anyhow call the build method of the widget containing the state property with the updated state, so that the UI with the updated state properties is being rendered. The state management solutions come up with different techniques to achieve the same, i.e. calling the build method. Different solutions follow different design patterns to achieve this.
StatefulWidget comes with a method setState(cb) which can be used to call the build method of that particular StatefulWidget anytime. It takes in a callback cb function which is called from inside the setState body just before calling the build method. Thus, you can use setState to call the build method with the updated state. Usually, the callback is used to update the properties of the StatefulWidget’s state for the UI to render with the updated state.
Here’s an example for your reference:
Calling a single **build** method for a single widget is very simple using StatefulWidget and it’s setState(cb).
Things become challenging and interesting when you need to call multiple build methods of multiple widgets. Such cases usually appear when you need change the state which is shared across multiple widgets, i.e. when you need to deal with shared / global application state. Our goal in such cases is to reflect multiple widgets with a top level state whenever the state updates.
Sharing state across multiple widgets is where things get interesting and there are many different techniques to do this. Flutter offers InheritedWidget to share state across multiple widgets.
InheritedWidget is all about making some data or state available to multiple widgets via scoped access. For example, inside your widgets you can call Navigator.of(context) to access the main Navigator, and this uses InheritedWidget under the hood.
Implementing your own InheritedWidget subclasses is not easy and quite error-prone. For this reason, the provider package was introduced.
To learn more about InheritedWidget, you can follow this article:
Flutter comes with a class ChangeNotifier that allows other parts of code to subscribe and listen to the changes in properties of the class. This ChangeNotifier comes with a method notifyListener() which does what it says. Meaning, it notifies all the subscribers about the current status or state of the properties.
This concept is used to achieve the ultimate target of state management i.e. to call the build method of the concerned of widget with the updated state.
You can wrap the state properties in a class which extends ChangeNotifier to be able to use notifyListener(). The widgets concerned with those state properties should subscribe to this ChangeNotifier from inside of their build methods to be able to listen to changes in the state and thus rebuild the UI.
For the ChangeNotifier instances, with the state properties, to be available to the widget layer, provider package comes with widgets like ChangeNotifierProvider, MultiProvider, ProxyProvider, ChangeNotifierProxyProvider, etc.
For example, if you want to provide ThemeNotifier to the widget layer:
For the widgets to be able to subscribe to ChangeNotifier instances, provider package comes with a Consumer widget which rebuilds the UI whenever the notifyListeners() is called. An alternative to Consumer can be using Provider.of<Notifier>(context) inside build method of a widget, which sets the subscription.
For example, to subscribe ThemeNotifier from within a widget:
Provider.of<ThemeNotifier>(context) performs a look-up above the widget tree to find the first location of instantiation of ThemeNotifier.
If the listen property is set to true (which is by default true), it subscribes to ThemeNotifier and gets notified whenever notifyListeners() is called from within ThemeNotifier.
Bloc uses stream based architecture to rebuild the UI with the updated state.
The flutter_bloc package exposes various widgets which help in achieving the ultimate target of state management i.e. to call the build method(s) of the concerned widget(s) to render UI with updated state.
One such widget is BlocBuilder which requires a Bloc and a builder function. BlocBuilder handles building the widget in response to new states. BlocBuilder is very similar to StreamBuilder but has a more simple API to reduce the amount of boilerplate code needed. The builder function will potentially be called many times and should be a pure function that returns a widget in response to the state.
In order to use bloc, you need to have the following 3:
Source: Bloc Architecture
In order to implement the color picker feature using bloc, you need to have these 3 things:
- Here, state is the themeColor
- Here, you have to trigger a ChangeColorEvent whenever you wish to change the themeColor state property
- It is the responsibility of ThemeBloc to handle various ThemeEvent
For the Bloc instances to be made available to the widget layer, flutter_bloc comes with widgets like BlocProvider, MultiBlocProvideretc.
Bloc also exposes a method void add(Event event), to be able to add new events which are then handled inside the bloc to update the state according to the type of event being triggered (added).
According to the official documentation:
Riverpod is a complete rewrite of the Provider package to make improvements that would be otherwise impossible.
The biggest advantage we found using Riverpod is its compete independence with the widget tree for instantiating notifiers or providers. And that’s why Riverpod is my favorite Flutter State Management solution.
In case of provider package or bloc package, it completely depends on the location of the widget tree where the notifiers are being instantiated, so that while subscribing to the notifiers or for getting the instance of providers, you need to use something like Provider.of<Notifier>(context) or BlocProvider.of<Notifier>(context), which then performs a look-up searching for the nearest location in widget tree for instantiation of Notifier.
But, Riverpod completely eliminates the dependency on the BuildContext, which we found it better because, while referring a notifier or provider using Provider.of<Notifier>(context), you need not remember in which level of the widget tree you are at and whether the instantiation of Notifier is present above the current level you are at. If not careful, you would get ProviderNotFoundException only at runtime. This can become a problem with large apps and it would be much better if provider access could be resolved at compile-time.
Creating notifier/provider instances is as simple as creating a global variable. For them to be made available to the widget tree, you just need ProviderScope, exposed by riverpod, to be wrapped around the root widget of the app.
To maintain state and modify state, Riverpod exports different ways, among which simplest one is StateProvider.
You can create a global variable themeColorStateProvider.
ProviderScope will take care of making all the providers available to be accessed from within the widget tree.
Now, to achieve the ultimate goal of state management, i.e. to call the build method(s) of concerned widget(s) with updated state property, Riverpod offers multiple ways like Consumer widget, ConsumerWidget and ConsumerStatefulWidget with ConsumerState.
Riverpod exposes an instance of WidgetRef, which is available while using Consumer , ConsumerWidget and ConsumerState which is an object which allows widgets to interact with the providers. You can think of WidgetRef as something similar to BuildContext in case of flutter:
WidgetRef has a method watch which when used inside build method of a widget, it sets up a subscription with the provider and makes the corresponding widget reactive and thus rebuilds the widget whenever the provider value changes.
For dealing with simple state like themeColor, Provider or StateProvider is sufficient enough, but while deal with some complex applications, you need to store some states along with some business logic. In such cases, riverpod also offers classes like StateNotifierProvider, ChangeNotifierProvider.
Below is a summary of all state management solutions discussed so far.
This comprehensive blog explores the significance of UI frameworks, theming in React applications, popular UI frameworks such as MaterialUI, Bootstrap, and Ant Design, along with their strengths and weaknesses. It delves into the importance of theming for consistent UI/UX, provides insights into various theming approaches in React, and offers a step-by-step guide on implementing theming in React applications.
Discover how Polars, a powerful Rust-based DataFrame library for Python, revolutionizes high-performance data analysis and manipulation. Explore its key features, from speed and efficiency to data manipulation capabilities and lazy evaluation.
In this blog, we cover a wide range of topics, including monitoring, optimization, design patterns, error handling, security measures, scalability, and cost optimization, providing valuable insights and guidance for data engineers and practitioners working with big data processing on cloud platforms like Amazon EMR.