A Complete Guide To Flutter State Management

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.

GraphQL has a role beyond API Query Language- being the backbone of application Integration

A Complete Guide To Flutter State Management

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.

Solutions available in Flutter

Here’s a list of all the Flutter state management solutions:

  • Provider
  • Riverpod
  • setState
  • InhertiedWidget and InheritedModel
  • Redux
  • Fish-Redux
  • BLoC/Rx
  • GetIt
  • MobX
  • Flutter Commands
  • Binder
  • GetX
  • States Rebuilder
  • Triple Pattern

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.

StatefulWidget

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 - Widget - State - Context - InheritedWidget

Provider

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 example:

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

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:

  • States
  • Events (added from the UI layer to the Bloc)
  • Bloc (to manipulate state based on events triggered)

                                                                                       Source: Bloc Architecture

In order to implement the color picker feature using bloc, you need to have these 3 things:

  • State

            - Here, state is the themeColor

  • Event

           - Here, you have to trigger a ChangeColorEvent whenever you wish to change the themeColor state property

  • Bloc

            - 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).

Riverpod

Riverpod is a popular Flutter state management library that shares many of the advantages of Provider and brings many additional benefits.

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:

  • BuildContext lets you access ancestor widgets in the widget tree (such as Theme.of(context) and MediaQuery.of(context))
  • WidgetRef lets you access any provider inside the app

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.

Riverpod has an excellent documentation covering all of its API in great detail and examples which are pretty straight forward to understand if you are familiar with provider.

Summary

Below is a summary of all state management solutions discussed so far.

Solution Summary
StatefulWidget Sufficient when dealing with the state which is shared with only one widget.
Provider

A powerful package recommended by the Flutter team to manage shared state.
When the application grows big, and you have to deal with deep dependency injections
among services or providers, it is quite difficult to achieve using providers. Provider
package do offer class for dealing with dependency injection called
ProxyProvider but we found its syntax to be very unreadable.
Bloc Currently the most popular state management package. It strictly separate
presentation layer from business logic which becomes quite important to maintain
a large code base. Similar to the provider package, it has a complete dependency on
BuildContext i.e. on the widget tree and thus is error prone while accessing BlocProvider
because you need to remember at which level a bloc was instantiated using BlocProvider.
Riverpod Allows easily accessing that state in multiple locations. Riverpod-providers are
a complete replacement for patterns like Singletons, Service Locators, Dependency
Injection or InheritedWidgets. Simplifies combining the state with others.
You need not remember if a provider is instantiated before accessing it, because it
is resolved during compile time as every provider is assigned to simple global variables.

Want to receive update about our upcoming podcast?

Thanks for joining our newsletter.
Oops! Something went wrong.