Flutter status Management

State management is an important concept in declarative programming. Flutter is declarative programming, as we explained earlier, and distinguishes between declarative and imperative programming.

Here we will take a systematic look at the important state management of Flutter declarative programming

Why is state management needed?

1.1. Understand state management

Many of the changes from imperative programming frameworks (Android or iOS native developers) to declarative programming (Flutter, Vue, React, etc.) were initially inappropriate because they required a new perspective on APP development mode.

Flutter, as a modern framework, is declaratively programmed:

Application process of Flutter construction

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 interface refresh:

Status Management process

1.2. Classification of different state management

1.2.1. Ephemeral state

Some states only need to be used in their own widgets

  • 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
  • Such as a currently selected TAB in the BottomNavigationBar

This State can be managed by ourselves using the State class corresponding to the StatefulWidget; the rest of the Widget tree does not need to access this State.

We’ve used this method many times in the past.

1.2.2. App State

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

  • Let’s say the user has a personalized option
  • Such as user login status information
  • Like a shopping cart for an e-commerce app
  • Like the read or unread messages of a news app

If we pass this state from Widget to Widget, it will be infinite, and the code will become very highly coupled, and the quality of writing, maintenance, and scalability will be very poor.

At this time, we can choose the mode of global status management to manage and apply the status in a unified manner.

1.2.3. How to choose different management modes

In development, there are no clear rules to distinguish between short-time states and application states.

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

But we can simply follow the rules of this flowchart:

State Management options

In response to the question of whether it’s better to use setState or Store in Redux to manage state with React, Redux’s Dan Abramov wrote on Redux’s issue:

The rule of thumb is: Do whatever is less awkward

The rule of thumb is: choose a way to reduce trouble.

Choose a way to reduce the amount of trouble

Share state management

2.1. InheritedWidget

InheritedWidget uses the same functionality as Context in React to transfer data across components.

Define an InheritedWidget that shares data and needs to inherit the InheritedWidget

  • Here we define an of method that uses the context to start looking for the ancestor’s HYDataWidget.
  • The updateShouldNotify method is used to compare the old and new HYDataWidget and whether the widgets that depend on the update are required
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; } } Copy the code

Create HYDataWidget and pass in the data (click the button here to modify the data and rebuild)

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++;  });  },  ),  );  } } Copy the code

Use shared data in a Widget and listen

2.2. The Provider

Provider is the currently officially recommended global state management tool written by community authors Remi Rousselet and 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
Copy the code

2.2.1. Basic Use of Provider

When using providers, we are concerned with three main concepts:

  • ChangeNotifier: Place where real data (status) is stored
  • ChangeNotifierProvider: The place in the Widget tree where data (state) is provided, and the corresponding ChangeNotifier is created
  • Consumer: The part of the Widget tree where data (state) is needed

Let’s start with a simple case where the official counter case is implemented using a Provider:

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 mixin, depending on whether the probability needs to be inherited from another class
  • We use a private _counter and provide getters and setters
  • When we hear changes to _counter in the setter, we call notifyListeners to notify all consumers of the changes
class CounterProvider extends ChangeNotifier {
  int _counter = 100;
  int get counter {
    return _counter;
  }
 set counter(int value) {  _counter = value;  notifyListeners();  } } Copy the code

Step 2: Insert the ChangeNotifierProvider in the Widget Tree

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

  • The ChangeNotifierProvider is placed at the top level so that CounterProvider can be used anywhere throughout the application
void main() {
  runApp(ChangeNotifierProvider(
    create: (context) => CounterProvider(),
    child: MyApp(),
  ));
} Copy the code

Step 3: Use Consumer on the home page to introduce and modify the status

  • Introducing location one: Using Consumer in the body, Consumer needs to pass in a Builder callback that tells the data-dependent Consumer to re-call the Builder method when the data changes.
  • Introduce position 2: Use Consumer in floatingActionButton and modify counter data in CounterNotifier when clicking the button;
class HYHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
 title: Text("List test"),  ),  body: Center(  child: Consumer<CounterProvider>(  builder: (ctx, counterPro, child) {  return Text("Current count:${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),  ),  );  } } Copy the code

Consumer Builder

  • Context. Each build method has a context to know the current tree position
  • Parameter 2: The instance corresponding to ChangeNotifier, which is the main object we use in the Builder function
  • Three parameters: Child, for optimization purposes. If there is a large subtree underneath the Builder and we don’t want to rebuild the subtree when the model changes, we can place the subtree in the Consumer child and import it directly here (note the position of Icon in my example).
Effect of 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),  ),  );  } } Copy the code
The second page modifies the data

Disadvantages of 2.2.2. Provider

In fact, because providers are based on inheritedWidgets, we can use provider.of when using data in ChangeNotifier, as shown in the following code:

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

It is obvious that the above code is much cleaner, so should we choose this method in development?

  • The answer is no, more often we should choose the way of Consumer.

Why is that? Because the Consumer refreshes the entire Widget tree, it will rebuild as few widgets as possible.

Method 1: provider. of mode complete code:

  • When we click 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("Called the build method of HYHomePage");
    return Scaffold(
 appBar: AppBar(  title: Text("Provider"),  ),  body: Center(  child: Column(  mainAxisAlignment: MainAxisAlignment.center,  children: <Widget>[  Text("Current count:${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),  ),  );  } } Copy the code

Method 2: Modify the content in the Text in the way of Consumer as follows:

  • You will notice that the build method of HYHomePage will not be called again;
  • If we have the corresponding Child widget, we can organize it as shown in the above example for better performance.
Consumer<CounterProvider>(builder: (ctx, counterPro, child) {
  print("Call Consumer Builder");
  return Text(
    "Current count:${counterPro.counter}".    style: TextStyle(fontSize: 30, color: Colors.red),
 ); }), Copy the code

2.2.3. Selector

Is Consumer the best choice? No, it can have its drawbacks

  • For example, when clicking floatingActionButton, whether the Builder that prints them separately in the code is called again;
  • We’ll see that as soon as we click on floatingActionButton, both positions will be re-Built;
  • But does the floatingActionButton location need to be rebuilt? No, because whether it’s manipulating data, it’s not shown;
  • How can I make it not build again? Use Selector instead of Consumer
The disadvantages of the Select

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("FloatingActionButton displays the location builder being called");
 return FloatingActionButton(  child: child,  onPressed: () {  counterPro.counter += 1;  },  );  },  child: Icon(Icons.add), ), Copy the code

There are three key differences between Selector and Consumer:

  • Key point 1: Generic parameters are two
    • Generic parameter 1: The Provider we will use this time
    • Generic parameter 2: The converted data type. For example, if I still use CounterProvider after the conversion, they are the same type
  • Key point 2: the selector callback function
    • How do you want to convert
    • S Function(BuildContext, A) selector
    • I’m not converting here, so I’m just going to return instance A
  • Key point 3: Whether you want to rebuild
    • This is also a callback function, so we can get two instances before and after the conversion;
    • bool Function(T previous, T next);
    • Because I don’t want it to rebuild, no matter what happens to the data, so I’ll just return false;
The use of the Selector

At this point, we retest the floatingActionButton. The code in floatingActionButton does not rebuild.

So in some cases, we could use a Selector instead of a Consumer, and the performance would be better.

2.2.4. MultiProvider

In development, we need to share more than one data, and we need to organize the data together, so a Provider is definitely not 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;  } } Copy the code

What if we have multiple providers to provide during development?

Method 1: Multiple providers are nested

  • The disadvantages of this approach are that it is not easy to maintain and scalability is poor if there are too many nested layers
  runApp(ChangeNotifierProvider(
    create: (context) => CounterProvider(),
    child: ChangeNotifierProvider(
      create: (context) => UserProvider(),
      child: MyApp()
 ),  )); Copy the code

Method 2: Use MultiProvider

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

Note: All content will be published on our official website. Later, Flutter will also update other technical articles, including TypeScript, React, Node, Uniapp, MPvue, data structures and algorithms, etc. We will also update some of our own learning experiences

The public,