Flutter journey from scratch: Provider

Time:2021-10-23

Flutter journey from scratch: Provider

Previous review

Flying from scratch: statelesswidget

Flying from scratch: statefulwidget

Flyter journey from scratch: inheritedwidget

In the last article, we introducedInheritedWidgetAnd at the end, it raises a problem.

Although the inheritedwidget can provide shared data and release the call of didchangedependencies through getelementforinheritedwidgetofexacttype, the rebuild of countwidget is not avoided and the build is not minimized.

Today we will solve how to avoid unnecessary build and reduce the build to the minimum counttext.

analysis

First, let’s analyze why it leads to the rebuild of the parent widget.

class CountWidget extends StatefulWidget {
  @override
  _CountState createState() {
    return _CountState();
  }
}
 
class _CountState extends State<CountWidget> {
  int count = 0;
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Count App',
      theme: new ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(
          title: Text("Count"),
        ),
        body: Center(
          child: CountInheritedWidget(
            count: count,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                CountText(),
                RaisedButton(
                  onPressed: () => setState(() => count++),
                  child: Text("Increment"),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

For the convenience of analysis, I mentioned the previous code here.

Let’s see. When we click raisedbutton, we will update the count through setstate. At this time, the provider of the setstate method is_ Countstate, that is, countwidget. The change of state will lead to the reconstruction of build. The effect is that the build of countwidget is called again, and then its child widgets are rebuilt one after another.

Now that we know the reason, let’s think about the solution.

  1. At its simplest, we narrow the scope of the setstate provider. Now it’s the countwidget. Let’s narrow it down to column.
  2. Although it has been reduced to column, it is still impossible to avoid the rebuild of its own build and its child widget (raisedbutton) other than counttext. What if we cache all the columns? We set a widget in the outer layer of the column and cache it. Once the outer widget is rebuilt, we all use the column cache, so as not to avoid the rebuilding of the column. However, there will be a problem after using the cache. Since it is the cache, the counttext in the center will not change. In order to solve this problem, we will use the method in the previous articleInheritedWidget。 Put the entire column into the inheritedwidget. Although the column is a cache, the counttext references the count data in the inheritedwidget. Once the count changes, it will be notified to rebuild. This ensures that only counttext is refreshed.

If you are not familiar with inheritedwidget, it is recommended to read itFlyter journey from scratch: inheritedwidget

Let’s summarize by putting a layer of widget on the column and caching the column, and then combining the outer widget with the inheritedwidget to provide the data source of the shared count. Once the count is updated, the setstate of the outer widget will be called and rebuilt, but we use the column cache. At the same time, counttext references the shared count data source through dependency, so the build update will be synchronized. Raisedbutton uses an independent shared count data source, so it will not be rebuilt. This ensures that only counttext is refreshed.

This method is uniformly defined as a provider. In fact, there are already internal providers in fluentProviderHowever, in order to learn the idea of this solution, we will implement a simple version of the provider ourselves. It will be easier to see the provider of fluent later.

The scheme already exists. Let’s look at the specific implementation details directly below.

realization

  1. Defines the providerinheritedwidget for shared data
  2. Defines the notifymodel that listens to refresh
  3. Provides a modelproviderwidget that caches widgets
  4. Assemble and replace the original implementation scheme

ProviderInheritedWidget

Implement your own inheritedwidget, which is mainly used to provide shared data sources and accept cached children.

class ProviderInheritedWidget<T> extends InheritedWidget {
  final T data;
  final Widget child;
 
  ProviderInheritedWidget({@required this.data, this.child})
      : super(child: child);
 
  @override
  bool updateShouldNotify(ProviderInheritedWidget oldWidget) {
    //True - > child widgets that depend on shared data in the notification tree
    return true;
  }
}

NotifyModel

In order to monitor the change of shared data count, we implement it through observer subscription mode.

class NotifyModel implements Listenable {
  List _listeners = [];
 
  @override
  void addListener(listener) {
    _listeners.add(listener);
  }
 
  @override
  void removeListener(listener) {
    _listeners.remove(listener);
  }
 
  void notifyDataSetChanged() {
    _listeners.forEach((item) => item());
  }
}

Listenable provides a simple listening interface to add and remove listeners through add and remove, and then provides a notify method to notify listeners.

Finally, we inherit notifymodel to make count listenable

class CountModel extends NotifyModel {
  int count = 0;
 
  CountModel({this.count});
 
  void increment() {
    count++;
    notifyDataSetChanged();
  }
}

Once the count is incremented, notifydatasetchanged is called to notify the listener of the subscription.

ModelProviderWidget

With the above provider and model, we are providing an external widget to manage them uniformly and combine them.

class ModelProviderWidget<T extends NotifyModel> extends StatefulWidget {
  final T data;
 
  final Widget child;
 
  //The context must be the context of the current widget
  static T of<T>(BuildContext context, {bool listen = true}) {
    return (listen ? context.dependOnInheritedWidgetOfExactType<ProviderInheritedWidget<T>>()
            : (context.getElementForInheritedWidgetOfExactType<ProviderInheritedWidget<T>>()
        .widget as ProviderInheritedWidget<T>)).data;
  }
 
  ModelProviderWidget({Key key, @required this.data, @required this.child})
      : super(key: key);
 
  @override
  _ModelProviderState<T> createState() => _ModelProviderState<T>();
}
 
class _ModelProviderState<T extends NotifyModel>
    extends State<ModelProviderWidget> {
  void notify() {
    setState(() {
      print("notify");
    });
  }
 
  @override
  void initState() {
    //Add listening
    widget.data.addListener(notify);
    super.initState();
  }
 
  @override
  void dispose() {
    //Remove listening
    widget.data.removeListener(notify);
    super.dispose();
  }
 
  @override
  void didUpdateWidget(ModelProviderWidget<T> oldWidget) {
    //Remove old data listener when updating data
    if (oldWidget.data != widget.data) {
      oldWidget.data.removeListener(notify);
      widget.data.addListener(notify);
    }
    super.didUpdateWidget(oldWidget);
  }
 
  @override
  Widget build(BuildContext context) {
    return ProviderInheritedWidget<T>(
      data: widget.data,
      child: widget.child,
    );
  }
}

Here, we provide the data that can be monitored and the child that needs to be cached. At the same time, we conduct monitoring subscription and remove subscription for the data that can be monitored in the appropriate place in the state, and call notify to perform setstate operation when receiving the data change to notify the widget to refresh.

Providerinheritedwidget is referenced in the build to realize data sharing of shared child widgets. At the same time, the of method is provided in modelproviderwidget to expose the unified acquisition method of providerinheritedwidget.

The parameter listen (default true) controls the way to obtain shared data to determine whether to establish a dependency, that is, when the shared data is changed, whether the widget referencing the shared data is rebuilt.

Is this scene a little deja vu? It is basically the details of the use of inherited widget mentioned in the previous article.

The next step is the final alternative

Assemble and replace the original implementation scheme

We obtain the shared data through modelproviderwidget.of, so as long as the shared data is used, this method will be called. In order to avoid unnecessary repeated writing, we separately encapsulate it into the consumer, internally implement the call to it, and expose the result of the call.

class Consumer<T> extends StatelessWidget {
  final Widget Function(BuildContext context, T value) builder;
 
  const Consumer({Key key, @required this.builder}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    print("Consumer build");
    return builder(context, ModelProviderWidget.of<T>(context));
  }
}

When everything is ready, we will optimize the previous code.

class CountWidget extends StatefulWidget {
  @override
  _CountState createState() {
    return _CountState();
  }
}
 
class _CountState extends State<CountWidget> {
  @override
  Widget build(BuildContext context) {
    print("CountWidget build");
    return MaterialApp(
      title: 'Count App',
      theme: new ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(
          title: Text("Count"),
        ),
        body: Center(
          child: ModelProviderWidget<CountModel>(
            data: CountModel(count: 0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Consumer<CountModel>(
                    builder: (context, value) => Text("count: ${value.count}")),
                Builder(
                  builder: (context) {
                    print("RaiseButton build");
                    return RaisedButton(
                      onPressed: () => ModelProviderWidget.of<CountModel>(
                              context,
                              listen: false)
                          .increment(),
                      child: Text("Increment"),
                    );
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

We cache the column in the modelproviderwidget and share the countmodel data at the same time; Encapsulate text through the consumer and reference the count in the shared data countmodel.

For raisedbutton, it only provides clicks and triggers the auto increment operation of count without any changes on the UI. Therefore, in order to avoid rebuilding the shared data referenced by raisedbutton during auto increment, set the listen parameter to false here.

Finally, we run the above code. When we click the increase button, the console will output the following log:

Flutter journey from scratch: Provider

I/flutter ( 3141): notify
I/flutter ( 3141): Consumer build

It indicates that only the consumer has called build again, that is, text has been refreshed. No other widgets have changed.

This solves the problems mentioned at the beginning and minimizes the refresh of widgets.

The above is a simple use of provider consumer. Flutter has a more perfect implementation scheme for this piece. However, after this round of analysis, it will be easier to understand the source code of the provider in fluent.

If you want to know about the use of provider in fluent, you canflutter_githubTo understand its specific practical skills.

To view the actual skills of the provider, you need to switch the branch tosample_provider

recommendable projects

Here is a complete flutter project, which is a good entry for novices.

flutter_github, this is a GitHub client based on fluent. It supports both Android and IOS, account password and authentication login. Dart language is used for development, and the project architecture is MSVM based on Model / state / ViewModel; Use navigator to jump to the page; The network framework uses DIO. The project is constantly updated. Those interested can pay attention to it.

Flutter journey from scratch: Provider

Of course, if you want to know about Android native, believe itflutter_githubPure Android version ofAwesomeGithubIs a good choice.

If you love my article mode or interest in my next article, I suggest you pay attention to my WeChat official account: Android supply station.

Or scan the QR code below to establish effective communication with me and receive my update push faster and more accurately.

Flutter journey from scratch: Provider