Flutter完整開發實戰詳解(十五、全面理解State與Provider)

戀貓de小郭發表於2019-06-16

本篇將帶你深入理解 Flutter 中 State 的工作機制,並通過對狀態管理框架 Provider 解析加深理解,看完這一篇你將更輕鬆的理解你的 “State 大後宮” 。

前文:

⚠️第十二篇中更多講解狀態的是管理框架,本篇更多講解 Flutter 本身的狀態設計。

一、State

1、State 是什麼?

我們知道 Flutter 宇宙中萬物皆 Widget ,而 Widget@immutable 即不可變的,所以每個 Widget 狀態都代表了一幀。

在這個基礎上, StatefulWidgetState 幫我們實現了在 Widget 的跨幀繪製 ,也就是在每次 Widget 重繪的時候,通過 State 重新賦予 Widget 需要的繪製資訊。

2、State 怎麼實現跨幀共享?

這就涉及 Flutter 中 Widget 的實現原理,在之前的篇章我們介紹過,這裡我們說兩個涉及的概念:

  • Flutter 中的 Widget 在一般情況下,是需要通過 Element 轉化為 RenderObject 去實現繪製的。

  • ElementBuildContext 的實現類,同時 Element 持有 RenderObjectWidget我們程式碼中的 Widget build(BuildContext context) {} 方法,就是被 Element 呼叫的。

瞭解這個兩個概念後,我們先看下圖,在 Flutter 中構建一個 Widget ,首先會建立出這個 WidgetElement而事實上 State 實現跨幀共享,就是將 State 儲存在Element 中,這樣 Element 每次呼叫 Widget build() 時,是通過 state.build(this); 得到的新 Widget ,所以寫在 State 的資料就得以複用了。

Flutter完整開發實戰詳解(十五、全面理解State與Provider)

State 是在哪裡被建立的?

如下圖所示,StatefulWidgetcreateState 是在 StatefulElement 的構建方法裡建立的, 這就保證了只要 Element 不被重新建立,State 就一直被複用。

同時我們看 update 方法,當新的 StatefulWidget 被建立用於更新 UI 時,新的 widget 就會被重新賦予到 _state 中,而這的設定也導致一個常被新人忽略的問題。

Flutter完整開發實戰詳解(十五、全面理解State與Provider)

我們先看問題程式碼,如下圖所示:

  • 1、在 _DemoAppState 中,我們建立了 DemoPage , 並且把 data 變數賦給了它。
  • 2、DemoPage 在建立 createState 時,又將 data 通過直接傳入 _DemoPageState
  • 3、在 _DemoPageState 中直接將傳入的 data 通過 Text 顯示出來。

執行後我們一看也沒什麼問題吧? 但是當我們點選 4 中的 setState 時,卻發現 3 中 Text 沒有發現改變, 這是為什麼呢?

Flutter完整開發實戰詳解(十五、全面理解State與Provider)

問題就在於前面 StatefulElement 的構建方法和 update 方法:

State 只在 StatefulElement 的構建方法中建立,當我們呼叫 setState 觸發 update 時,只是執行了 _state.widget = newWidget,而我們通過 _DemoPageState(this.data) 傳入的 data ,在傳入後執行setState 時並沒有改變。

如果我們採用上圖程式碼中 3 註釋的 widget.data 方法,因為 _state.widget = newWidget 時,State 中的 Widget 已經被更新了,Text 自然就被更新了。

3、setState 幹了什麼?

我們常說的 setState ,其實是呼叫了 markNeedsBuildmarkNeedsBuild 內部會標記 elementdiry,然後在下一幀 WidgetsBinding.drawFrame 才會被繪製,這可以也看出 setState 並不是立即生效的。

Flutter完整開發實戰詳解(十五、全面理解State與Provider)

4、狀態共享

前面我們聊了 Flutter 中 State 的作用和工作原理,接下來我們看一個老生常談的物件: InheritedWidget

狀態共享是常見的需求,比如使用者資訊和登陸狀態等等,而 Flutter 中 InheritedWidget 就是為此而設計的,在第十二篇我們大致講過它:

Element 的內部有一個 Map<Type, InheritedElement> _inheritedWidgets; 引數,_inheritedWidgets 一般情況下是空的,只有當父控制元件是 InheritedWidget 或者本身是 InheritedWidgets 時,它才會有被初始化,而當父控制元件是 InheritedWidget 時,這個 Map 會被一級一級往下傳遞與合併。

所以當我們通過 context 呼叫 inheritFromWidgetOfExactType 時,就可以通過這個 Map 往上查詢,從而找到這個上級的 InheritedWidget

噢,是的,InheritedWidget 共享的是 Widget ,只是這個 Widget 是一個 ProxyWidget ,它自己本身並不繪製什麼,但共享這個 Widget 內儲存有的值,卻達到了共享狀態的目的。

如下程式碼所示,Flutter 內 Theme 的共享,共享的其實是 _InheritedTheme 這個 Widget ,而我們通過 Theme.of(context) 拿到的,其實就是儲存在這個 Widget 內的 ThemeData

  static ThemeData of(BuildContext context, { bool shadowThemeOnly = false }) {
    final _InheritedTheme inheritedTheme = context.inheritFromWidgetOfExactType(_InheritedTheme);
    if (shadowThemeOnly) {
      /// inheritedTheme 這個 Widget 內的 theme
      /// theme 內有我們需要的 ThemeData
      return inheritedTheme.theme.data;
    }
    ···
  }
複製程式碼

這裡有個需要注意的點,就是 inheritFromWidgetOfExactType 方法剛了什麼?

我們直接找到 Element 中的 inheritFromWidgetOfExactType 方法實現,如下關鍵程式碼所示:

  • 首先從 _inheritedWidgets 中查詢是否有該型別的 InheritedElement
  • 查詢到後新增到 _dependencies 中,並且通過 updateDependencies 將當前 Element 新增到 InheritedElement_dependents 這個Map 裡。
  • 返回 InheritedElement 中的 Widget
  @override
  InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
    /// 在共享 map _inheritedWidgets 中查詢
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
    if (ancestor != null) {
      /// 返回找到的 InheritedWidget ,同時新增當前 element 處理
      return inheritFromElement(ancestor, aspect: aspect);
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }

  @override
  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
   /// 就是將當前 element(this) 新增到  _dependents 裡
   /// 也就是 InheritedElement 的 _dependents
   /// _dependents[dependent] = value;
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }

  @override
  void notifyClients(InheritedWidget oldWidget) {
    for (Element dependent in _dependents.keys) {
      notifyDependent(oldWidget, dependent);
    }
  }
複製程式碼

這裡面的關鍵就是 ancestor.updateDependencies(this, aspect); 這個方法:

我們都知道,獲取 InheritedWidget 一般需要 BuildContext ,如Theme.of(context) ,而 BuildContext 的實現就是 Element所以當我們呼叫 context.inheritFromWidgetOfExactType 時,就會將這個 context 所代表的 Element 新增到 InheritedElement_dependents 中。

這代表著什麼?

比如當我們在 StatefulWidget 中呼叫 Theme.of(context).primaryColor 時,傳入的 context 就代表著這個 WidgetElement, 在 InheritedElement 裡被“登記”到 _dependents 了。

而當 InheritedWidget 被更新時,如下程式碼所示,_dependents 中的 Element 會被逐個執行 notifyDependent ,最後觸發 markNeedsBuild ,這也是為什麼當 InheritedWidget 被更新時,通過如 Theme.of(context).primaryColor 引用的地方,也會觸發更新的原因。

Flutter完整開發實戰詳解(十五、全面理解State與Provider)

下面開始實際分析 Provider

二、Provider

為什麼會有 Provider

因為 Flutter 與 React 技術棧的相似性,所以在 Flutter 中湧現了諸如flutter_reduxflutter_dvaflutter_mobxfish_flutter 等前端式的狀態管理,它們大多比較複雜,而且需要對框架概念有一定理解。

而作為 Flutter 官方推薦的狀態管理 scoped_model ,又因為其設計較為簡單,有些時候不適用於複雜的場景。

所以在經歷了一端坎坷之後,今年 Google I/O 大會之後, Provider 成了 Flutter 官方新推薦的狀態管理方式之一。

它的特點就是: 不復雜,好理解,程式碼量不大的情況下,可以方便組合和控制重新整理顆粒度 , 而原 Google 官方倉庫的狀態管理 flutter-provide 已宣告GG , provider 成了它的替代品。

⚠️注意,`provider` 比 `flutter-provide` 多了個 `r`。

題外話:以前面試時,偶爾會被面試官問到“你的開源專案程式碼量也不多啊”這樣的問題,每次我都會笑而不語,雖然程式碼量能代表一些成果,但是我是十分反對用程式碼量來衡量貢獻價值,這和你用加班時長來衡量員工價值有什麼區別?

0、演示程式碼

如下程式碼所示, 實現的是一個點選計數器,其中:

  • _ProviderPageState 中使用MultiProvider 提供了多個 providers 的支援。
  • CountWidget 中通過 Consumer 獲取的 counter ,同時更新 _ProviderPageState 中的 AppBarCountWidget 中的 Text 顯示。
class _ProviderPageState extends State<ProviderPage> {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(builder: (_) => ProviderModel()),
      ],
      child: Scaffold(
        appBar: AppBar(
          title: LayoutBuilder(
            builder: (BuildContext context, BoxConstraints constraints) {
              var counter =  Provider.of<ProviderModel>(context);
              return new Text("Provider ${counter.count.toString()}");
            },
          )
        ),
        body: CountWidget(),
      ),
    );
  }
}

class CountWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<ProviderModel>(builder: (context, counter, _) {
      return new Column(
        children: <Widget>[
          new Expanded(child: new Center(child: new Text(counter.count.toString()))),
          new Center(
            child: new FlatButton(
                onPressed: () {
                  counter.add();
                },
                color: Colors.blue,
                child: new Text("+")),
          )
        ],
      );
    });
  }
}

class ProviderModel extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void add() {
    _count++;
    notifyListeners();
  }
}
複製程式碼

所以上述程式碼中,我們通過 ChangeNotifierProvider 組合了 ChangeNotifier (ProviderModel) 實現共享;利用了 Provider.ofConsumer 獲取共享的 counter 狀態;通過呼叫 ChangeNotifiernotifyListeners(); 觸發更新。

這裡幾個知識點是:

  • 1、 Provider 的內部 DelegateWidget 是一個 StatefulWidget ,所以可以更新且具有生命週期。

  • 2、狀態共享是使用了 InheritedProvider 這個 InheritedWidget 實現的。

  • 3、巧妙利用 MultiProviderConsumer 封裝,實現了組合與重新整理顆粒度控制。

接著我們逐個分析

1、Delegate

既然是狀態管理,那麼肯定有 StatefulWidgetsetState 呼叫。

Provider 中,一系列關於 StatefulWidget 的生命週期管理和更新,都是通過各種代理完成的,如下圖所示,上面程式碼中我們用到的 ChangeNotifierProvider 大致經歷了這樣的流程:

  • 設定到 ChangeNotifierProviderChangeNotifer 會被執行 addListener 新增監聽 listener
  • listener 內會呼叫 StateDelegateStateSetter 方法,從而呼叫到 StatefulWidgetsetState
  • 當我們執行 ChangeNotifernotifyListeners 時,就會最終觸發 setState 更新。

Flutter完整開發實戰詳解(十五、全面理解State與Provider)

而我們使用過的 MultiProvider 則是允許我們組合多種 Provider ,如下程式碼所示,傳入的 providers 會倒序排列,最後組合成一個巢狀的 Widget tree ,方便我們新增多種 Provider

  @override
  Widget build(BuildContext context) {
    var tree = child;
    for (final provider in providers.reversed) {
      tree = provider.cloneWithChild(tree);
    }
    return tree;
  }

  /// Clones the current provider with a new [child].
  /// Note for implementers: all other values, including [Key] must be
  /// preserved.
  @override
  MultiProvider cloneWithChild(Widget child) {
    return MultiProvider(
      key: key,
      providers: providers,
      child: child,
    );
  }
複製程式碼

通過 Delegate 中回撥出來的各種生命週期,如 Disposer,也有利於我們外部二次處理,減少外部 StatefulWidget 的巢狀使用。

2、InheritedProvider

狀態共享肯定需要 InheritedWidgetInheritedProvider 就是InheritedWidget 的子類,所有的 Provider 實現都在 build 方法中使用 InheritedProvider 進行巢狀,實現 value 的共享。

3、Consumer

ConsumerProvider 中比較有意思的東西,它本身是一個 StatelessWidget , 只是在 build 中通過 Provider.of<T>(context) 幫你獲取到 InheritedWidget 共享的 value

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

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

那我們直接使用 Provider.of<T>(context) ,不使用 Consumer 可以嗎?

當然可以,但是你還記得前面,我們在介紹 InheritedWidget 時所說的:

傳入的 context 代表著這個 WidgetElementInheritedElement 裡被“登記”到 _dependents 了。

Consumer 做為一個單獨 StatelessWidget它的好處就是 Provider.of<T>(context) 傳入的 context 就是 Consumer 它自己。 這樣的話,我們在需要使用 Provider.value 的地方用 Consumer 做巢狀, InheritedWidget 更新的時候,就不會更新到整個頁面 , 而是僅更新到 Consumer 這個 StatelessWidget

所以 Consumer 貼心的封裝了 contextInheritedWidget 中的“登記邏輯”,從而控制了狀態改變時,需要更新的精細度。

同時庫內還提供了 Consumer2Consumer6 的組合,感受下 :


  @override
  Widget build(BuildContext context) {
    return builder(
      context,
      Provider.of<A>(context),
      Provider.of<B>(context),
      Provider.of<C>(context),
      Provider.of<D>(context),
      Provider.of<E>(context),
      Provider.of<F>(context),
      child,
    );
複製程式碼

這樣的設定,相信用過 BLoC 模式的同學會感覺很貼心,以前正常用做 BLoC 時,每個 StreamBuildersnapShot 只支援一種型別,多個時要不就是多個狀態合併到一個實體,要不就需要多個StreamBuilder巢狀。

當然,如果你想直接利用 LayoutBuilder 搭配 Provider.of<T>(context) 也是可以的:

LayoutBuilder(
            builder: (BuildContext context, BoxConstraints constraints) {
              var counter =  Provider.of<ProviderModel>(context);
              return new Text("Provider ${counter.count.toString()}");
            }
複製程式碼

其他的還有 ValueListenableProviderFutureProviderStreamProvider 等多種 Provider ,可見整個 Provider 的設計上更貼近 Flutter 的原生特性,同時設計也更好理解,並且兼顧了效能等問題。

Provider 的使用指南上,更詳細的 Vadaski《Flutter | 狀態管理指南篇——Provider》 已經寫過,我就不重複寫輪子了,感興趣的可以過去看看。

自此,第十五篇終於結束了!(///▽///)

資源推薦

完整開源專案推薦:

相關文章