[- Flutter-技能篇 -] 使用Provider前你應瞭解Consumer

張風捷特烈 發表於 2019-10-19

本文收錄於張風捷特烈的公眾號程式設計之王: 文章記憶體地址f-s-p-01
如何獲取更多知識乾糧,詳見 <<程式設計之王食用規範1.0>>


Flutter的狀態管理三足鼎立,明媒正室當Provider莫屬,可謂劉備級別的大佬,名正言順。作為一個喜歡偷懶的人,能省則省。都知道Provider有一把梭,打遍天下無敵手。不過刷這兩招,可要悠著點,否則代價就是效能。

Provider.of<XXX>(context).資料
Provider.of<XXX>(context).方法
複製程式碼

一、一把梭

頁面如下,第一個介面是四個色塊,點選藍色字時跳到紫色介面
這裡進行了五次操作:狀態同步實現,貌似表面上完美無瑕,而且一把梭就OK了,也很方便,BUT!!!----往下翻。

1:開啟介面
2:點選按鈕,+1
3:點選藍塊文字,跳轉介面
4:點選紫塊,觸發方法
5:返回
複製程式碼

[- Flutter-技能篇 -] 使用Provider前你應瞭解Consumer

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來幫忙

這時候使用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可以指定小塊的區域性消費,避免整體的的全部重新整理

[- Flutter-技能篇 -] 使用Provider前你應瞭解Consumer

---->[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內部打上斷點,來一窺這四個小天使的容顏。

[- Flutter-技能篇 -] 使用Provider前你應瞭解Consumer

[- Flutter-技能篇 -] 使用Provider前你應瞭解Consumer

所以BuildContext並不是我們想象中的,什麼代代相傳的東西。而是每個Widget特有的存在,就像他們的基因一樣,在每個Widget裡都是不同的。所以我們的問題很簡單,Consumer作為一個Widget,它提供的context便是Consumer的context。下面看一下這幾個小天使在介面的Element樹上的位置。
再強調一下,Element是實現BuildContext抽象介面協議的具象類,Widget或State中Build傳入的BuildContext都是各自的元件對應的Element。每個Element都會記錄它們的父親,就像這樣,按照一個BuildContext(即Element),你可以找到它的祖宗18代,應該是祖宗108代。雖然Widget偽樹非常簡短,但Element樹並不想你想象的那樣。

[- Flutter-技能篇 -] 使用Provider前你應瞭解Consumer

也許你會好奇,最頂級的元老是誰?那我們就偷瞄一下,誰是天使之王。
框架在開始是會建立一個 RenderObjectToWidgetElement,她便是一切美麗的根源。
緊接著便是Provider提供的MultiProvider ,我們的MyApp還要後兩輩。

[- Flutter-技能篇 -] 使用Provider前你應瞭解Consumer


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, 

再放行,按鈕更新,一次介面的點選重新整理完成。

複製程式碼

現在看來唯一的關注點是ListenableProvider這個東東,何許人也?
在這幅圖中已經浮現大佬的身姿了,它老爹是MutiProvider。

[- Flutter-技能篇 -] 使用Provider前你應瞭解Consumer

在buildScope中,我們的故事便發生在ListenableProvider的rebuild方法裡

[- Flutter-技能篇 -] 使用Provider前你應瞭解Consumer

進入後我們到達Element#rebuild()=> ComponentElement#performRebuild()
看到InheritedProvider,我也就會心一笑了。就快打完收工了。

[- Flutter-技能篇 -] 使用Provider前你應瞭解Consumer


rebuild一波後,髒表加了5個,每錯,都是Consumer的節點。只要四個塊,為什麼有5個?
百思不得其解,最後一句TMD,按鈕上也加了Consumer,被自己蠢死。
眾所周知,Flutter只會繪製重建髒表裡的元素。所以會直接構建Consumer而非整體。

[- Flutter-技能篇 -] 使用Provider前你應瞭解Consumer


沒有對比就沒有傷害,最後看一下不用Consumer時重構頁面的髒表情況。在rebuild一波後髒表加入的是整個Widget的元素。

[- Flutter-技能篇 -] 使用Provider前你應瞭解Consumer

就這樣,所以層次較深時,推薦使用Consumer來將更新的粒度變小。


結語

本文到此接近尾聲了,如果想快速嚐鮮Flutter,《Flutter七日》會是你的必備佳品;如果想細細探究它,那就跟隨我的腳步,完成一次Flutter之旅。
另外本人有一個Flutter微信交流群,歡迎小夥伴加入,共同探討Flutter的問題,本人微訊號:zdl1994328,期待與你的交流與切磋。另外歡迎關注公眾號程式設計之王

[- Flutter-技能篇 -] 使用Provider前你應瞭解Consumer