State management is very important in Flutter, but it is very extensive.

What are states and state management? Then we will understand the use of the official state management library Provider, and finally analyze the secret behind Provider.

State management

state

Flutter is declaratively programmed. The UI defined by the Widget is implemented in the build() function, which transforms the state into the UI.

UI = f(state)

The official definition of status is as follows:

whatever data you need in order to rebuild your UI at any moment in time

State is the data needed to reconstruct the UI in any scenario at any time.

There are at least two implications:

  1. State is data;
  2. State changes drive UI changes.

Classification of states

We can divide state into local state and global state.

Local State is the State held internally within a Widget, typically represented by StatefuleWidget and its corresponding State. Local state only affects the UI rendering of a single Widget.

A state is global when it needs to be used across widgets or across the entire APP. An example of global state is an InheritedWidget.

We’ve covered InheritedWidget in detail in our use of InheritedWidget and source code analysis article, but we’ve also mentioned some of its imperfections.

State management library

By state management libraries, we mean libraries that handle global state. In addition to inheritedWidgets, there are some libraries that are very popular these days:

  • flutter_bloc

It is currently the highest rated library for large projects. However, it has the disadvantage of being difficult to understand and writing code in a unique way, requiring repetitive code templates.

  • Provider

It is a project co-maintained by the official Flutter team. Due to its official background, there is no need to worry about future maintenance and upgrades.

  • getx

Getx is one of the fastest growing libraries, very simple to use, very simple code, and a lot of functionality.

There are other libraries, such as Mobx, Flutter_redux, which you probably won’t use.

We will introduce the use and source code of Provider and getX libraries.

The use of the Provider

Similar to the InheritedWidget example, this article uses a simple counter example to describe providers: There is a global state of number that is used by three widgets, and clicking FloatingActionButton increments the value of Number by one. The effect is as follows:

Of course, complex multi-interface logic is implemented in the same way. For example, the following functions are implemented:

The basic use

Before use, it must be introduced into the storage:

Dependencies: the provider: ^ 5.0.0Copy the code

Here are three steps to understand its use:

  1. Encapsulate the number in the ChangeNotifier to create the state to be shared
class NumberModel extends ChangeNotifier { int _number = 0; int get number => _number; set number(int value) { _number = value; notifyListeners(); }}Copy the code

ChangeNotifier is the base class of the Flutter Framework, not a Provider library class. ChangeNotifier is a descendant of Listenable, so ChangeNotifier can notify observers of value changes (implementing observer mode).

NumberModel has a _number state and then provides methods to get and set.

  1. Add the ChangeNotifierProvider at the top of your application
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (ctx) => NumberModel(),
      child: MyApp(),
    ),
  );
}
Copy the code

Set the top layer of your app to ChangeNotifierProvider, and then make MyApp() its child Widget.

The ChangeNotifierProvider create function needs to return ChangeNotifier.

  1. Other widgets use shared state

There are four places to use shared state, three Text widgets to display Text and FloatingActionButton.

  • Provider.of
Class NumberWidget1 extends StatelessWidget {@override Widget Build (BuildContext Context) {// Get the number of NumberModel int  number = Provider.of<NumberModel>(context).number; Return Container(child: Text(" Clicktimes: $number", style: TextStyle(fontSize: 30),); }}Copy the code

We encapsulate the Text Widget as NumberWidget1 with int number = provider.of

(context).number; Get the number value of the NumberModel, and you can display it.

Class HomePage extends StatelessWidget {@override Widget Build (BuildContext Context) {// 1 Gets NumberModel NumberModel model = Provider.of<NumberModel>(context); return Scaffold( appBar: AppBar( title: Text("Provider"), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]), ), floatingActionButton: FloatingActionButton(onPressed: () {// 2 Change the number value model.number++; }, child: Icon(Icons.add), )); }}Copy the code

FloatingActionButton also uses the Provider. Of

(context) method to get the NumberModel and then calls the set method to change the value of number.

Full code:

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
          primarySwatch: Colors.blue, splashColor: Colors.transparent),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    
    NumberModel model = Provider.of<NumberModel>(context);

    return Scaffold(
        appBar: AppBar(
          title: Text("Provider"),
        ),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            model.number++;
          },
          child: Icon(Icons.add),
        ));
  }
}

class NumberWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    int number = Provider.of<NumberModel>(context).number;
    return Container(
      child: Text(
        "点击次数: $number",
        style: TextStyle(fontSize: 30),
      ),
    );
  }
}

class NumberModel extends ChangeNotifier {
  int _number = 0;

  int get number => _number;

  set number(int value) {
    _number = value;
    notifyListeners();
  }
}
Copy the code
  • Consumer

Problem: The problem with provider.of is that when the state value changes, the entire build method of the Widget in which provider.of is located is rebuilt.

In the example above, FloatingActionButton causes the Scaffold to refactor and therefore has the greatest impact on performance.

Consumer<NumberModel>( builder: (context, value, child) { return FloatingActionButton( onPressed: () { value.number++; }, child: Icon(Icons.add), ); },)Copy the code

We wrap FloatingActionButton in Consumer, and the value parameter in the builder is the NumberModel we need.

Here we can optimize it even further by reusing the child.

Consumer<NumberModel>(
    builder: (context, value, child) {
        return FloatingActionButton(
            onPressed: () {
                value.number++;
            },
            child: child,
        );
        },
    child: Icon(Icons.add),
));
Copy the code

We can reuse the child by passing it into the Consumer constructor.

The logic for child reuse was explained in the previous article on animation source code, which you can refer back to if necessary.

The code for the difference part is as follows:

class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Provider"), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]), ), floatingActionButton: Consumer<NumberModel>( builder: (context, value, child) { return FloatingActionButton( onPressed: () { value.number++; }, child: child, ); }, child: Icon(Icons.add), )); }}Copy the code
  • Consumer

Problem: Consumer always needs to be refactored. In fact, when we use FloatingActionButton, we only use the NumberModel setting method. We don’t use the _number property at all, so we don’t need to refactor even if the _number changes.

If we don’t need refactoring, we can use Selector:

Selector<NumberModel, NumberModel>(
    selector: (ctx, numberModel) => numberModel,
    shouldRebuild: (previous, next) => false,
    builder: (context, value, child) {
        return FloatingActionButton(
            onPressed: () {
                value.number++;
            },
        child: child,
        );
    },
    child: Icon(Icons.add),
)
Copy the code

Code explanation:

  1. There are two parameter types in a Selector’s generic type, the first is the original type, and the second is the converted type, which means that a Selector has the ability to convert data;
  2. selectorIs a function that converts data types;
  3. shouldRebuildIs it really necessary to refactor, we obviously do not need, so passfalse;
  4. builderandConsumerIs similar in function.

The code for the difference part is as follows:

class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Provider"), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]), ), floatingActionButton: Selector<NumberModel, NumberModel>( selector: (ctx, numberModel) => numberModel, shouldRebuild: (previous, next) => false, builder: (context, value, child) { return FloatingActionButton( onPressed: () { value.number++; }, child: child, ); }, child: Icon(Icons.add), )); }}Copy the code

The use of multiple states

In some cases, a Widget may need to use more than one state, and we’ll show you how to use this situation.

  1. Create multiple states that need to be shared
class RandomNumberModel extends ChangeNotifier { int _randomNumber = Random().nextInt(100); int get randomNumber => _randomNumber; void resetRandomNumber() { _randomNumber = Random().nextInt(100); notifyListeners(); }}Copy the code

Let’s create a RandomNumberModel with a random number _randomNumber and set the get method and resetRandomNumber method.

  1. Change the top-level of your application to MultiProvider
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<NumberModel>(
          create: (ctx) => NumberModel(),
        ),
        ChangeNotifierProvider<RandomNumberModel>(
          create: (ctx) => RandomNumberModel(),
        ),
      ],
      child: MyApp(),
    ),
  );
}
Copy the code

MultiProvider providers place shared providers.

  1. Other widgets use shared state
  • Provider.of
Class NumberWidget1 extends StatelessWidget {@override Widget Build (BuildContext Context) {// Reads int number = Provider.of<NumberModel>(context).number; Provider = Provider. Of <RandomNumberModel>(context).randomNumber; Return Container(// use child: Text(" click times: $number randomNumber: $randomNumber", style: TextStyle(fontSize: 30),); }}Copy the code

We can fetch NumberModel and RandomNumberModel via provider.of, and then read the corresponding values.

  • Consumer2
class NumberWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Consumer2<NumberModel, RandomNumberModel>(
        builder: (context, value, value2, child) {
          return Text("点击次数: ${value.number}  随机数: ${value2.randomNumber}",
              style: TextStyle(fontSize: 30));
        },
      ),
    );
  }
}

Copy the code

The two generics in Consumer2 represent which two pieces of data to use,value in the build method is NumberModel,value2 is RandomNumberModel, and the corresponding values are read.

  • Selector2
class NumberWidget3 extends StatelessWidget { @override Widget build(BuildContext context) { return Container( child: Selector2<NumberModel, RandomNumberModel, Tuple2<int, int>>( selector: (ctx, value1, value2) => Tuple2(value1.number, value2.randomNumber), builder: (context, value, child) {return Text(" Click times: ${value.item1} random number: ${value.item2}", style: TextStyle(fontSize: 30)); }, shouldRebuild: (previous, next) => previous ! = next, ) ); }}Copy the code
  1. Selector2There are three generic parameters:NumberModelandRandomNumberModelRepresents the two data types used. The third parameter represents the new data type converted from the first two data types. We need to use twointValue.

To use Tuple2, you need to import the tripartite library tuple: ^2.0.0. The advantage of using it is that it has the built-in == comparison operator, which does not require us to compare elements for equality.

  1. selectorThe three parameters of are:BuildContext.NumberModelandRandomNumberModelThe return value is the converted data.

Item1 and value.item2 can be used directly in the Builder method.

  1. shouldRebuildmethodspreviousandnextIs of typeTuple2<int, int>Can be directly compared. If they’re the same, they don’t refactor.

Multiple state use supplements

Consumer2 has several other Cousins: Consumer3, Consumer4, Consumer5, Consumer6.

Selector2 also has several brothers: Selector3, Selector4, Selector5, Selector6.

As you can see from their names, they can combine corresponding data.

Full code:

void main() { runApp( MultiProvider( providers: [ ChangeNotifierProvider<NumberModel>( create: (ctx) => NumberModel(), ), ChangeNotifierProvider<RandomNumberModel>( create: (ctx) => RandomNumberModel(), ), ], child: MyApp(), ), ); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, splashColor: Colors.transparent), home: HomePage(), ); } } class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Provider"), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [NumberWidget1(), NumberWidget2(), NumberWidget3()]), ), floatingActionButton: Consumer2<NumberModel, RandomNumberModel>( child: Icon(Icons.add), builder: (context, value, value2, child) { return FloatingActionButton( onPressed: () { value.number++; value2.resetRandomNumber(); }, child: child, ); },),); } } class NumberWidget1 extends StatelessWidget { @override Widget build(BuildContext context) { int number = Provider.of<NumberModel>(context).number; int randomNumber = Provider.of<RandomNumberModel>(context).randomNumber; Return Container(child: Text(" click times: $number randomNumber: $randomNumber", style: TextStyle(fontSize: 30),); } } class NumberWidget2 extends StatelessWidget { @override Widget build(BuildContext context) { return Container( Child: Consumer2<NumberModel, RandomNumberModel>(Builder: (context, value, value2, child) {return Text(" ${value2. number} randomNumber: ${value2.randomNumber}", style: TextStyle(fontSize: 30); },),); } } class NumberWidget3 extends StatelessWidget { @override Widget build(BuildContext context) { return Container( child: Selector2<NumberModel, RandomNumberModel, Tuple2<int, int>>( selector: (ctx, value1, value2) => Tuple2(value1.number, value2.randomNumber), builder: (context, value, child) {return Text(" Click times: ${value.item1} random number: ${value.item2}", style: TextStyle(fontSize: 30)); }, shouldRebuild: (previous, next) => previous ! = next, ) ); } } class NumberModel extends ChangeNotifier { int _number = 0; int get number => _number; set number(int value) { _number = value; notifyListeners(); } } class RandomNumberModel extends ChangeNotifier { int _randomNumber = Random().nextInt(100); int get randomNumber => _randomNumber; void resetRandomNumber() { _randomNumber = Random().nextInt(100); notifyListeners(); }}Copy the code

Provider source code analysis

  • The basic architecture of the Provider is as follows:

  1. All providers inherit from InheritedProvider;
  2. InheritedProviderHolds a _CreateInheritedProvider object_delegate._delegateBy holding _ValueInheritedProviderState object, _ValueInheritedProviderState objectcreateState()Method calledInheritedProviderthecreate()Method generates_value._valueThat is, the developer provides a monitorChangeNotifier;

Create () is called only when _value is needed, not when InheritedProvider inserts the Widget Tree, which is a lazy implementation.

  1. InheritedProviderThere is aInheritedWidgettheThe child widgets_InheritedProviderScope. _InheritedProviderScope holds the one mentioned above_valueThe value of the;

The Provider relies on the InheritedWidget, and when you find the InheritedWidget, you get the value of the _value.

  1. WidgetWhen you’re refactoring, if you callProvider.ofMethods will be found_valueAnd listen for it to change.
  • The partial refresh logic of the Provider is as follows:

  1. _valueWhen the value changes, the listener is notified to refresh. Where _InheritedProviderScope is calledmarkNeedsNotifyDependentsMethod to invoke a dependencyWidgetthedidChangeDependencies, both methods are calledmarkNeedsBuild(), for reconstruction;
  2. WidgetCalled during refactoringProvider.ofMethod, update pairs_valueFor the next refactoring.
  • Optimization logic for Consumer and Selector:

Consumer and the Selector is just one layer encapsulates the SingleChildStatefulWidget, reconstruct the scope of the restricted within the Consumer and the Selector, internal calls or Provider. Of method.

  • The logic of MultiProvider:

A MultiProvider is a nested Provider that is no different from a single Provider.

conclusion

Actually the Provider library also provides several other Provider, ListenableProvider, ValueListenableProvider, StreamProvider and FutureProvider, they are all the options of our development.

At this point, we have explained how to use the Provider library and the underlying logic.