为了账号安全,请及时绑定邮箱和手机立即绑定

[- Flutter-技能篇 -] 使用Provider前你应了解Consumer

标签:
WebApp

Flutter的状态管理三足鼎立,明媒正室当Provider莫属,可谓刘备级别的大佬,名正言顺。作为一个喜欢偷懒的人,能省则省。都知道Provider有一把梭,打遍天下无敌手。不过刷这两招,可要悠着点,否则代价就是性能。

Provider.of<XXX>(context).数据
Provider.of<XXX>(context).方法

一、一把梭

页面如下,第一个界面是四个色块,点击蓝色字时跳到紫色界面
这里进行了五次操作:状态同步实现,貌似表面上完美无瑕,而且一把梭就OK了,也很方便,BUT!!!----往下翻。

1:打开界面
2:点击按钮,+1
3:点击蓝块文字,跳转界面
4:点击紫块,触发方法
5:返回


https://img1.sycdn.imooc.com//5daac51c0001351d12750377.jpg


class CountState with ChangeNotifier {
  int _count = 0;
  get count => _count;
  void increment() {
    _count++;
    notifyListeners();
  }
}

void main() {
  final count = CountState();
  runApp(MultiProvider(
    providers: [ChangeNotifierProvider.value(value: count)],
    child: MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: Colors.blue,
      ),
      home: new HomePage(),

    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Wrap(
          spacing: 10,
          runSpacing: 10,
          children: <Widget>[
            RedBox(),YellowBox(),BlueBox(),GreenBox(),
          ],
        ),
      ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            Provider.of<CountState>(context).increment();
          },
          child: Icon(Icons.add),
        )
    );
  }
}

class RedBox extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("---------RedBox---------build---------");
    return Container(
      color: Colors.red,
      width: 150,
      height: 150,
      alignment: Alignment.center,
      child: Text("Red:${Provider.of<CountState>(context).count}",
        style: TextStyle(fontSize: 20),),
    );
  }
}

class YellowBox extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("---------YellowBox---------build---------");
    return Container(
      color: Colors.yellow,
      width: 150,
      height: 150,
      alignment: Alignment.center,
      child: Text("Yellow:${Provider.of<CountState>(context).count}",
          style: TextStyle(fontSize: 20)),
    );
  }
}

class BlueBox extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("---------BlueBox---------build---------");
    return Container(
      color: Colors.blue,
      width: 150,
      height: 150,
      alignment: Alignment.center,
      child: InkWell(
        onTap:() {
          Navigator.of(context).push(MaterialPageRoute(builder: (context) => NextPage()));
        },
        child: Text("Blue:${Provider.of<CountState>(context).count}",
            style: TextStyle(fontSize: 20)),
      ),
    );
  }
}

class GreenBox extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("---------GreenBox---------build---------");
    return Container(
      color: Colors.green,
      width: 150,
      height: 150,
      alignment: Alignment.center,
      child: Text("GreenBox:${Provider.of<CountState>(context).count}",
          style: TextStyle(fontSize: 20)),
    );
  }
}


class NextPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("---------NextPage---------build---------");
    return Scaffold(
      body: Center(
        child: InkWell(
          onTap: (){
            Provider.of<CountState>(context).increment();
          },
          child: Container(
              color: Colors.purple,
              width: 150,
              height: 150,
              alignment: Alignment.center,
              child: Text("NextPage:${Provider.of<CountState>(context).count}",
                  style: TextStyle(fontSize: 20))),
        )),
    );
  }
}

打印的日志让我一身冷汗。不知道有多少人为了方便滥用这一把梭。
可见这里在跳转时、五个组件全部触发build,在第二个页面(紫块)执行方法时竟五个组件全部触发build,其中四个都是不可见的组件,build何用?

---->[1.打开时]----
I/flutter (24913): ---------RedBox---------build---------
I/flutter (24913): ---------YellowBox---------build---------
I/flutter (24913): ---------BlueBox---------build---------
I/flutter (24913): ---------GreenBox---------build---------

---->[2.点击+号,触发方法]----
I/flutter (24913): ---------RedBox---------build---------
I/flutter (24913): ---------YellowBox---------build---------
I/flutter (24913): ---------BlueBox---------build---------
I/flutter (24913): ---------GreenBox---------build---------

---->[3.点击蓝块文字,跳转界面]----
I/flutter (24913): ---------NextPage---------build---------
I/flutter (24913): ---------RedBox---------build---------
I/flutter (24913): ---------YellowBox---------build---------
I/flutter (24913): ---------BlueBox---------build---------
I/flutter (24913): ---------GreenBox---------build---------

---->[4.点击紫块,触发方法]----
I/flutter (24913): ---------NextPage---------build---------
I/flutter (24913): ---------GreenBox---------build---------
I/flutter (24913): ---------BlueBox---------build---------
I/flutter (24913): ---------YellowBox---------build---------
I/flutter (24913): ---------RedBox---------build---------

---->[5.返回]----
I/flutter (24913): ---------RedBox---------build---------
I/flutter (24913): ---------YellowBox---------build---------
I/flutter (24913): ---------BlueBox---------build---------
I/flutter (24913): ---------GreenBox---------build---------

这时候使用Consumer包裹需要更新的节点。将四个色块处理

class HomePage extends StatelessWidget { //英雄所见...        floatingActionButton://使用Consumer包裹        Consumer<CountState>(builder: (ctx,state,child)=>FloatingActionButton(          onPressed: () {            state.increment();//使用其内的state执行方法          },          child: Icon(Icons.add),        ))    );  } } class RedBox extends StatelessWidget {  @override  Widget build(BuildContext context) {    print("---------RedBox---------build---------");    return Container(      color: Colors.red,      width: 150,      height: 150,      alignment: Alignment.center,      child: Consumer<CountState>(builder: (ctx,state,child)=>          Text("Red:${state.count}",            style: TextStyle(fontSize: 20),)),    );  } } //其他三个处理类似,略... 

Consumer可以指定小块的局部消费,避免整体的的全部刷新

https://img1.sycdn.imooc.com//5daac38a000172d606470206.jpg


---->[1.打开时]----
I/flutter (24913): ---------RedBox---------build---------
I/flutter (24913): ---------YellowBox---------build---------
I/flutter (24913): ---------BlueBox---------build---------
I/flutter (24913): ---------GreenBox---------build---------

---->[2.点击+号,触发方法]----
无打印信息

---->[3.点击蓝块文字,跳转界面]----
I/flutter (26468): ---------NextPage---------build---------


---->[4.点击紫块,触发方法]----
I/flutter (26468): ---------NextPage---------build---------

---->[5.返回]----
无打印信息

你也许会说,乖乖,这么秀,都不用build了?但不吃饭是长不胖的...且静看下文。


三、Consumer做了什么

1.瞄一下源码注释:

源码第一句说的很清楚:从先祖获取Provider<T>然后传递给builder出的组件
本来代代相承的传家宝直接通过Consumer隔代传送。不做那些花里胡哨的传递。

目的有2:
其一:当没有BuildContext时可以使用Consumer

@override // ERROR:ProviderNotFoundError 因为该context中并没有Provider
Widget build(BuildContext context) {
  return ChangeNotifierProvider(
    builder: (_) => Foo(),
    child: Text(Provider.of<Foo>(context).value),
  );
}

@override // OK 
Widget build(BuildContext context) {
  return ChangeNotifierProvider(
    builder: (_) => Foo(),
    child: Consumer<Foo>(
      builder: (_, foo, __) => Text(foo.value),
    },
  );
}

其二:它通过更细粒度的重构来帮助性能优化。

2.Consumer的builder

通过上面可知其实是创建有构建组件的,只不过是局部构建
这样可以让构建的粒度变细,自然避免了不必要的过程,可以在Builder里打印来测试一下也就是说,构建的只是知识一个Text而非整个RedBox。

class RedBox extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("---------RedBox---------build---------");
    return Container(
      color: Colors.red,
      width: 150,
      height: 150,
      alignment: Alignment.center,
      child: Consumer<CountState>(builder: (ctx,state,child){
        print("---------RedBox----Consumer-----build---------");
        return Text("Red:${state.count}",
            style: TextStyle(fontSize: 20),);
      }),
    );
  }
}

那他是如何实现的呢?一共也不到20行。它继承自StatelessWidget
可以看出有三个字段:key、child和、builder,其中builder是一个三参的方法 既然是StatelessWidget,build方法自然跑不了。可见该方法是由 builder方法全权负责的。T泛型就是状态模型,这里也是通过Provider.of<T>(context),来拿到的。

class Consumer<T> extends StatelessWidget
    implements SingleChildCloneableWidget {
    
  Consumer({
    Key key,
    @required this.builder,
    this.child,
  })  : assert(builder != null),
        super(key: key);

  final Widget child;

  final Widget Function(BuildContext context, T value, Widget child) builder;

  @override
  Widget build(BuildContext context) {
    return builder(
      context,
      Provider.of<T>(context),
      child,
    );
  }

  @override
  Consumer<T> cloneWithChild(Widget child) {
    return Consumer(
      key: key,
      builder: builder,
      child: child,
    );
  }
}

3.你需要认识的BuildContext

那问题来了?传入的context是谁的BuildContext? 众所周知,每个Widget都有属于自己的元素Element,在该Element进行mount的时候回将自身化作美丽的天使(Context)传入组件或State的build方法中来供你使用。这里分别在顶层MyApp的build页面HomePage的build``红色的build红色Consumer内部打上断点,来一窥这四个小天使的容颜。

https://img1.sycdn.imooc.com//5daac3fe0001062906690391.jpg




所以BuildContext并不是我们想象中的,什么代代相传的东西。而是每个Widget特有的存在,就像他们的基因一样,在每个Widget里都是不同的。所以我们的问题很简单,Consumer作为一个Widget,它提供的context便是Consumer的context。下面看一下这几个小天使在界面的Element树上的位置。
再强调一下,Element是实现BuildContext抽象接口协议的具象类,Widget或State中Build传入的BuildContext都是各自的组件对应的Element。每个Element都会记录它们的父亲,就像这样,按照一个BuildContext(即Element),你可以找到它的祖宗18代,应该是祖宗108代。虽然Widget伪树非常简短,但Element树并不想你想象的那样。


https://img1.sycdn.imooc.com//5daac41f0001c84518530767.jpg

也许你会好奇,最顶级的元老是谁?那我们就偷瞄一下,谁是天使之王。
框架在开始是会创建一个 RenderObjectToWidgetElement,她便是一切美丽的根源。
紧接着便是Provider提供的MultiProvider ,我们的MyApp还要后两辈。

https://img1.sycdn.imooc.com//5daac433000190bc16050458.jpg



4.Consumer何德何能?

Consumer何德何能,竟然直接越过父亲? 遇事不决,量子力学,把哥的debug量子炮拿来
在点击时触发方法是打个断点,来走一波。

断点处: 3个 state.increment() 红色Consumer内部 buildScope方法 当第一次点击按钮时: buildScope 中脏表元素 1 ,为按钮元素:RawMaterialButton(dirty) 接下来断点走到state.increment();开始触发通知更新 会走到buildScope,脏表数为3 ListenableProvider<CountState>(dirty, state: _DelegateWidgetState#ae650) RawMaterialButton(dirty, state: _RawMaterialButtonState#80335) _MaterialInterior(duration: 200ms, shape: CircleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none)), elevation: 12.0, c 断点方行,来到[红色Consumer内部]断点,此时控制台已经显示: I/flutter (13300): ---------GreenBox----Consumer-----build--------- I/flutter (13300): ---------YellowBox----Consumer-----build--------- 再次方行此时控制台已经显示: I/flutter (13300): ---------RedBox----Consumer-----build--------- I/flutter (13300): ---------BlueBox----Consumer-----build--------- 此时界面已更新。但按钮还没缓过神 会走到buildScope,脏表数为1, _MaterialInterior(duration: 200ms, 再放行,按钮更新,一次界面的点击刷新完成。

在这幅图中已经浮现大佬的身姿了,它老爹是MutiProvider。


https://img1.sycdn.imooc.com//5daac4650001341f16090457.jpg

在buildScope中,我们的故事便发生在ListenableProvider的rebuild方法里

https://img1.sycdn.imooc.com//5daac4900001475b18600660.jpg


进入后我们到达Element#rebuild()=> ComponentElement#performRebuild()
看到InheritedProvider,我也就会心一笑了。就快打完收工了。


https://img1.sycdn.imooc.com//5daac4b10001696c12800390.jpg



rebuild一波后,脏表加了5个,每错,都是Consumer的节点。只要四个块,为什么有5个?
百思不得其解,最后一句TMD,按钮上也加了Consumer,被自己蠢死。
众所周知,Flutter只会绘制重建脏表里的元素。所以会直接构建Consumer而非整体。


https://img1.sycdn.imooc.com//5daac4c4000154f016820538.jpg



没有对比就没有伤害,最后看一下不用Consumer时重构页面的脏表情况。在rebuild一波后脏表加入的是整个Widget的元素。


https://img1.sycdn.imooc.com//5daac4d60001e94c12820320.jpg


就这样,所以层次较深时,推荐使用Consumer来将更新的粒度变小。





点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
21
获赞与收藏
57

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消