Review past

The Journey of Flutter from scratch: StatelessWidget

The Journey of Flutter from Scratch: StatefulWidget

A Journey to Flutter from scratch: InheritedWidget

In the previous article, we introduced the InheritedWidget and raised a problem at the end.

Although InheritedWidget can provide Shared data, and remove by getElementForInheritedWidgetOfExactType didChangeDependencies calls, But it still doesn’t avoid rebuilds of CountWidget and doesn’t minimize builds.

Today we will address how to avoid unnecessary build builds and reduce builds to a minimal CountText.

Analysis of the

Let’s first examine why this causes a 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"), a)],),),),),); }}Copy the code

For the sake of analysis, I’ve included the previous code here.

Let’s see, when we click on raise Button, we update the count with setState. The provider of the setState method is _CountState, which is CountWidget. The state change causes the build to be rebuilt, with the effect that CountWidget’s build will be called again and its child widgets will be rebuilt in turn.

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

  1. Most simply, we narrow down the setState provider. Now we have CountWidget, and we’ll zoom it down to Column.
  2. Although it has shrunk down to Column, there is no way to avoid re-building its own build and its child Widget(RaisedButton) beyond CountText. What if we cache all the columns? We put a Widget around Column and cache it. Once the Widget is rebuilt, we use the cache for Column to avoid the rebuild of Column. There is a problem with caching, however. Since it is a cache, CountText in Center will not change. To solve this problem, use the InheritedWidget from the previous article. The entire Column is placed in the InheritedWidget. Although Column is cached, CountText references the count data in the InheritedWidget and will inform it to rebuild if the count changes. This ensures that only CountText is refreshed.

If you’re not familiar with The InheritedWidget, it’s recommended to read the Inherent Widget journey to Flutter from scratch

To recap, wrap a layer of widgets around Column and cache the Column, and then the outer widgets combine with the inheritedWidgets to provide the data source that shares the count. Once the count update calls setState of the outer Widget and builds again, but we are using the Column cache, and CountText relies on the shared count data source to synchronize build updates. RaisedButton uses an undependent shared count data source, so it doesn’t rebuild. This ensures that only CountText is refreshed.

This method is uniformly defined as Provider. Actually, there is a complete implementation of Provider inside Flutter. However, in order to learn the idea of this solution, we will implement a simple version of Provider ourselves. It will be easier to look at the Flutter Provider later.

The solution is already there, so let’s go straight to the implementation details.

implementation

  1. Define the ProviderInheritedWidget for shared data
  2. Define a NotifyModel that listens for refreshes
  3. The ModelProviderWidget that provides cached widgets
  4. Assemble and replace the original implementation

ProviderInheritedWidget

Implement an InheritedWidget of your own that primarily provides shared data sources and accepts 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 in the notification tree that depend on changing shared datareturn true; }}Copy the code

NotifyModel

To listen for changes in the shared data count, we use the 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()); }}Copy the code

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

Finally, we make count listen by inheriting NotifyModel

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

Once the count increases, notifyDataSetChanged is called to notify the subscribed listener.

ModelProviderWidget

With the Provider and Model above, we are providing an external Widget to unify them and combine them.

class ModelProviderWidget<T extends NotifyModel> extends StatefulWidget { final T data; final Widget child; // Context must be the current widget's context 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() {// addListener widget.data.addlistener (notify); super.initState(); } @override voiddispose() {/ / remove monitoring widget. Data. RemoveListener (notify); super.dispose(); } @override void didUpdateWidget(ModelProviderWidget<T> oldWidget) {// Remove the old data listener when data updatesif(oldWidget.data ! = widget.data) { oldWidget.data.removeListener(notify); widget.data.addListener(notify); } super.didUpdateWidget(oldWidget); } @override Widget build(BuildContext context) {returnProviderInheritedWidget<T>( data: widget.data, child: widget.child, ); }}Copy the code

Here we provide the data that can be listened to and the child that needs to be cached. Meanwhile, we monitor and remove the subscription of the data that can be listened to in the state where appropriate. When the data data is changed, we call notify setState to notify the widget to refresh.

The ProviderInheritedWidget is referenced in the build to implement data sharing on shared child widgets, and the of method is provided in the ModelProviderWidget to expose a unified way to get the ProviderInheritedWidget.

The listen parameter (default true) controls how shared data is retrieved to determine whether dependencies are established, that is, whether widgets referencing shared data are rebuilt when shared data changes.

If this scenario sounds familiar, it’s basically all about the details that InheritedWidget uses as mentioned in the previous article.

Then comes the final scheme substitution

Assemble and replace the original implementation

We get the shared data through ModelProviderWidget.of, so this method will be called whenever shared data is used. To avoid unnecessary duplication of writing, we wrapped it separately in the Consumer to implement calls to it internally and expose the results of the calls.

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");
    returnbuilder(context, ModelProviderWidget.of<T>(context)); }}Copy the code

With everything in place, let’s 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")); }, [, [, (), (), (), (), (); }}Copy the code

We cache the Column in the ModelProviderWidget and share the CountModel data; The Consumer encapsulates the Text by referring to the count in the shared data CountModel.

In the case of RaisedButton, it simply provides a click and triggers the increment of count without any UI changes. So to avoid rebuilding the shared data referenced by RaisedButton when incrementing, the listen parameter is set to false.

Finally we run the code above, and when we click the Increment button, the console will output the following log:

I/flutter ( 3141): notify
I/flutter ( 3141): Consumer build
Copy the code

Only the Consumer calls build again, i.e. Text is refreshed. None of the other widgets have changed.

This solves the problem mentioned earlier and minimizes widget refreshes.

The above is a simple provider-consumer use. Flutter has a more complete implementation of this. But after this round of analysis, it will be easier for you to read the source code for The Provider in Flutter.

If you want to learn about the use of Providers in Flutter, check out flutter_github to learn how to use them in action.

To see Provider techniques in action, switch the branch to sample_provider

Recommended project

The following is a complete description of the Flutter project, which is a good introduction to the Flutter for beginners.

Flutter_github is a Github client based on Flutter that supports both Android and IOS, passwords and authentication login. Dart language was used for development, and the project architecture was MSVM based on Model/State/ViewModel. Use Navigator to jump to the page; The network framework uses DIO. The project is being updated continuously, those who are interested can follow it.

Of course, if you want to learn about Android native, flutter_Github’s pure Android version AwesomeGithub is a good choice.

If you like my article mode, or are interested in my next article, I suggest you follow 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.