flutter中Provider詳解

為什么我們需要狀態管理

如果應用足夠簡單,Flutter 作為一個聲明式框架,你或許只需要將 數據 映射成 視圖 就可以了。你可能并不需要狀態管理,就像下面這樣。

image.png

但是隨著功能的增加,你的應用程序將會有幾十個甚至上百個狀態。這個時候你的應用應該會是這樣。


image.png

這實在是太復雜了!而且還會有多個頁面共享同一個狀態,需要同步狀態。

Flutter 提供了一種狀態管理方式,那就是 StatefulWidget。在 State 屬于某一個特定的 Widget,在多個 Widget 之間進行交流的時候,雖然你可以使用 callback 解決,但是當嵌套足夠深的話,我們增加非常多可怕的垃圾代碼。

狀態管理Provider

Provider 從名字上就很容易理解,它就是用于提供數據,無論是在單個頁面還是在整個 app 都有它自己的解決方案,我們可以很方便的管理狀態。可以說,Provider 的目標就是完全替代 StatefulWidget。

可以看看官方的例子

Consumer

我們先看 floatingActionButton,使用了一個 Consumer 的情況。

Consumer 使用了 Builder模式,收到更新通知就會通過 builder 重新構建。Consumer<T> 代表了它要獲取哪一個祖先中的 Model。

Consumer 的 builder 實際上就是一個 Function,它接收三個參數 (BuildContext context, T model, Widget child)

  • context: context 就是 build 方法傳進來的 BuildContext 。
  • T:T也很簡單,就是獲取到的最近一個祖先節點中的數據模型。
  • child:它用來構建那些與 Model 無關的部分,在多次運行 builder 中,child 不會進行重建。

然后它會返回一個通過這三個參數映射的 Widget 用于構建自身。

在這個浮動按鈕的例子中,我們通過 Consumer 獲取到了頂層的 CounterModel 實例。并在浮動按鈕 onTap 的 callback 中調用其 increment 方法。

而且我們成功抽離出 Consumer 中不變的部分,也就是浮動按鈕中心的 Icon 并將其作為 child 參數傳入 builder 方法中。

還有 Consumer2, Consumer3,Consumer4,……

區別

我們來看 Consumer 的內部實現。

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

可以發現,Consumer 就是通過 Provider.of<T>(context) 來實現的。但是從實現來講 Provider.of<T>(context)Consumer 簡單好用太多,為啥我要搞得那么復雜捏。

實際上 Consumer 非常有用,它的經典之處在于能夠在復雜項目中,極大地縮小你的控件刷新范圍Provider.of<T>(context) 將會把調用了該方法的 context 作為聽眾,并在 notifyListeners 的時候通知其刷新。

Provides 的構造方法

在上面這個例子中??,我們選擇了使用 XProvider<T>.value 的構造方法來創建祖先節點中的 提供者。除了這種方式,我們還可以使用默認構造方法。

Provider({
    Key key,
    @required ValueBuilder<T> builder,
    Disposer<T> dispose,
    Widget child,
  }) : this._(
          key: key,
          delegate: BuilderStateDelegate<T>(builder, dispose: dispose),
          updateShouldNotify: null,
          child: child,
        );

ValueBuilder

相比起 .value 構造方式中直接傳入一個 value 就 ok,這里的 builder 要求我們傳入一個 ValueBuilder。
typedef ValueBuilder<T> = T Function(BuildContext context);
其實很簡單,就是傳入一個 Function 返回一個數據而已。在上面這個例子中,你可以替換成這樣。

Provider(
    builder: (context) => textSize,
    ...
)

由于是 Builder 模式,這里默認需要傳入 context,實際上我們的 Model(textSize)與 context 并沒有關系,所以你完全可以這樣寫。

Provider(
    builder: (_) => textSize,
    ...
)

Disposer

現在我們知道了 builder,那這個 dispose 方法又用來做什么的呢。實際上這才是 Provider 的點睛之筆。

typedef Disposer<T> = void Function(BuildContext context, T value);

dispose 屬性需要一個 Disposer<T>,而這個其實也是一個回調。

當 Provider 所在節點被移除的時候,它就會啟動 Disposer<T>,然后我們便可以在這里釋放資源。

在需要釋放資源,但是又不想使用 StatefulWidget的時候,我們就可以在頁面頂層套上這個 Provider。

Provider(
    builder:(_) => ValidatorBLoC(),
    dispose:(_, ValidatorBLoC bloc) => bloc.dispose(),
    }
)

這樣就完美解決了數據釋放的問題!
我應該選擇哪種構造方法呢。

簡單模型就選擇 Provider<T>.value,好處是可以精確控制刷新時機。
需要對資源進行釋放處理等復雜模型的時候,Provider() 默認構造方式絕對是你的最佳選擇。

Provider分類

你也可以在 main 方法中通過下面這行代碼來禁用此提示。 Provider.debugCheckInvalidValueType = null;

這是由于 Provider 只能提供恒定的數據,不能通知依賴它的子部件刷新。提示也說的很清楚了,假如你想使用一個會發生 change 的 Provider,請使用下面的 Provider。

  • ListenableProvider
  • ChangeNotifierProvider
  • ValueListenableProvider
  • StreamProvider

這幾個 Provider 有什么異同。

先關注 ListenableProvider / ChangeNotifierProvider 這兩個類。

ListenableProvider 提供(provide)的對象是繼承了 Listenable 抽象類的子類。由于無法混入,所以通過繼承來獲得 Listenable 的能力,同時必須實現其 addListener / removeListener 方法,手動管理收聽者。顯然,這樣太過復雜,我們通常都不需要這樣做。

class ChangeNotifier implements Listenable

而混入了 ChangeNotifier 的類自動幫我們實現了聽眾管理,所以 ListenableProvider 同樣也可以接收混入了 ChangeNotifier 的類。

ChangeNotifierProvider 則更為簡單,它能夠對子節點提供一個 繼承 / 混入 / 實現 了 ChangeNotifier 的類。通常我們只需要在 Model 中 with ChangeNotifier ,然后在需要刷新狀態的時候調用 notifyListeners 即可。

那么 ChangeNotifierProviderListenableProvider 究竟區別在哪呢,ChangeNotifierProvider 會在你需要的時候,自動調用其 _disposer 方法。

static void _disposer(BuildContext context, ChangeNotifier notifier) => notifier?.dispose();

我們可以在 Model 中重寫 ChangeNotifier 的 dispose 方法,來釋放其資源。

ValueListenableProvider。

ValueListenableProvider 用于提供實現了 繼承 / 混入 / 實現 了 ValueListenable 的 Model。它實際上是專門用于處理只有一個單一變化數據的 ChangeNotifier。

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T>

通過 ValueListenable 處理的類不再需要數據更新的時候調用 notifyListeners

StreamProvider

StreamProvider 專門用作提供(provide)一條 Single Stream。我在這里僅對其核心屬性進行講解。

  • T initialData:你可以通過這個屬性聲明這條流的初始值。
  • ErrorBuilder<T> catchError:這個屬性用來捕獲流中的 error。在這條流 addError 了之后,你會能夠通過 T Function(BuildContext context, Object error) 回調來處理這個異常數據。實際開發中它非常有用。
  • updateShouldNotify:和之前的回調一樣,這里不再贅述。

除了這三個構造方法都有的屬性以外,StreamProvider 還有三種不同的構造方法。

  • StreamProvider(...):默認構造方法用作創建一個 Stream 并收聽它。
  • StreamProvider.controller(...):通過 builder 方式創建一個 StreamController<T>。并且在 StreamProvider 被移除時,自動釋放 StreamController。
  • StreamProvider.value(...):監聽一個已有的 Stream 并將其 value 提供給子孫節點。

FutureProvider,它提供了一個 Future 給其子孫節點,并在 Future 完成時,通知依賴的子孫節點進行刷新

優雅地處理多個 Provider ---- MultiProvider

void main() {
  final counter = CounterModel();
  final textSize = 48;
  runApp(
    MultiProvider(
      providers: [
        Provider.value(value: textSize),
        ChangeNotifierProvider.value(value: counter)
      ],
      child: MyApp(),
    ),
  );
}

Tips

保證 build 方法無副作用

build 無副作用也通常被人叫做,build 保持 pure,二者是一個意思。

通常為了獲取頂層數據我們會在 build 方法中調用 XXX.of(context) 方法。 build 函數不應該產生任何副作用,包括新的對象(Widget 以外),請求網絡,或作出一個映射視圖以外的操作等。這是因為無法控制什么時候你的 build 函數將會被調用。

舉一個例子。

假如你有一個 ArticleModel 這個 Model 的作用是 通過網絡 獲取一頁 List 數據,并用 ListView 顯示在頁面上。

@override
  Widget build(BuildContext context) {
      final articleModel = Provider.of<ArticleModel>(context);
      articleModel.getPage(); // requesting data 
      return XWidget(...);
  }

我們在 build 函數中獲得了祖先節點中的 articleModel,隨后調用了 getPage 方法。

這時候會發生什么事情呢,當我們請求成功獲得了結果的時候,根據之前我們已經介紹過的,調用了 Provider.of<T>(context); 會重新運行其 build。這樣 getPage 就又被執行了一次。就會不斷的重繪。

由于 didChangeDependence 方法也會隨著依賴改變而被調用,所以也需要保證它沒有副作用。具體解釋參見下面單頁面數據初始化。

不要所有狀態都放在全局

開發者為了圖方便省事,經常把所有東西都放在頂層 MaterialApp 之上。嚴格區分你的全局數據與局部數據,資源不用了就要釋放!否則將會嚴重影響你的應用 performance。

盡量在 Model 中使用私有變量“_”,減少耦合

控制你的刷新范圍

在 Flutter 中,組合大于繼承的特性隨處可見。常見的 Widget 實際上都是由更小的 Widget 組合而成,直到基本組件為止。為了使我們的應用擁有更高的性能,控制 Widget 的刷新范圍便顯得至關重要。盡量使用 Consumer 來獲取祖先 Model,以維持最小刷新范圍。

Provider 是如何做到狀態共享的

這個問題實際上得分兩步。

獲取頂層數據

實際上在祖先節點中共享數據這件事我們已經在之前的文章中接觸過很多次了,都是通過系統的 InheritedWidget 進行實現的。Provider 也不例外,在所有 Provider 的 build 方法中,返回了一個 InheritedProvider。

class InheritedProvider<T> extends InheritedWidget

Flutter 通過在每個 Element 上維護一個 InheritedWidget 哈希表來向下傳遞 Element 樹中的信息。通常情況下,多個 Element 引用相同的哈希表,并且該表僅在 Element 引入新的 InheritedWidget 時改變。時間復雜度為 O(1) 。

通知刷新

通知刷新這一步實際上就是使用了 Listener 模式。Model 中維護了一堆聽眾,然后 notifiedListener 通知刷新。

全局狀態需要放在頂層 MaterialApp 之上,優先初始化,以便在 Navigator 以及 BuildContex控制全局狀態

數據初始化

全局數據

當需要獲取全局頂層數據,并需要做一些會產生額外結果的時候,main 函數是一個很好的選擇。在 main 方法中創建 Model 并進行初始化的工作,這樣就只會執行一次。

單頁面

如果我們的數據只是在這個頁面中需要使用,那么你有這兩種方式可以選擇。

StatefulWidget

在InitState()中使用 Provider.of<T>(context)是錯誤的。

  /// If [listen] is `true` (default), later value changes will trigger a new
  /// [State.build] to widgets, and [State.didChangeDependencies] for
  /// [StatefulWidget].

源碼中的注釋解釋了,如果這個 Provider.of<T>(context) listen 了的話,那么當 notifyListeners 的時候,就會觸發 context 所對應的 State 的 [State.build] 和 [State.didChangeDependencies] 方法。也就是說,如果你使用了非 Provider 提供的數據,例如 ChangeNotifierProvider 這樣會改變依賴的類,并且獲取數據時 Provider.of<T>(context, listen: true) 選擇 listen (默認就為 listen)的話,數據刷新時會重新運行 didChangeDependencies 和 build 兩個方法。這樣一來對 didChangeDependencies 也會產生副作用。假如在這里請求了數據,當數據到來的時候,又回觸發下一次請求,最終無限請求下去。

這里除了副作用以外還有一點,假如數據改變是一個同步行為,例如這里的 counter.increment 這樣的方法,在 didChangeDependencies 中調用的話,就會造成下面這個錯誤。

The following assertion was thrown while dispatching notifications for CounterModel:
flutter: setState() or markNeedsBuild() called during build.
flutter: This ChangeNotifierProvider<CounterModel> widget cannot be marked as needing to build because the
flutter: framework is already in the process of building widgets. A widget can be marked as needing to be
flutter: built during the build phase only if one of its ancestors is currently building. This exception is
flutter: allowed because the framework builds parent widgets before children, which means a dirty descendant
flutter: will always be built. Otherwise, the framework might not visit this widget during this build phase.

這里和 Flutter 的構建算法有關。簡單來說,就是不能夠在 State 的 build 期間調用 setState() 或者 markNeedsBuild(),在我們這里 didChangeDependence 的時候調用了此方法,導致出現這個錯誤。異步數據則會由于 event loop 的緣故不會立即執行。

首先 要保證初始化數據不能夠產生副作用,我們需要找一個在 State 聲明周期內一定只會運行一次的方法。initState 就是為此而生的。但是 initState 不是無法獲取到 Inherit 嗎。但是我們現在本身就在頁面頂層啊,頁面級別的 Model 就在頂層被創建,現在根本就不需要 Inherit。

class _HomeState extends State<Home> {
    final _myModel = MyModel();
      @override
  void initState() {
    super.initState();
    _myModel.init(); 
  }
}

頁面級別的 Model 數據都在頁面頂層 Widget 創建并初始化即可。

 void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((callback){
      Provider.of<CounterModel>(context).increment();
    });
  }

我們通過 addPostFrameCallback 回調中在第一幀 build 結束時調用 increment 方法,這樣就不會出現構建錯誤了。

provider 作者 Remi 給出了另外一種方式

This code is relatively unsafe. There's more than one reason for didChangeDependencies to be called.
You probably want something similar to:

MyCounter counter;

@override
void didChangeDependencies() {
  final counter = Provider.of<MyCounter>(context);
  if (conter != this.counter) {
    this.counter = counter;
    counter.increment();
  }
}

This should trigger increment only once.

也就是說初始化數據之前判斷一下這個數據是否已經存在。

cascade

你也可以在使用 dart 的級連語法 ..do() 直接在頁面的 StatelessWidget 成員變量聲明時進行初始化。

class FirstScreen extends StatelessWidget {
    CounterModel _counter = CounterModel()..increment();
    double _textSize = 48;
    ...
}

使用這種方式需要注意,當這個 StatelessWidget 重新運行 build 的時候,狀態會丟失。這種情況在 TabBarView 中的子頁面切換過程中就可能會出現。

性能問題

遵守其規范,做任何事情都考慮對性能的影響,要知道 Flutter 把更新算法可是優化到了 O(N)。
Provider 僅僅是對 InheritedWidget 的一個升級,不必擔心引入 Provider 會對應用造成性能問題。

為什么選擇 Provider

Provider 不僅做到了提供數據,而且它擁有著一套完整的解決方案,覆蓋了你會遇到的絕大多數情況。
但是僅僅使用 Provider,Model 和 View 之間還是容易產生依賴。

只有通過手動將 Model 轉化為 ViewModel 這樣才能消除掉依賴關系,所以假如各位有組件化的需求,還需要另外處理。

不過對于大多數情況來說,Provider 足以優秀,它能夠讓你開發出簡單高性能層次清晰 的應用。

源碼淺析

這里在分享一點源碼淺析(真的很淺??)

Flutter 中的 Builder 模式

在 Provider 中,各種 Provider 的原始構造方法都有一個 builder 參數,這里一般就用 (_) => XXXModel() 就行了。感覺有點多次一舉,為什么不能像 .value() 構造方法那樣簡潔呢。

實際上,Provider 為了幫我們管理 Model,使用到了 delegation pattern。

builder 聲明的 ValueBuilder 最終被傳入代理類 BuilderStateDelegate / SingleValueDelegate。 然后通過代理類才實現的 Model 生命周期管理。

class BuilderStateDelegate<T> extends ValueStateDelegate<T> {
  BuilderStateDelegate(this._builder, {Disposer<T> dispose})
      : assert(_builder != null),
        _dispose = dispose;

  final ValueBuilder<T> _builder;
  final Disposer<T> _dispose;

  T _value;
  @override
  T get value => _value;

  @override
  void initDelegate() {
    super.initDelegate();
    _value = _builder(context);
  }

  @override
  void didUpdateDelegate(BuilderStateDelegate<T> old) {
    super.didUpdateDelegate(old);
    _value = old.value;
  }

  @override
  void dispose() {
    _dispose?.call(context, value);
    super.dispose();
  }
}

附:
flutter 狀態管理指南之 Provider
圖解新的狀態管理 Provider
全面理解State與Provider

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容