After all these articles, the path of Flutter state management is finally coming to an end.

In fact, the previous said so much, the final conclusion is still — Provider really sweet. After all, this is the official recommended solution for state management, and for now, providers can be used for state management in most scenarios, and are generally the best solution.

Google’s style is true. They don’t give any specific solution, let a hundred flowers bloom, and finally choose a better change, and this is the official solution!

But why are we talking about so many other state management schemes? In fact, there are not many. If you look through the previous articles, you will find that what I have talked about are all the original schemes of Flutter. As for third-party schemes such as Redux and Scope_model, I have not actually covered them. The reason for this is the hope that readers can understand from fundamental principle “what is a state management”, “how to manage the state” and “state management the advantages and disadvantages of various options”, only by understanding these, again using the Provider for state management, is so simple, just call API you will to know the root cause, That is the core of this series.

Provider is the state management solution officially provided by Flutter. The basic principle of Flutter is to use the InheritedWidget and Pub address as shown below.

Github.com/rrousselGit…

The introduction of

The iteration of Provider is fast. The latest version is 4.x. Add the dependency of Provider in pubspec.yaml, as shown below.

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  provider: ^4.3.2+1 
Copy the code

After performing pub get, you can update the Provider library.

The core of Provider is actually InheritedWidget, which is actually the encapsulation of InheritedWidget, so that the InheritedWidget can be used more conveniently by developers in data management.

So if your InheritedWidget is familiar, there’s a certain sense of deja vu when using a Provider.

Create a DataModel

Before using a Provider, you need to process the Model to provide notifyListeners with mixins.

class TestModel with ChangeNotifier { int modelValue; int get value => modelValue; TestModel({this.modelValue = 0}); void add() { modelValue++; notifyListeners(); }}Copy the code

The Model manages shared data and provides methods for modifying it. The only difference is that notifyListeners() are provided by the ChangeNotifier to refresh the data.

ChangeNotifierProvider

Use the ChangeNotifierProvider to maintain the data to be managed, as shown in the following code.

class ProviderState1Widget extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => TestModel(modelValue: 1), child: Padding( padding: Const EdgeInsets. All (8.0), the child: the Column (mainAxisAlignment: mainAxisAlignment center, the children: <Widget>[ ChildWidget1(), SizedBox(height: 24), ChildWidget2(), ], ), ), ); }}Copy the code

Create the initialized Model using the ChangeNotifierProvider create function. Create an InheritedWidget with its Child.

Providers there are many different types of providers, so we will only use ChangeNotifierProvider here

Provider of management data

To manage data through Provider, you can use provider. of<TestModel>(context). To read the data, as shown below.

var style = TextStyle(color: Colors.white); class ChildWidget1 extends StatelessWidget { @override Widget build(BuildContext context) { debugPrint('ChildWidget1 build'); var model = Provider.of<TestModel>(context); return Container( color: Colors.redAccent, height: 48, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text('Child1', style: style), Text('Model data: ${model.value}', style: style), RaisedButton( onPressed: () => model.add(), child: Text('add'), ), ], ), ); }}Copy the code

You can also get the operation data method add() from model.

The effect is shown below.

This completes one of the simplest ways to use a Provider.

But you can see from the log that every time Provider. Of <TestModel>(context) is called; Will cause the Widget where the Context is located to Rebuild.

I/flutter (18490): ChildWidget2 build
I/flutter (18490): ChildWidget1 build
Copy the code

Is it deja vu again? Yes, this is mentioned in the previous articles that dependOnInheritedWidgetOfExactType problem, it will record, callers when data update, to rebuild operation data.

In addition, the above example actually hides a problem that is easy for beginners to overlook. Let’s take a look at this code.

RaisedButton(
  onPressed: () => model.add(),
  child: Text('add'),
),
Copy the code

In the button click event, we do not use provider.of <TestModel>(context).add(). We extract provider.of <TestModel>(context) from each call.

In fact, you can try this call, click, will be an error, as shown below.

Tried to listen to a value exposed with provider, from outside of the widget tree.

This is likely caused by an event handler (like a button's onPressed) that called
Provider.of without passing `listen: false`.

To fix, write:
Provider.of<TestModel>(context, listen: false);

It is unsupported because may pointlessly rebuild the widget associated to the
event handler, when the widget tree doesn't care about the value.
Copy the code

Provider. Of is triggered in the Button event Handler, but the Context passed in is not in the Widget, causing the notifyListeners to fail.

There are two solutions. One is to extract provider.of and use the Widget Context to retrieve the Model. The other is to use another parameter of provider.of to remove the listening registration.

RaisedButton(
  onPressed: () => Provider.of<TestModel>(context, listen: false).add(),
  child: Text('add'),
),
Copy the code

By listening: false, the default registered listener is removed.

The default implementation of Provider. Of has listen = true, and you can see why in this discussion.

Github.com/rrousselGit… Github.com/rrousselGit…

Therefore, we summarize two rules for using providers.

  • Provider. Of

    (context) : used for scenarios that need to be refreshed automatically based on data changes
  • Provider. Of

    (context, listen: false) : used for scenarios where you just need to trigger actions in Model without caring about refreshing

Accordingly, in the new version of Provider, the author also provides extension functions for two contexts to further simplify calls.

  • T watch<T>()
  • T read<T>()

They correspond to the above two usage scenarios respectively, so in the above example, the way Text gets data, and the way Button clicks can also be written in the following form.

Text('watch: ${context.watch<TestModel>().value}', style: style)

RaisedButton(
  onPressed: () => context.read<TestModel>().add(),
  child: Text('add'),
),
Copy the code

Code address Flutter dojo-backend-providerState1Widget

Consumer of management data

There are two ways to obtain the data Model managed by the Provider. One is through provider.of <T>(context), and the other is through Consumer. When designing Consumer, the author gives it two functions.

  • When we pass in a BuildContext that doesn’t have a specified Provider, the Consumer allows us to get data from the Provider (because the Provider uses an InheritedWidget, so we can only iterate through the parent Widget). The InheritedWidget cannot be found when the Widget corresponding to the specified Context is in the same Context as the Provider.
  • Provide more detailed data refresh range to avoid unnecessary refresh

Create a new Context

First, let’s look at the first feature.

@override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => TestModel(modelValue: 1), child: Center(child: Padding(Padding: const EdgeInsets. All (8.0), child: Container(color: color) Colors.redAccent, height: 48, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text('Child1', style: style), Text('Model data: ${Provider.of<TestModel>(context).value}', style: style), RaisedButton( onPressed: () => Provider.of<TestModel>(context, listen: false).add(), child: Text('add'), ), ], ), ), ), ), ); }Copy the code

In the example above, the ChangeNotifierProvider and the Widget that uses the Provider use the same Context, so we must not be able to find the corresponding InheritedWidget, so an error is reported.

The following ProviderNotFoundException was thrown building ProviderState2Widget(dirty):
Error: Could not find the correct Provider<TestModel> above this ProviderState2Widget Widget

This likely happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:

- The provider you are trying to read is in a different route.

  Providers are "scoped". So if you insert of provider inside a route, then
  other routes will not be able to access that provider.

- You used a `BuildContext` that is an ancestor of the provider you are trying to read.

  Make sure that ProviderState2Widget is under your MultiProvider/Provider<TestModel>.
  This usually happen when you are creating a provider and trying to read it immediately.
Copy the code

The solution is simple. One is to extract the Widget that you need to use the Provider and put it into a new Widget, so that the Widget has its own Context, and the other is to create a new Context with the Consumer. The code is shown below.

@override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => TestModel(modelValue: 1), child: Center(child: Padding(Padding: const EdgeInsets. All (8.0), child: Container(color: color) Colors.redAccent, height: 48, child: Consumer<TestModel>( builder: (BuildContext context, value, Widget child) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text('Child1', style: style), Text('Model data: ${value.value}', style: style), RaisedButton( onPressed: () => Provider.of<TestModel>(context, listen: false).add(), child: Text('add'), ), ], ); }, (), (), (), (); }Copy the code

Control more detailed refresh range

Take a look at the following example.

class ProviderState2Widget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => TestModel(modelValue: 1),
      child: NewWidget(),
    );
  }
}

class NewWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Container(
          color: Colors.redAccent,
          height: 48,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              Text('Child1', style: style),
              Text('Model data: ${Provider.of<TestModel>(context).value}', style: style),
              RaisedButton(
                onPressed: () => Provider.of<TestModel>(context, listen: false).add(),
                child: Text('add'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
Copy the code

When Provider. Of is called, the Widget in the Context scope is rebuilt, as we saw in the previous example. The problem can be solved by wrapping the widgets that need to be refreshed with Consumer listeners. Therefore, when receiving notifyListeners, only the widgets in the Consumer range are refreshed, and the rest of the listeners are not refreshed. In the Consumer Builder, you can get data objects for the specified generics, as shown below.

@override Widget build(BuildContext context) { return Center( child: Padding( padding: Const EdgeInsets. All (8.0), child: Container(color: Colors. RedAccent, height: 48, child: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text('Child1', style: style), Consumer<TestModel>( builder: (BuildContext context, value, Widget child) { return Text( 'Model data: ${value.value}', style: style, ); }, ), RaisedButton( onPressed: () => Provider.of<TestModel>(context, listen: false).add(), child: Text('add'), ), ], ), ), ), ); }Copy the code

Code address Flutter dojo-backend-providerState2Widget

So why is Consumer able to achieve more detailed refresh control? In fact, the principle is very simple, and has even been mentioned before, that is, “when calling Provider. Of, Widget in Context will be rebuilt”. Therefore, you just need to narrow the scope of call as much as possible, then the scope of Rebuild will be smaller. Take a look at the source code for Consumer.

It can be found that Consumer is encapsulated by a Builder, and ultimately it is called provider. of<T>(context).

more Consumer

There are several types of variations in Consumer that represent the way data is retrieved using multiple data models, as shown in the figure.

To put it more simply, fetching multiple data models of different types simultaneously in a Consumer Builder is a simple way of writing, and it is a process of flattening out nested processes. The source code only writes Consumer6, which supports up to six data types at a time. If you want to support more, you need to implement it yourself.

Manage the Selector of the data

Selector is also a way to get data. In theory, Selector is equal to Consumer and Provider. Of, but the granularity of control over data is the fundamental difference between them.

The way we get data, from provider.of, to Consumer, to Selector, has actually evolved this way.

  • Provider. Of: Context is rebuilt
  • Consumer: Model Content changes to Rebuild
  • Selector: Changes to the specified content in Model to Rebuild

It can be found that although all the data are acquired, the precision of their control is increasing.

So let’s do an example of how to use Selector.

First, we define a data model, as shown below.

class TestModel with ChangeNotifier { int modelValueA; int modelValueB; int get valueA => modelValueA; int get valueB => modelValueB; TestModel({this.modelValueA = 0, this.modelValueB = 0}); void addA() { modelValueA++; notifyListeners(); } void addB() { modelValueB++; notifyListeners(); }}Copy the code

In this data model, two types of data are managed, modelValueA and modelValueB.

Here is the display interface.

class ProviderState3Widget extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => TestModel(modelValueA: 1, modelValueB: 1), child: Padding( padding: Const EdgeInsets. All (8.0), the child: the Column (mainAxisAlignment: mainAxisAlignment center, the children: <Widget>[ ChildWidgetA(), SizedBox(height: 24), ChildWidgetB(), ], ), ), ); } } var style = TextStyle(color: Colors.white); class ChildWidgetA extends StatelessWidget { @override Widget build(BuildContext context) { debugPrint('ChildWidgetA build'); var model = Provider.of<TestModel>(context); return Container( color: Colors.redAccent, height: 48, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text('ChildA', style: style), Text('Model data: ${model.valueA}', style: style), RaisedButton( onPressed: () => model.addA(), child: Text('add'), ), ], ), ); } } class ChildWidgetB extends StatelessWidget { @override Widget build(BuildContext context) { debugPrint('ChildWidgetB  build'); var model = Provider.of<TestModel>(context); return Container( color: Colors.blueAccent, height: 48, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text('ChildB', style: style), Text('Model data: ${model.valueB}', style: style), RaisedButton( onPressed: () => model.addB(), child: Text('add'), ), ], ), ); }}Copy the code

The effect is shown below.

Under the code above, whether we click Add for ChildA or Add for ChildB, the entire interface will Rebuild. Even through Consumer, only corresponding data cannot be refreshed, because they share the same data model. Consumer can only update and refresh data model level, but cannot update for the transformation of different fields in the same data model.

So, the Consumer solution is to split this data schema into two, ModelA and ModelB, This uses MultiProvider to manage ChangeNotifierProvider(ModleA) and ChangeNotifierProvider(ModelB), and Consumer to manage ModelA and ModelB, respectively. In this way, complementary interference can be refreshed.

What if the data model can’t be split? And at that point, we’re ready to use a Selector, so let’s look at the constructor of a Selector.

  • A represents the incoming data source, such as the TestModel above
  • S stands for some property in the A data source that you want to listen on, such as ModelA of TestModel
  • The function of selector is to select the data S to be monitored from the data source A, and then pass S to the Builder for construction
  • ShouldRebuild is used to override the default comparison algorithm and can be left unchecked

The comparison algorithm is shown below.

From the source code, it can be found that the criterion for Selector judgment is whether the old and new data Model is “==”. If it is Collection type, it is compared through DeepCollectionEquality. The official recommendation is pub.flutter-io.cn/packages/tu… To simplify the judgment

With Selector, you can filter out different refresh conditions in the same data model according to the conditions, so that you can avoid the whole data model refresh caused by the transformation of an attribute in the data model.

So I’m going to modify this code with Selector.

class ChildWidgetA extends StatelessWidget { @override Widget build(BuildContext context) { debugPrint('ChildWidgetA build'); return Selector<TestModel, int>( selector: (context, value) => value.modelValueA, builder: (BuildContext context, value, Widget child) { return Container( color: Colors.redAccent, height: 48, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text('ChildA', style: style), Text('Model data: $value', style: style), RaisedButton( onPressed: () => context.read<TestModel>().addA(), child: Text('add'), ), ], ), ); }); }}Copy the code

In this way, we can avoid Model Rebuild problems caused by different data refreshes in the same Model. For example, the Selector above specifies that we need to look for int data in TestModel. ShouldRebuild (the default implementation) returns whether ChildWidgetA needs Rebuild in this case.

Similar to provider. of, providers provide buildContext-based extensions to simplify the use of selectors after 4.1. For example, the above code is implemented through the Selector extension function, as shown below.

class ChildWidgetB extends StatelessWidget { @override Widget build(BuildContext context) { debugPrint('ChildWidgetB build'); return Container( color: Colors.blueAccent, height: 48, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text('ChildB', style: style), Builder( builder: (BuildContext context) { return Text( 'Model data: ${context.select((TestModel value) => value.modelValueB)}', style: style, ); }, ), RaisedButton( onPressed: () => context.read<TestModel>().addB(), child: Text('add'), ), ], ), ); }}Copy the code

Note that you need to create a subclass Context with Builder to avoid refreshing the current Context.

more Selector

Just like Consumer, there are many different implementations of selectors.

In fact, it is very simple to implement a variety of different data types, in these data models, find the need to listen to the type, this situation is more commonly used in multiple data models with specific parameters in common.

Here are three ways to obtain managed data through providers: provider. of, Consumer, and Selector. They all function exactly the same, differing only in the granularity of control over the refresh.