Flutter State Management | Must Learn Basics

State management is a very important concept in declarative programming. We introduced earlier that Flutter is declarative programming, and also distinguishes the difference between declarative programming and imperative programming.

Here, let's systematically learn about the very important state management in Flutter declarative programming

1. Why do you need state management?

1.1 Understanding state management

Many people who switch from imperative programming frameworks (Android or iOS native developers) to declarative programming (Flutter, Vue, React, etc.) are not comfortable at first, because a new perspective is needed to consider the development model of APP.

Flutter, as a modern framework, is declarative programming

In the process of writing an application, we have a large number of states to manage, and it is the changes to these states that update the refresh of the interface:

 

1.2 Different state management classifications

1.2.1 Ephemeral state

Some states only need to be used in their own Widget

For example, the simple counter we did earlier

For example, a PageView component records the current page

For example, an animation records the current progress

For example, the currently selected tab in a BottomNavigationBar

We only need to manage this state by using the State class corresponding to StatefulWidget, and other parts of the Widget tree do not need to access this state.

We have used this method many times in previous studies.

1.2.2 App state

There is also a lot of state in development that needs to be shared across multiple parts

Such as a user personalization option

Such as the user's login status information

For example, an e-commerce application shopping cart

For example, a news app's read or unread messages

If we pass this state between Widgets, it will be endless, and the coupling degree of the code will become very high, affecting the whole body, whether it is the quality of code writing, post-maintenance, and scalability. and scalibility would be very poor.

At this time, we can choose the global state management method to manage and apply the state in a unified manner.

1.2.3 How to choose different management methods

In development, there is no clear rule to distinguish which states are short-term states and which states are application states.

Some short-term states may need to be upgraded to application states in subsequent development and maintenance.

But we can simply follow the rules of the following flowchart:

In response to the question of which is better for React to use setState or Store in Redux to manage state, on the issue of Redux, the author of Redux, Dan Abramov, answered:

The rule of thumb is: Do whatever is less awkward

The rule of thumb is this: choose the way that reduces the hassle.

 

2.1 InheritedWidget

InheritedWidget is similar to the context function in React, which can realize the transfer of data across components.

Define an InheritedWidget that shares data, which needs to inherit from InheritedWidget

A of method is defined here, which starts to find the ancestor's HYDataWidget through the context (you can view the source code search process)

The updateShouldNotify method is to compare the old and new HYDataWidget, whether it is necessary to update the dependent Widget

class HYDataWidget extends InheritedWidget {
  final int counter;

  HYDataWidget({this.counter, Widget child}): super(child: child);

  static HYDataWidget of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType();
  }

  @override
  bool updateShouldNotify(HYDataWidget oldWidget) {
    return this.counter != oldWidget.counter;
  }
}

Create HYDataWidget and pass in data (clicking the button here will modify the data and rebuild it)

class HYHomePage extends StatefulWidget {
  @override
  _HYHomePageState createState() => _HYHomePageState();
}

class _HYHomePageState extends State<HYHomePage> {
  int data = 100;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("InheritedWidget"),
      ),
      body: HYDataWidget(
        counter: data,
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              HYShowData()
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          setState(() {
            data++;
          });
        },
      ),
    );
  }
}

Use shared data in a Widget and listen

2.2 Provider

Provider is the currently officially recommended global state management tool, co-written by community author Flutter Team. Before using it, we need to introduce a dependency on it. As of this article, the latest version of Provider is 4.0.4:

dependencies:
  provider: ^4.0.4

 

2.2.1 Basic usage of Provider

When using Provider, we are mainly concerned with three concepts:

ChangeNotifier: where the real data (state) is stored

ChangeNotiferProvider: Where data (status) is provided in the Widget tree, the corresponding ChangeNotifier will be created in it

Cosumer: Where data (state) needs to be used in the Widget tree

Let's first complete a simple case and use the Provider to implement the official counter case:

Step 1: Create your own ChangeNotifier

We need a ChangeNotifier to hold our state, so create it

Here we can use inheritance from ChangeNotifier, or we can use mixins, depending on whether the probability needs to inherit from other classes

We use a private _counter and provide getters and setters

In the setter, when we listen to the change of _counter, we call the notifyListeners method to notify all consumers to update

class CounterProvider extends ChangeNotifier {
  int _counter = 100;
  int get counter {
    return _counter;
  }
  set counter(int value) {
    _counter = value;
    notifyListeners();
  }
}

Step 2: Insert ChangeNotifierProvider in Widget Tree

We need to insert ChangeNotifierProvider in Widget Tree so that Consumer can get data:

Put ChangeNotifierProvider at the top level, so that CounterProvider can be used anywhere in the whole application

void main() {
  runApp(ChangeNotifierProvider(
    create: (context) => CounterProvider(),
    child: MyApp(),
  ));
}

Step 3: Use Consumer to import and modify the state on the home page

Introductory position 1: Use Consumer in the body, Consumer needs to pass in a builder callback function, when the data changes, it will notify the data-dependent Consumer to call the builder method to build again;

introductory position 2: Use Consumer in floatingActionButton, when the button is clicked, modify the counter data in CounterNotifier;

class HYHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Column Test"),
      ),
      body: Center(
        child: Consumer<CounterProvider>(
          builder: (ctx, counterPro, child) {
            return Text("Current Value:${counterPro.counter}", style: TextStyle(fontSize: 20, color: Colors.red),);
          }
        ),
      ),
      floatingActionButton: Consumer<CounterProvider>(
        builder: (ctx, counterPro, child) {
          return FloatingActionButton(
            child: child,
            onPressed: () {
              counterPro.counter += 1;
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

Analysis of Consumer's builder method:

Parameter 1: context, each build method will have a context, the purpose is to know the position of the current tree

Parameter 2: The instance corresponding to ChangeNotifier is also the object we mainly use in the builder function

Parameter 3: child, the purpose is to optimize, if there is a huge subtree under the builder, when the model changes, we do not want to rebuild this subtree, then we can put this subtree into the Consumer In the child, you can directly introduce it here (note the location of the Icon in my case)

Step 4: Create a new page and modify the data in the new page

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Page"),
      ),
      floatingActionButton: Consumer<CounterProvider>(
        builder: (ctx, counterPro, child) {
          return FloatingActionButton(
            child: child,
            onPressed: () {
              counterPro.counter += 1;
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

 

2.2.2. Disadvantages of Provider.of

In fact, because Provider is based on InheritedWidget, when we use the data in ChangeNotifier, we can use Provider.of, such as the following code:

Text("Current value:${Provider.of<CounterProvider>(context).counter}",
  style: TextStyle(fontSize: 30, color: Colors.purple),
),

We will find that the above code will be more concise, so should we choose the above method in development?

The answer is no, more often we still have to choose the way of Consumer.

why? Because when the Consumer refreshes the entire Widget tree, it will rebuild the Widget as little as possible.

Method 1: The complete code of the Provider.of method:

When we click on the floatingActionButton, the build method of HYHomePage will be called again.

This means that the entire HYHomePage widget needs to be rebuilt

class HYHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("调用了HYHomePage的build方法");
    return Scaffold(
      appBar: AppBar(
        title: Text("Provider"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("当前计数:${Provider.of<CounterProvider>(context).counter}",
              style: TextStyle(fontSize: 30, color: Colors.purple),
            )
          ],
        ),
      ),
      floatingActionButton: Consumer<CounterProvider>(
        builder: (ctx, counterPro, child) {
          return FloatingActionButton(
            child: child,
            onPressed: () {
              counterPro.counter += 1;
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

Method 2: Modify the content in the Text using the Consumer method as follows:

You will find that the build method of HYHomePage will not be called again;

If we have corresponding child widgets, we can use the method in the above case to organize, and the performance is higher.

Consumer<CounterProvider>(builder: (ctx, counterPro, child) {
  print("Calls builder of consumer");
  return Text(
    "Current value:${counterPro.counter}",
    style: TextStyle(fontSize: 30, color: Colors.red),
  );
}),

 

2.2.3. Selection of Selector

Is Consumer the best choice? No, it also has disadvantages

For example, when the floatingActionButton is clicked, we print in two places in the code whether their builder will be called again;

We will find that as long as the floatingActionButton is clicked, both positions will be re-builder;

But is it necessary to rebuild the position of floatingActionButton? No, because whether it is manipulating data or not, it is not displayed;

How can I make it not to rebuild? Use Selector instead of Consumer

Let's first implement the code directly and explain what it means:

floatingActionButton: Selector<CounterProvider, CounterProvider>(
  selector: (ctx, provider) => provider,
  shouldRebuild: (pre, next) => false,
  builder: (ctx, counterPro, child) {
   print("builder gets called from floatingActionButton");
    return FloatingActionButton(
      child: child,
      onPressed: () {
        counterPro.counter += 1;
      },
    );
  },
  child: Icon(Icons.add),
),

The difference between Selector and Consumer is mainly three key points:

Key point 1: The generic parameter is two

Generic parameter 1: The Provider we are going to use this time

Generic parameter 2: The data type after conversion, for example, I still use CounterProvider after conversion here, then they are the same type

Key point 2: selector callback function

The callback function of the conversion, how you want the conversion to be performed

S Function(BuildContext, A) selector

I haven't done the conversion here, so just return the A instance directly

Key point 3: Do you want to rebuild

Here is also a callback function, we can get two instances before and after the conversion;

bool Function(T previous, T next);

Because I don't want it to rebuild here, no matter how the data changes, so here I just return false.

At this time, we re-test and click on the floatingActionButton, the code in the floatingActionButton will not perform the rebuild operation.

So in some cases, we can use Selector instead of Consumer, and the performance will be higher.

 

2.2.4. MultiProvider

In development, there must be more than one data that we need to share, and we need to organize the data together, so one Provider must not be enough.

We are adding a new ChangeNotifier

import 'package:flutter/material.dart';

class UserInfo {
  String nickname;
  int level;

  UserInfo(this.nickname, this.level);
}

class UserProvider extends ChangeNotifier {
  UserInfo _userInfo = UserInfo("why", 18);

  set userInfo(UserInfo info) {
    _userInfo = info;
    notifyListeners();
  }

  get userInfo {
    return _userInfo;
  }
}

What should we do if we have multiple Providers that need to be provided during development?

Method 1: Nesting between multiple Providers

This has great drawbacks. If there are too many nesting levels, it is inconvenient to maintain and the scalability is relatively poor.

  runApp(ChangeNotifierProvider(
    create: (context) => CounterProvider(),
    child: ChangeNotifierProvider(
      create: (context) => UserProvider(),
      child: MyApp()
    ),
  ));

Method 2: Use MultiProvider

runApp(MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (ctx) => CounterProvider()),
    ChangeNotifierProvider(create: (ctx) => UserProvider()),
  ],
  child: MyApp(),
));

Recent posts