从零开始的Flutter之旅: Provider

作者: 午后一小憩 | 来源:发表于2020-06-22 19:40 被阅读0次

    往期回顾

    从零开始的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补给站.jpg-fc2643-1592825845975-0)]


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

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

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

    相关文章

      网友评论

        本文标题:从零开始的Flutter之旅: Provider

        本文链接:https://www.haomeiwen.com/subject/gzyffktx.html