从零开始的Flutter之旅-Provider

24次阅读

共计 6654 个字符,预计需要花费 17 分钟才能阅读完成。

往期回顾

从零开始的 Flutter 之旅: StatelessWidget

从零开始的 Flutter 之旅: StatefulWidget

从零开始的 Flutter 之旅: InheritedWidget

在上篇文章中我们介绍了 InheritedWidget,并在最后引发出一个问题。

虽然 InheritedWidget 可以提供共享数据,并且通过 getElementForInheritedWidgetOfExactType 来解除 didChangeDependencies 的调用,但还是没有避免 CountWidget 的重新 build,并没有将 build 最小化。

我们今天就来解决如何避免不必要的 build 构建,将 build 缩小到最小的 CountText。

分析

首先我们来分析下为什么会导致父 widget 的重新 build。

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"),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

为了方便分析,我把之前的代码提到这里来。

我们来看,在点击 RaisedButton 的时候,我们会通过 setState 将 count 进行更新。而此时的 setState 方法的提供者是_CountState,即 CountWidget。而 state 的改变会导致 build 的重新构建,导致的效果是 CountWidget 的 build 被重新调用,继而它的子 widget 也相继被重新 build。

既然已经知道了原因,那么我们再来思考下解决方案。

  1. 最简单的,我们缩小 setState 提供者的范围。现在是 CountWidget,我们将其缩小到 Column。
  2. 虽然已经缩小到了 Column,但还是无法避免自身的 build 与其 CountText 之外的子 Widget(RaisedButton) 的重新 build。如果我们将 Column 全部缓存下来呢?我们在 Column 外层套一个 Widget,并将其进行缓存,一旦外层的 Widget 重新 build,我们都使用 Column 的缓存,这样不就避免了 Column 的重新 build。不过使用缓存以后会有个问题,既然是缓存,Center 里面的 CountText 也将不会改变。为了解决这个问题,我们就要使用上篇文章中的 InheritedWidget。将整个 Column 放到 InheritedWidget 中,虽然 Column 是缓存,但是 CountText 中引用了 InheritedWidget 中的 count 数据,一旦 count 发生改变,将会通知其进行重新 build。这样就保证了只刷新 CountText。

如果你对 InheritedWidget 不熟悉,推荐阅读从零开始的 Flutter 之旅: InheritedWidget

我们来总结一下,在 Column 外套一层 Widget,并将 Column 进行缓存,然后外层的 Widget 结合 InheritedWidget 来提供共享 count 的数据源。一旦 count 更新将会调用外层 Widget 的 setState,并且重新 build,但我们使用的是 Column 缓存,同时 CountText 通过依赖的方式引用了共享的 count 数据源,从而会同步 build 更新。而 RaisedButton 使用的是未依赖的共享 count 数据源,所以并不会重新 build。这样就保证了只刷新 CountText。

这种方式统一定义为 Provider,其实 Flutter 内部已经有 Provider 的完整实现,不过我们为了学习这种解决方法的思想,自己来实现一个简易版的 Provider。之后再去看 Flutter 的 Provider 将会更加简单。

方案已经有了,下面我们直接来看具体实现细节。

实现

  1. 定义共享数据的 ProviderInheritedWidget
  2. 定义监听刷新的 NotifyModel
  3. 提供缓存 Widget 的 ModelProviderWidget
  4. 组装替换原有实现方案

ProviderInheritedWidget

实现一个自己的 InheritedWidget,主要用来提供共享数据源,并接受缓存的 child。

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 -> 通知树中依赖改共享数据的子 widget
    return true;
  }
}

NotifyModel

为了监听共享数据 count 的变化,我们通过观察者订阅模式来实现。

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 提供一个简单的监听接口,通过 add 与 remove 来增加与移除监听,然后提供一个 notify 方法来进行通知监听者。

最后我们通过继承 NotifyModel 来使 count 具有可监听能力

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

一旦 count 自增,就调用 notifyDataSetChanged 来通知订阅的监听者。

ModelProviderWidget

有了上面的 Provider 与 Model,我们在提供一个外部 Widget 来统一管理它们,将它们结合起来。

class ModelProviderWidget<T extends NotifyModel> extends StatefulWidget {
  final T data;
 
  final Widget child;
 
  // context 必须为当前 widget 的 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() {
    // 添加监听
    widget.data.addListener(notify);
    super.initState();}
 
  @override
  void dispose() {
    // 移除监听
    widget.data.removeListener(notify);
    super.dispose();}
 
  @override
  void didUpdateWidget(ModelProviderWidget<T> oldWidget) {
    // data 更新时移除老的 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,
    );
  }
}

在这里我们提供可监听的 data 数据与需要缓存的 child,同时在 state 中对可监听的 data 在合适的地方进行监听订阅与移除订阅,并在收到 data 数据改变时调用 notify 进行 setState 操作,通知 widget 刷新。

在 build 中引用了 ProviderInheritedWidget,来实现对共享子 widget 的数据共享,同时在 ModelProviderWidget 中提供 of 方法来暴露 ProviderInheritedWidget 的统一获取方式。

通过参数 listen(默认 true) 来控制获取共享数据的方式,来决定是否建立依赖关系,即共享数据改变时,引用共享数据的 widget 是否重新 build。

这一幕是不是有点似曾相识,基本上都是上篇文章中提到的 InheritedWidget 使用的细节。

接下来就是最终的方案替换

组装替换原有实现方案

我们通过 ModelProviderWidget.of 来获取共享的数据,所以只要使用到了共享数据,将要调用该方法。为了避免不必要的重复书写,我们将其单独封装到 Consumer 中,内部来实现对其的调用,并且将调用的结果暴露出来。

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));
  }
}

一切准备就绪,我们再对之前的代码进行优化。

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"),
                    );
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

我们将 Column 缓存到 ModelProviderWidget 中,同时对 CountModel 数据进行共享;通过 Consumer 进行 Text 的封装,引用共享数据 CountModel 中的 count。

对于 RaisedButton,因为它只是提供点击,并且触发 count 的自增操作、没有发生 ui 上的任何变化。所以为了避免 RaisedButton 引用的共享数据进行自增时重新 build,这里将 listen 参数置为 false。

最后我们运行上面的代码,我们点击 Increment 按钮时,控制台将会输出如下日志:

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

说明只有 Consumer 重新调用了 build,即 Text 进行了刷新。其它的 widget 都没有变化。

这样就解决了开篇提到的疑问,达到了 widget 刷新的最小化。

以上是一个简单地 Provider-Consumer 的使用。Flutter 对这一块有更完善的实现方案。但是经过我们这一轮分析,你再去看 Flutter 中 Provider 的源码将会更加简单易懂。

如果你想了解 Flutter 中 Provider 的使用,你可以通过 flutter_github 来了解它的具体实战使用技巧。

想要查看 Provider 实战技巧,需要将分支切换到 sample_provider

推荐项目

下面介绍一个完整的 Flutter 项目,对于新手来说是个不错的入门。

flutter_github,这是一个基于 Flutter 的 Github 客户端同时支持 Android 与 IOS,支持账户密码与认证登陆。使用 dart 语言进行开发,项目架构是基于 Model/State/ViewModel 的 MSVM;使用 Navigator 进行页面的跳转;网络框架使用了 dio。项目正在持续更新中,感兴趣的可以关注一下。

当然如果你想了解 Android 原生,相信 flutter_github 的纯 Android 版本 AwesomeGithub 是一个不错的选择。

如果你喜欢我的文章模式,或者对我接下来的文章感兴趣,建议您关注我的微信公众号:【Android 补给站】

或者扫描下方二维码,与我建立有效的沟通,同时更快更准的收到我的更新推送。

正文完
 0