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

本篇將帶你深刻理解 Flutter 中 State 的工做機制,並經過對狀態管理框架 Provider 解析加深理解,看完這一篇你將更輕鬆的理解你的 「State 大後宮」 。前端

前文:git

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

1、State

一、State 是什麼?

咱們知道 Flutter 宇宙中萬物皆 Widget ,而 Widget@immutable 即不可變的,因此每一個 Widget 狀態都表明了一幀。面試

在這個基礎上, StatefulWidgetState 幫咱們實現了在 Widget 的跨幀繪製 ,也就是在每次 Widget 重繪的時候,經過 State 從新賦予 Widget 須要的繪製信息。redux

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

這就涉及 Flutter 中 Widget 的實現原理,在以前的篇章咱們介紹過,這裏咱們說兩個涉及的概念:bash

  • Flutter 中的 Widget 在通常狀況下,是須要經過 Element 轉化爲 RenderObject 去實現繪製的。app

  • ElementBuildContext 的實現類,同時 Element 持有 RenderObjectWidget咱們代碼中的 Widget build(BuildContext context) {} 方法,就是被 Element 調用的。框架

瞭解這個兩個概念後,咱們先看下圖,在 Flutter 中構建一個 Widget ,首先會建立出這個 WidgetElement而事實上 State 實現跨幀共享,就是將 State 保存在Element 中,這樣 Element 每次調用 Widget build() 時,是經過 state.build(this); 獲得的新 Widget ,因此寫在 State 的數據就得以複用了。less

State 是在哪裏被建立的?ide

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

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

咱們先看問題代碼,以下圖所示:

  • 一、在 _DemoAppState 中,咱們建立了 DemoPage , 而且把 data 變量賦給了它。
  • 二、DemoPage 在建立 createState 時,又將 data 經過直接傳入 _DemoPageState
  • 三、在 _DemoPageState 中直接將傳入的 data 經過 Text 顯示出來。

運行後咱們一看也沒什麼問題吧? 可是當咱們點擊 4 中的 setState 時,卻發現 3 中 Text 沒有發現改變, 這是爲何呢?

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

State 只在 StatefulElement 的構建方法中建立,當咱們調用 setState 觸發 update 時,只是執行了 _state.widget = newWidget,而咱們經過 _DemoPageState(this.data) 傳入的 data ,在傳入後執行setState 時並無改變。

若是咱們採用上圖代碼中 3 註釋的 widget.data 方法,由於 _state.widget = newWidget 時,State 中的 Widget 已經被更新了,Text 天然就被更新了。

三、setState 幹了什麼?

咱們常說的 setState ,實際上是調用了 markNeedsBuildmarkNeedsBuild 內部會標記 elementdiry,而後在下一幀 WidgetsBinding.drawFrame 纔會被繪製,這能夠也看出 setState 並非當即生效的。

四、狀態共享

前面咱們聊了 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 引用的地方,也會觸發更新的緣由。

下面開始實際分析 Provider

2、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(); 觸發更新。

這裏幾個知識點是:

  • 一、 Provider 的內部 DelegateWidget 是一個 StatefulWidget ,因此能夠更新且具備生命週期。

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

  • 三、巧妙利用 MultiProviderConsumer 封裝,實現了組合與刷新顆粒度控制。

接着咱們逐個分析

一、Delegate

既然是狀態管理,那麼確定有 StatefulWidgetsetState 調用。

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

  • 設置到 ChangeNotifierProviderChangeNotifer 會被執行 addListener 添加監聽 listener
  • listener 內會調用 StateDelegateStateSetter 方法,從而調用到 StatefulWidgetsetState
  • 當咱們執行 ChangeNotifernotifyListeners 時,就會最終觸發 setState 更新。

而咱們使用過的 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 的嵌套使用。

二、InheritedProvider

狀態共享確定須要 InheritedWidgetInheritedProvider 就是InheritedWidget 的子類,全部的 Provider 實現都在 build 方法中使用 InheritedProvider 進行嵌套,實現 value 的共享。

三、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》 已經寫過,我就不重複寫輪子了,感興趣的能夠過去看看。

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

資源推薦

完整開源項目推薦:

相關文章
相關標籤/搜索