The article is translated from the official document šŸ‘‰ status management. Kind of a primer on state management.

If you are already familiar with state management in a reactive framework App, you can skip the previous sections and just look at the list of state management methods that follow as a refresher on state management frameworks.

To develop Flutter, you may need to share your App’s state without pages in your App. There are many ways to do this, and many questions to ponder further.

In this article, you’ll learn the basics of handling state management in your App.

Start by understanding the declarative

If you learn Flutter from an imperative framework, such as Android, IOS, etc. You need to think about App development in a new way.

Many ideas may not apply to Flutter. For example, when a page changes, you don’t need to modify the UI, but rebuild it from scratch. Flutter is very fast, even in every frame of processing, can do exactly this.

A Flutter is declarative, which means that a Flutter is a reaction that applies the current state:

When your app’s state changes, such as when the user sets the switch, it triggers a UI redraw. The UI is redrawn from scratch, so there is no need to change the UI itself. There is no code for widget.setText in Flutter, all are redraw widgets.

You can learn about šŸ‘‰ declarative UI in this article.

There are many benefits to declarative UIs, most notably that any state of the UI points only to its own code, and the relationship between what state corresponds to what UI is very clear.

Declarative programming may not be as straightforward as imperative programming, so this section introduces declarative programming first.

Distinguish between temporary state and application state

This section covers application state and temporary state, and how is each state managed

Basically, the state of an application is all the data that exists in memory at runtime. This includes the application’s resources, all the variables that the Flutter framework holds about the UI, animation state, textures, fonts, etc. Although this definition is acceptable, it is not useful for the application architecture.

First, you don’t even need to manage some states, such as textures, the Flutter framework does that for you. So the definition of state should be whatever data is needed whenever the UI is rebuilt. Second, the state we developers manage falls into two categories: temporary state and application state.

A temporary state

Temporary states, sometimes called UI state or Local state, are states that developers can cleverly build into a single Widget.

šŸ‘† The above definition is a vague abstraction, here are a few small examples:

  • PageView Indicates the current Page
  • Current progress of the animation
  • BottomNavigationBarThe currently selected TAB

The rest of the Widget tree has little need to access this state, serialize it, or modify it in complex ways.

That is, no state management (ScopedModel, Redux, and so on) techniques are required for this state, just a StatefulWidget.

In the following example, you’ll see that _MyHomepageState holds the _index field, which represents the TAB currently selected at BottomNavigationBar. _index is the temporary state.

class MyHomepage extends StatefulWidget {
  const MyHomepage({Key? key}) : super(key: key);

  @override
  _MyHomepageState createState() => _MyHomepageState();
}

class _MyHomepageState extends State<MyHomepage> {
  int _index = 0;

  @override
  Widget build(BuildContext context) {
    return BottomNavigationBar(
      currentIndex: _index,
      onTap: (newIndex) {
        setState(() {
          _index = newIndex;
        });
      },
      // ... items ...); }}Copy the code

Here, using setState() + member variables in the State of the StatefulWidget is sufficient. Other parts of the application don’t need to access _index either. This variable only changes inside MyHomepage, and if the user reopens the app, you will naturally reset _index to 0.

Application state

This state is not temporary, but is shared within the application, stored within the user’s session, and is sometimes referred to as shared state.

Here are some examples of application states:

  • User preferences
  • The login information
  • Network notifications for social apps
  • Shopping cart of e-commerce App
  • The status of news reading in the news App

To manage this state, you need to compare the pros and cons. Consider the nature of the application, the complexity of the application, the team’s experience, and so on

Fuzzy rules

First of all, developers can use State + setState() with you to manage all states, and the Flutter team does use this method to manage some simple apps, such as the demo project we created with Flutter Create.

The reverse is also true, for example, you can make the TAB selected in the Bottom navigation globally shared in a particular context and need to modify it from an external class, keep it for the entire session, etc. In this case the _index state is applied, not temporary.

Therefore, there is no clear-cut rule between temporary state and application state, and sometimes temporary state and application state may transition. For example, as an application grows and expands, a state that starts out as an explicitly temporary state may become an application state.

For this reason, we can take the following graph, which is a general partition of states, with a grain of salt:

In general, there are two types of states in Flutter. The State that serves a single Widget is temporary and can be managed using State + setState(). The others are App states, both of which have value and status, and the dividing line between them is the complexity of the App and the developer’s preferences.

Simple application status management

You can continue to learn about managing application state from the declarative UI, temporary state, and application state introduced earlier.

Next, use the Provider library to manage the application state. If you are just starting to develop Flutter and have no particular reason to choose any other way, then a Provider is a good place to start. Most of the concepts of state management are similar, and providers are easy to understand and don’t require much code. Learning about providers is very helpful for understanding other approaches. If you have a good background, you can look directly at the following way of comparison.

Case study

The App has two separate pages: the catalog page MyCatalog and the shopping cart page MyCart. You can also imagine contacts and favorites in social apps. The effect is shown below:

The catalog page consists of a navigation bar (MyAppBar) and a scrolling list (MyListItems).

The Widget tree is shown below:

There are now at least five widgets, most of which probably need to access state that doesn’t belong to them. For example, each MyListItem needs to be able to add itself to the shopping cart and also need to see if it already exists in the shopping cart.

This begs the question: Where should we put the current state of the shopping cart?

State of ascension

In Flutter, it is helpful to put state on top of widgets that use it.

Why is that? In a declarative framework like Flutter, if you want to change the UI and you want to rebuild it, there is no way to call myCart.updateWith (somethingNew) directly. That is, we cannot externally call the Widget’s methods to make changes. There are ways to do this, but it goes against the framework’s design. Let’s explain from the code level:

// BAD: Never do this. Find the widget and call the widget's method
void myTapHandler() {
  var cartWidget = somehowGetMyCartWidget();
  cartWidget.updateWith(item);
}
Copy the code

Even if you write the above code, you still have to deal with the leaky methods in the “MyCart” widget:

// BAD: DO NOT DO THIS
Widget build(BuildContext context) {
  return SomeWidget(
    // The initial state of the cart.
  );
}

void updateWith(Item item) {
  // Somehow you need to change the UI from here.
}
Copy the code

You need to think about what the current state of the UI is, but also have the UI apply new data. This can easily lead to problems.

In Flutter, you construct a new widget every time its contents change. Instead ofĀ MyCart.updateWith(somethingNew)Ā (a method call) you useĀ MyCart(contents)Ā (a constructor). Because you can only construct new widgets in the build methods of their parents, if you want to changeĀ contents, it needs to live inĀ MyCartā€™s parent or above.

In Flutter, new widgets are built whenever the content changes. We can use MyCart(contents) instead of myCart.updateWith (somethingNew) and constructors instead of method calls. Since widgets are hierarchical and the parent component builds the child component, building to change the display of MyCart requires placing the contents you want to change on or above the parent node of MyCart.

// GOOD
void myTapHandler(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  cartModel.add(item);
}
Copy the code

There is now only one code entry for the UI that builds MyCart.

// GOOD Widget build(BuildContext context) { var cartModel = somehowGetMyCartModel(context); Return SomeWidget(// Use state to build UI // Ā·Ā·Ā· Ā·); }Copy the code

In our case, contents lives in MyApp. As soon as the changes are made, build MyCart. MyCart doesn’t have to worry about the life cycle, it just has to declare what UI to display for a given contents. As soon as contents is modified, the old MyCart disappears and the new MyCart is displayed.

This is what we call an immutable Widget. Just replace the old with the new.

Now that we know where the shopping cart state is, let’s look at how to access it.

Accessing the state

Whenever the user clicks on an item in the catalog list, the item is added to the cart. Since the shopping cart is at the level of MyListItem, the two are isolated. So how do you add?

A simple option is to provide a callback thatĀ MyListItemĀ can call when it is clicked. Dartā€™s functions are first class objects, so you can pass them around any way you want. So, insideĀ MyCatalogĀ you can define the following:

One simple way to do this is to call a callback. When MyListItem is clicked, MyListItem invokes the callback. In Dart, Dart’s methods are first-class objects that can be passed anywhere. So MyCatalog can be defined as follows:

@override
Widget build(BuildContext context) {
  return SomeWidget(
    // Pass the method to MyListItem
    MyListItem(myTapCallback),
  );
}

void myTapCallback(Item item) {
  print('user tapped on $item');
}
Copy the code

This is fine, but the application state can be changed by any node, and the way the callbacks are called above means passing a lot of callbacks, especially layers of callbacks. Such practices are out of date.

Fortunately, Flutter provides a mechanism for providing data and services to offspring nodes, not just children, but any offspring. Everything is a Widget, and this mechanism is a Widget — InheritedWidget, InheritedNotifier, InheritedModel, etc. You can learn and use these widgets by looking at their documentation. We’re not focusing on that.

We continue to use the provider library, which is as simple to use as the components that Flutter provides. You need to add the provider to pubspec.yaml before using it.

name: my_name
description: Blah blah blah.

# ...

dependencies:
  flutter:
    sdk: flutter

  provider: ^6.0. 0

dev_dependencies:
  # ...
Copy the code

Now can guide package: import ‘package: the provider/provider. The dart’; , ready to use.

WithĀ provider, you donā€™t need to worry about callbacks orĀ InheritedWidgets. But you do need to understand 3 concepts:

With providers, you don’t have to worry about callbacks and InheritedWidgets. Just three concepts need to be understood:

  • ChangeNotifier
  • ChangeNotifierProvider
  • Consumer

ChangeNotifier

ChangeNotifier is a simple class in a Flutter that sends notifications to its listeners. So, as long as a component or service is a ChangeNotifier, you can subscribe to its changes, kind of like the concept of being observed.

In providers, ChangeNotifier is a way to encapsulate application state. For many simple apps, a Single ChangeNotifier might suffice. For complex apps, you may need several complex event sources — ChangeNotifiers. Multiple event source scenarios are also simple and do not need to worry about ChangeNotifier and provider nesting.

We create a new class that extends it, like so: In our example, We want to manage the state in ChangeNotifier. We create a new class successor, ChangeNotifier.

class CartModel extends ChangeNotifier {
  /// Internal, private state of the cart.
  final List<Item> _items = [];

  /// An unmodifiable view of the items in the cart.
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  /// The current total price of all items (assuming all items cost $42).
  int get totalPrice => _items.length * 42;

  /// Adds [item] to cart. This and [removeAll] are the only ways to modify the
  /// cart from the outside.
  void add(Item item) {
    _items.add(item);
    // This call tells the widgets that are listening to this model to rebuild.
    notifyListeners();
  }

  /// Removes all items from the cart.
  void removeAll() {
    _items.clear();
    // This call tells the widgets that are listening to this model to rebuild.notifyListeners(); }}Copy the code

The only code generalizing a custom class is a call to notifyListeners(). Whenever the event source changes, which may alter the UI, the notifyListeners() method is called. The code for CartModel is just the business logic and the model itself.

ChangeNotifier is part of Flutter: Foundation and does not rely on higher-level classes. It is also easy to do unit tests. Here is an example of unit tests.

test('adding item increases total cost', () {
  final cart = CartModel();
  final startingPrice = cart.totalPrice;
  cart.addListener(() {
    expect(cart.totalPrice, greaterThan(startingPrice));
  });
  cart.add(Item('Dash'));
});
Copy the code

ChangeNotifierProvider

ChangeNotifierProvider is the widget that provides an instance of a ChangeNotifier to its descendants. It comes from the provider package.

ChangeNotifierProvider is a widget in the Provider package that provides a ChangeNotifier instance for descendant nodes.

We already know where to put the ChangeNotifierProvider: above the level of the widget that needs to access the instance. In this example, the CartModel needs to be placed on top of MyCart and MyCatalog.

You don’t want to place ChangeNotifierProvider higher than necessary (because You don’t want to crash the scope). But in our case, the only widget that is on top of both MyCart and MyCatalog is MyApp.

You probably don’t want to place the ChangeNotifierProvider higher than necessary, for example to pollute the scope. But in our case, the hierarchy higher than both MyCart and MyCatalog is MyApp.

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: const MyApp(),
    ),
  );
}
Copy the code

Note that we defined builder to build CartModel instances, ChangeNotifierProvider is very clever and will only refactor CartModel when necessary. The Dispose () method is also automatically called when the instance is no longer needed.

If you have more than one event source, you can use MultiProvider:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CartModel()),
        Provider(create: (context) => SomeOtherClass()),
      ],
      child: const MyApp(),
    ),
  );
}
Copy the code

Consumer

Now that we have the CartModel for our descendant nodes via the ChangeNotifierProvider, we are ready to use it.

You can do this using the Consumer Widget.

return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text("Total price: ${cart.totalPrice}"); });Copy the code

We have to specify the type we want to access, in this case we want CartModel, so we write Consumer

. If you don’t specify generics, the provider doesn’t work. The design of a provider is type-based, and if you don’t specify a type, it doesn’t know what you want.

The only required parameter for the Consumer Widget is a Builder, which is a method. Whenever ChangeNotifier changes, the Builder method is called. So whenever the Model notifyListeners() method is called, all corresponding Consumer Listeners’ Builder methods are called.

The Builder method takes three parameters, the first of which is context. The second is an instance of ChangeNotifier, which normally contains the data the UI wants. The third argument is child, which is optional. In the Consumer scenario, there is a large subtree that does not require response data, so you can build it once with the Child, and the rest is built by passing.

return Consumer<CartModel>(
  builder: (context, cart, child) => Stack(
    children: [
      // Use SomeExpensiveWidget here, without rebuilding every time.
      if(child ! =null) child,
      Text("Total price: ${cart.totalPrice}"),,),// Build the expensive widget here.
  child: const SomeExpensiveWidget(),
);
Copy the code

The best practice is to place the Consumer as deep as possible. Typically, you’ll rebuild most of your UI just because one detail in one place has changed.

// DON'T DO THIS
return Consumer<CartModel>(
  builder: (context, cart, child) {
    return HumongousWidget(
      // ...
      child: AnotherMonstrousWidget(
        // ...
        child: Text('Total price: ${cart.totalPrice}'),),); });Copy the code

Use the following method:

// DO THIS
return HumongousWidget(
  // ...
  child: AnotherMonstrousWidget(
    // ...
    child: Consumer<CartModel>(
      builder: (context, cart, child) {
        return Text('Total price: ${cart.totalPrice}'); },),),);Copy the code

Provider.of

In some cases, you may not really need the data to change the UI, just to access it. For example, the ClearCart button wants to allow the user to remove all items. Instead of displaying the CART’S UI, you just call the clear() method.

We might use the Consumer

, but this would be wasteful because we require the framework to rebuild a UI that needs to be refactored — the ClearCart button.

In this case, we can use provider.of and set listen to false.

Provider.of<CartModel>(context, listen: false).removeAll();
Copy the code

Using the above line in a build method won’t cause this widget to rebuild when notifyListeners is called. The code above does not rebuild the widget when the notifyListeners are called.

Combine these things

Check out the example at šŸ‘‰ to see the code for the article. For a quick look, see šŸ‘‰ Built with Provider code, which is a modification of the counter code.

In this section, we have a good understanding of how to manage application state. Now we can use providers to build our own apps.

Other state management frameworks

State management is a complex topic, so if there are questions that remain unanswered, or if the methods described above are not available, check out the following.

Learn more in the links below, many of which have been contributed by the Flutter community:

Methods the overview

When choosing one approach, consider the content first:

  • Pragmatic State Management in Flutter, Google I/O 2019 video
  • Flutter Architecture Samples, ä½œč€… Brian Egan

Provider

Recommended usage

  • Provider package
  • You Might Not Need Redux: The Flutter Edition by Ryan Edge
  • Making sense of all those Flutter Providers

Riverpod

Riverpod, another good choice, is similar to Provider and is compile-safe and testable. Riverpod doesnā€™t have a dependency on the Flutter SDK.

Another good choice is Riverpod, which is provider-like in that it is compile safe and unit testable. And Riverpod does not rely on the Flutter SDK.

  • Riverpod home page
  • Getting started with Riverpod

setState

Temporary status management mode

  • Add InterActivity to your Flutter app, Flutter document
  • Basic state management in Google Flutter, ä½œč€… Agung Surya

InheritedWidget & InheritedModel

The way ancestor and descendant nodes communicate, providers and many third-party libraries are based on this mechanism.

Here’s what InheritedWidget uses:

  • InheritedWidget docs
  • Managing Flutter Application State With InheritedWidgets, ä½œč€… Hans Muller
  • Inheriting Widgets, ä½œč€… Mehmet Fidanboylu
  • Using Flutter Inherited Widgets Effectively, ä½œč€… Eric Windmill
  • Widget-state-context-inheritedwidget, by Didier Bolelens

Redux

The state container approach is familiar to many Web developers

  • Animation Management with Redux and Flutter, DartConf 2018 Video Accompanying Article on Medium
  • Flutter Redux package
  • Redux Saga Middleware Dart and Flutter, author Bilal Uslu
  • Introduction to Redux in Flutter by Xavi Rigau
  • Flutter + Redux — How to Make a Shopping List App, by Paulina Szklarska on Hackernoon
  • Building a TODO Application (CRUD) in Flutter with Redux — Part 1, Video by Tensor Programming
  • Flutter Redux Thunk, an example, ä½œč€… Jack Wong
  • Building a (Large) Flutter App with Redux by Hillel Coren
  • Fish-Redux — An Open flutter Application Framework Based on Redux, author Alibaba
  • Async Redux — Redux without Boilerplate. Allows for both sync and Async Reducers, by Marcelo Glasberg
  • Flutter meets Redux: The Redux Way of Managing Flutter Applications State by Amir Ghezelbash
  • Redux and EPics for Better – Organized Code in Flutter Apps, by Nihad Delic

Fish-Redux

The Flutter application framework, based on Redux state management, is suitable for building medium and large applications.

  • Fish-redux-library Package by Alibaba
  • Fish – story – the Source, the Source code
  • Flutter-Movie, demo use case

BLoC / Rx

Based on flow and observer patterns

  • Architect Your Flutter Project Using BLoC Pattern by Sagar Suri
  • BloC Library by Felix Angelov
  • Reactive Programming – Streams – BLoC – Practical Use Cases by Didier Boelens

GetIt

State management based on service location, without BuildContext.

  • GetIt Package, the service locator can be used in conjunction with BloCs.
  • GetIt Mixin package.GetItThe hybrid provides a state management solution
  • GetIt Hooks package,
  • Flutter State Management for Minimalists, author Suragch

MobX

Observer-based and responsive libraries

  • MobX.dart, Hassle free state-management for your Dart and Flutter apps
  • Getting started with MobX.dart
  • Flutter: State Management with Mobx, ä½œč€… Paul Halliday

Flutter Commands

Reactive state management using command mode is best used with GetIt, but can also be used with Provider or another locator.

  • Flutter Command package
  • RxCommand package.StreamThe implementation of

Binder

State management based on InheritedWidget. The separation of concerns.

  • Binder package
  • Binder examples
  • Binder snippets, vscode snippets

GetX

Simple state management solution

  • GetX package
  • Complete GetX State Management by Tadas Petra
  • GetX Flutter Firebase Auth Example by Jeff McMorris

states_rebuilder

It combines state management, dependency injection, and routing.

  • States Rebuilder source
  • States Rebuilder documentation

Triple Pattern (Segmented State Pattern)

Use Streams or ValueNotifier for state management. This mechanism (called triple for a reason: the stream always uses three values: Error, Loading, and State) is based on the Segmented State mode.

  • Triple documentation
  • Flutter Triple package
  • Triple Pattern: A new pattern for state management in FlutterĀ 
  • VIDEO: Flutter Triple Pattern by Kevlin OssadaĀ (recorded in English)