DEV Community

Cover image for State Management in Flutter.
Samuel Wahome
Samuel Wahome

Posted on

State Management in Flutter.

Managing state in an application is one of the most important and necessary processes in the life cycle of an application. The main job of a UI is to represent state. According to the official Flutter documentation:

In the broadest possible sense, the state of an app is everything that exists in memory when the app is running. This includes the app's assets, all the variables that the Flutter framework keeps about the UI, animation state, textures, fonts, and so on.

From this definition, we can then conclude that the pattern that programmatically establishes how to track changes and how to broadcast details about states to the rest of your app is known as state management.
The two state types that are normally considered are ephemeral state(also known as UI state or local state)and app state(sometimes also called shared state):

  • Ephemeral state is the state you can neatly contain in a single widget. Ephemeral state is used when no other component in the widget tree needs to access a widget's data. Examples include whether a TabBarView tab is selected or when tracking the current progress of a complex animation. In other words, there is no need to use state management techniques (ScopedModel, Redux, Provider, etc.) on this kind of state. All you need is a StatefulWidget.
  • State that is not ephemeral, that you want to share across many parts of your app, and that you want to keep between user sessions, is what is called application state. Application state when other parts of your app need to access a widget's state data. One example is an image that changes over time, like an icon for the current weather. Another example is when accessing login info that may be required in more than one section of your Flutter application.

Now that we've gotten the basic definitions out of the way, let's get to work exploring the various options we have at our disposal to manage the application state.

GIF


Managing state in a Flutter application.

1. InheritedWidget.

According to the official documentation:

InheritedWidget class is a base class for widgets that efficiently propagate information down the tree.

It's the basis for a lot of other state management widgets. If you create a class that extends InheritedWidget and give it some data, any child widget can access that data by calling context.dependOnInheritedWidgetOfExactType<class>()
Inherited widgets, when referenced in this way, will cause the consumer to rebuild when the inherited widget itself changes state.

class SampleWidget extends InheritedWidget {
  final Sample sample;
  SampleWidget(Key? key, required this.sample, required Widget child}) :
        super(key: key, child: child);

  @override
  bool updateShouldNotify(SampleWidget oldWidget) => sample!= oldWidget.sample;

  static SampleWidget of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<SampleWidget>()!;
}
Enter fullscreen mode Exit fullscreen mode

One can then extract data from that widget. Since that's such a long method name to call, the convention is to create an of() method. Then a child widget, for example, a text field that displays the sample text, can just use:

SampleWidget sampleWidget = SampleWidget.of(context);
print(sampleWidget.sample.text);
Enter fullscreen mode Exit fullscreen mode

An advantage of using InheritedWidget is it's a built-in widget so you don't need to worry about using external packages.

A disadvantage of using InheritedWidget is that the value of a Sample can't change unless you rebuild the whole widget tree because InheritedWidget is immutable. So, if you want to change the displayed sample text, you'll have to rebuild the whole SampleWidget.

2. Provider.

According to the official documentation:

Provider is a wrapper around InheritedWidget to make them easier to use and more reusable.

Provider was designed by Remi Rousselet to wrap around InheritedWidget, simplifying it. In short, Provider is a set of classes that simplifies building a state management solution on top of InheritedWidget.
Provider has several commonly used classes such as: ChangeNotifierProvider, Consumer, FutureProvider, MultiProvider , StreamProvider, etc.

ChangeNotifier
ChangeNotifier is a class that adds and removes listeners, then notifies those listeners of any changes. You usually extend the class for models so you can send notifications when your model changes. When something in the model changes, you call notifyListeners() and whoever is listening can use the newly changed model to redraw a piece of UI, for example.

ChangeNotifierProvider
ChangeNotifierProvider is a widget that wraps a class, implementing ChangeNotifier and uses the child widget for display. When changes are broadcast, the widget rebuilds its tree.

ChangeNotifierProvider(
  create: (context) => SampleModel(),
  child: <widget>,
);
Enter fullscreen mode Exit fullscreen mode

Consumer
Consumer is a widget that listens for changes in a class that implements ChangeNotifier, then rebuilds the widgets below itself when it finds any. When building your widget tree, try to put a Consumer as deep as possible in the UI hierarchy, so updates don't recreate the whole widget tree.

Consumer<SampleModel>(
  builder: (context, model, child) {
    return Text('Hello ${model.text}');
  }
);
Enter fullscreen mode Exit fullscreen mode

If you only need access to the model and don't need notifications when the data changes, use Provider.of, as shown below:

Provider.of<SampleModel>(context, listen: false).<method name>
Enter fullscreen mode Exit fullscreen mode

listen: false indicates you don't want notifications for any updates. This parameter is required to use Provider.of() inside initState().

FutureProvider
FutureProvider works like other providers and uses the required create parameter that returns a Future.

FutureProvider(
  initialData: null,
  create: (context) => createFuture(),
  child: <widget>,
);

Future<SampleModel> createFuture() async {
  return Future.value(SampleModel());
}
Enter fullscreen mode Exit fullscreen mode

A Future is handy when a value is not readily available but will be in the future. Examples include calls that request data from the internet or asynchronously read data from a database or network.

MultiProvider
What if you need more than one provider? You could nest them, but it'd get messy, making them hard to read and maintain. Instead, use MultiProvider to create a list of providers and a single child:

MultiProvider(
  providers: [
    Provider<SampleModel>(create: (_) => Model()),
    Provider<SampleDatabase>(create: (_) => Database()),
  ],
  child: <widget>
);
Enter fullscreen mode Exit fullscreen mode

StreamProvider
Provider also has a provider that's specifically for streams and works the same way as FutureProvider. Stream providers are handy when data comes in via streams and values change over time like, for example, when you're monitoring the connectivity of a device, or listening to active changes from a network.

3. Riverpod.

According to the official documentation:

Rivepod can be considered as a rewrite of provider to make improvements that would be otherwise impossible.

Riverpod was written by Provider's author, Remi Rousselet, to address some of Provider's weaknesses. In fact, you'd be surprised to find out that Riverpod is in fact an anagram of Provider.
Riverpod is a very powerful state management library that allows you to:

  • Easily create, access, and combine providers with minimal boilerplate code.
  • Write testable code and keep your logic outside the widget tree.
  • Catch programming errors at compile-time rather than at runtime.

By design, Provider is an improvement over InheritedWidget and it depends on the widget tree. This leads to some drawbacks such as:

  1. Combining Providers is very verbose.
  2. Getting Providers by type and runtime exceptions.

To combat this, Riverpod is completely independent of the widget tree and doesn't suffer from any of these drawbacks.

4. Redux.

According to the official documentation:

Redux is a predictable state container for Dart and Flutter apps.

Redux uses concepts such as actions, reducers, views, and store, whose flow is shown below:

https://blog.novoda.com/introduction-to-redux-in-flutter/

In summary, actions, like clicks on the UI or events from network operations, are sent to reducers, which turn them into a state. That state is saved in a store, which notifies listeners, like views and components, about changes.

5. BLoC.

According to the official documentation:

The goal of this package is to make it easy to implement the BLoC Design Pattern (Business Logic Component).This design pattern helps to separate presentation from business logic. Following the BLoC pattern facilitates testability and reusability. This package abstracts reactive aspects of the pattern allowing developers to focus on writing the business logic.

Think of it as a stream of events: some widgets submit events and other widgets respond to them. BLoC sits in the middle and directs the conversation, leveraging the power of streams.

https://www.dbestech.com/tutorials/flutter-bloc-pattern-examples

6. MobX.

According to the official documentation:

MobX is a library for reactively managing the state of your applications. Use the power of observables, actions, and reactions to supercharge your Dart and Flutter applications.

MobX utilizes the following concepts in its functioning:

  • Observables: Observables represent the reactive-state of your application. They can be simple scalars to complex object trees.
  • Actions: Actions are how you mutate the observables. Rather than mutating them directly, actions add a semantic meaning to the mutations.
  • Reactions: They are the observers of the reactive-system and get notified whenever an observable they track is changed.

https://itnext.io/flutter-state-management-with-mobx-and-providers-change-app-theme-dynamically-ba3b60619050

7. GetX.

According to the official documentation:

GetX is an extra-light and powerful solution for Flutter. It combines high-performance state management, intelligent dependency injection, and route management quickly and practically.

GetX uses two different state managers: the simple state manager (GetBuilder) and the reactive state manager (GetX/Obx).

class SampleController extends GetxController {

     @override 
    void onInit() {
       // Here you can fetch you product from server
       super.onInit();
    }

    @override 
    void onReady() {
       super.onReady();
    }

    @override
    void onClose() { 
          // Here, you can dispose your StreamControllers
          // you can cancel timers
          super.onClose();
    }
}
Enter fullscreen mode Exit fullscreen mode

https://kauemurakami.github.io/getx_pattern/

References.


While that is a long list of state management options, they are by no means the only options out there, and thus the choice solely depends on your particular use case. 
That was indeed all that I have to share for now. To all readers, cheers to code🥂, and have a blessed day.

Discussion (0)