Flutter狀態管理:Provider4 入門教程(二)

JarvanMo發表於2020-06-07

書接上文

Flutter狀態管理:Provider4 入門教程(一)中,我們對狀態管理以及Provider有了初步的瞭解,也學習了ChangeNotifierProvider以及Consumer的使用,但由於時間有限,講到Consumer就關賣了個關子,連廣告都忘打了,現在我們正式書接上文,從聊聊Consumer開始。

Flutter狀態管理:Provider4 入門教程(二)

怎麼還是Consumer

Consumer2-6

如果有細心的朋友在使用Consumer時可能會發現,有個幾個神奇的類:

  • Consumer2
  • Consumer3
  • Consumer4
  • Consumer5
  • Consumer6

Flutter狀態管理:Provider4 入門教程(二)
這是啥?難不成Consumer成精了?

其實並不是哦。這其實是Consumer大家族,一共六位成員,Consumer後面的數字代表了Consumer可接收的資料類數量。

天吶,原來是這個意思,那為什麼沒有Consumer1000?親,這邊建議您去問作者吧,我怕被打死呢。所以說,如果當真有這麼個需求,還是自力更生吧。

說到這裡,我們再簡單地聊一下Consumer

整個Consumer家族使用了Builder模式,當Consumer收到了更新就會通知builder更新。而builder實際上就是一個Function,也可以說是一個lamda了。以Conumer為例,它的builder被定義為Widget (BuildContext context, T model, Widget child),一共接收三個引數,其中的T就是可以接收的資料型別,T其實就是Consumer<T>裡的泛型T,所以說使用Consumer時一定要提供泛型。以前文的程式碼為例,Consumer<MyChangeNotifier>就是說這個Consumer接收MyChangeNotifier例項。以此類推,Consumer2<A,B>可以接收AB兩種型別的資料,從它的builder的定義中就可以看出來:Widget Function(BuildContext context, A value, B value2, Widget child)

builder中的child是用來構建那些與資料模型無關的部分,在多次執行builder中,child也不會進行重繪。

按道理來說,現在應該來說說Consumer3了,但是考慮到他們其實都大同小異了,就不湊字數了,具體大家可以參看原始碼。

為什麼推薦使用Consumer

前文我說過:

Consumer本身沒有魔法,也沒有什麼花裡胡哨的實現。只不過是在一個新的控制元件中使用Provider.of,然後將這個控制元件的build方法委託給引數裡的builder。這個builder會被呼叫多次。就是這麼簡單。

但是我也說過Consumer可以為我們提供效能上的優化,因為Consumer可以為我們提供更細小的顆粒化重繪。

我們要知道當使用Provider.of時,除非我們設定了listen:false,否則只要Provider.of中接收的資料發生了變化,與Provider.of中的BuildeContext相關的控制元件都會進行重新構建。這當然是我們期望的行為,但有時候這可能會引起不必要的過多的重繪。

我們看一個例子:

 @override
 Widget build(BuildContext context) {
   return FooWidget(
     child: BarWidget(
       bar: Provider.of<Bar>(context),
     ),
   );
 }
複製程式碼

上面的程式碼中,只有BarWidget依賴Provider.of中返回的資料。但是當Bar發生變化時,BarWidgetFooWidget都會進行重繪。

但理想情況中,應該只有BarWidget進行重繪。一種解決方案就是使用Consumer

為了實現我們剛說的方法,我們將對用Consumer對依賴Provider的控制元件進行包裹。

 @override
 Widget build(BuildContext context) {
   return FooWidget(
     child: Consumer<Bar>(
       builder: (_, bar, __) => BarWidget(bar: bar),
     ),
   );
 }
複製程式碼

現在,如果Bar要進行更新,只有BarWidget才會進行重繪。 但是如果FooWidget依賴了一個provider了怎麼辦? 比如說:

 @override
 Widget build(BuildContext context) {
   return FooWidget(
     foo: Provider.of<Foo>(context),
     child: BarWidget(),
   );
 }
複製程式碼

還記得我上面說的child嗎?對,就是它,上程式碼:

 @override
 Widget build(BuildContext context) {
   return Consumer<Foo>(
     builder: (_, foo, child) => FooWidget(foo: foo, child: child),
     child: BarWidget(),
   );
 }
複製程式碼

這個例子中,BarWidget是在builder之外進行重繪的。然後BarWidget例項作為最一個引數傳遞給給builder

這意味著builder會被反覆呼叫時,Consumer並不會建立BarWidget新例項。這會讓Flutter知道不必重新繪製BarWidget。所以通過這麼樣的一個寫法,當Foo有更新時,只有FooWidget才會進行重繪。

有興趣的朋友,可以實現上述示例程式碼,然後在build()中加幾個print,然後驗證一下說的對不對。

Consumer小結

總得來說,Consumer有兩個主要用處:

  • 當我們的BuildContext中不存在指定的Provider時,Consumer允許我們從Provider中的獲取資料。
  • 提升效能

所以我還是很建議使用Consumer的,畢竟可以帶來效能提升。Flutter誠然會我們做很多優化,但很多並不代表全部,效能的提升更多的是依靠我們自己的實現方式。

MultiProvider

我們之前一直在講一個頁面一個Provider的情型,但實際上很多上了規模的應用,它可能不止一個Provider,搞個幾十個provider也是可能的,這時我們會想巢狀大法:

Provider<Something>(
  create: (_) => Something(),
  child: Provider<SomethingElse>(
    create: (_) => SomethingElse(),
    child: Provider<AnotherThing>(
      create: (_) => AnotherThing(),
      child: someWidget,
    ),
  ),
),
複製程式碼

一句以F開頭以U結尾的話是不是又要蹦出來?別急,作者很體貼地設計了MultiProvider

MultiProvider(
  providers: [
    Provider<Something>(create: (_) => Something()),
    Provider<SomethingElse>(create: (_) => SomethingElse()),
    Provider<AnotherThing>(create: (_) => AnotherThing()),
  ],
  child: someWidget,
)
複製程式碼

當然了,這兩段程式碼是完全等價的,所以MultiProvider也沒有什麼黑魔法,只是不讓程式碼看起不那麼ugly

筆記:Consumer也可以在MultiProvider使用。但Consumer必須返回它在控制元件樹中建立的child,也就是builder中的builder

MultiProvider(
  providers: [
    Provider(create: (_) => Foo()),
    Consumer<Foo>(
      builder: (context, foo, child) =>
        Provider.value(value: foo.bar, child: child),
    )
  ],
);
複製程式碼

我有個問題

上面我們說到了MultiProvider,忽然間腦袋靈光一現,有個問題想問:

  • 可以在一個BuildContext中獲取具有相同資料型別的多個provider嗎?

用程式碼說就是這樣式的:

Provider<String>(
  create: (_) => '中國',
  child: Provider<String>(
    create: (_) => '大連',
    child: ...,
  ),
),
複製程式碼

答案是否定的。當有多個具體相同資料型別的provider時,一個控制元件只能獲取到一個:離他最近的。

所以嘞,我們必須明確地指出他們的型別:

Provider<Country>(
  create: (_) => Country('中國'),
  child: Provider<City>(
    create: (_) => City('大連'),
    child: ...,
  ),
),
複製程式碼

emmmm,看起來還不錯。

欲知後事請聽下回分解

作為Provider系列的第二篇,內容依然很簡單,而我又要說時間有限了。

未完待續。。。 期待不期待你說了算。

相關文章