Flutter | 狀態管理指南篇——Provider

本文於 2019.7.8 日更新,修正了關於數據初始化以及 保證 build 函數無反作用這兩部分的錯誤,若文章還存在任何問題,請聯繫我修復它。git

前言

2019 Google I/O 大會,官方在 Pragmatic State Management in Flutter (Google I/O'19) 主題演講上正式介紹了 由社區做者 Remi Rousselet 與 Flutter Team 共同編寫的 Provider 代替 Provide 成爲官方推薦的狀態管理方式之一。github

讀者老朋友應該都知道,在以前的文章中我介紹了 Google 官方倉庫下的一個狀態管理 Provide。乍一看這倆玩意可能很容易就被認爲是同一個東西,仔細一看,這不就差了一個字嗎,有什麼區別呢。🧐算法

首先,你要知道的最大的一個區別就是,Provide 被 Provider 幹掉了...假如你就是用了 Provide 的幸運鵝,你的心裏應該已經開始 甘霖* 這不是坑爹嗎 🤦‍♀️。我也在這先給這部分朋友說聲抱歉嗷,畢竟不少人是看了我以前那篇文章才入坑的。不過幸運的是,你要從 Provide 遷移到 Provider 並非太難。api

本文將基於最新 Provider v-3.0 進行介紹,除了講解其使用方式以外,我認爲更重要的是 Provider 不一樣「提供」方式的適用場景及使用原則。以及在使用狀態管理時候須要遵照的原則,在編寫 Flutter App 的過程當中減輕你的思考負擔。但願本文能給你帶來一些有價值的參考。(提早打個預防針,本文篇幅較長,建議馬住在看。)bash

推薦閱讀時間:1小時服務器

What's the problem

在正式介紹 Provider 以前容許我再囉嗦兩句,爲何咱們須要狀態管理。若是你已經對此十分清楚,那麼建議直接跳過這一節。markdown

若是咱們的應用足夠簡單,Flutter 做爲一個聲明式框架,你或許只須要將 數據 映射成 視圖 就能夠了。你可能並不須要狀態管理,就像下面這樣。網絡

可是隨着功能的增長,你的應用程序將會有幾十個甚至上百個狀態。這個時候你的應用應該會是這樣。架構

WTF,這是什麼鬼。咱們很難再清楚的測試維護咱們的狀態,由於它看上去實在是太複雜了!並且還會有多個頁面共享同一個狀態,例如當你進入一個文章點贊,退出到外部縮略展現的時候,外部也須要顯示點贊數,這時候就須要同步這兩個狀態。app

Flutter 實際上在一開始就爲咱們提供了一種狀態管理方式,那就是 StatefulWidget。可是咱們很快發現,它正是形成上述緣由的罪魁禍首

在 State 屬於某一個特定的 Widget,在多個 Widget 之間進行交流的時候,雖然你可使用 callback 解決,可是當嵌套足夠深的話,咱們增長很是多可怕的垃圾代碼。

這時候,咱們便迫切的須要一個架構來幫助咱們理清這些關係,狀態管理框架應運而生。

What is Provider

那麼咱們該如何解決上面這種糟糕的狀況呢。在上手這個庫以後我能夠說 Provider 是一個至關不錯的解決方案。(你上次介紹 Provide 也這麼說😒)咱們先來簡單說一下 Provider 的基本做用。

Provider 從名字上就很容易理解,它就是用於提供數據,不管是在單個頁面仍是在整個 app 都有它本身的解決方案,咱們能夠很方便的管理狀態。能夠說,Provider 的目標就是徹底替代 StatefulWidget。

說了不少仍是很抽象,咱們先一塊兒作一個最簡單的例子。

How to do

這裏咱們仍是用這個 Counter App 爲例,給你們介紹如何在兩個獨立的頁面中共享計數器(counter)的狀態應該怎麼作,具體長這樣。

兩個頁面中心字體共用了同一個字體大小。第二個頁面的按鈕將會讓數字增長,第一個頁面的數字將會同步增長。

第一步:添加依賴

在pubspec.yaml中添加Provider的依賴。

第二步:建立數據 Model

這裏的 Model 實際上就是咱們的狀態,它不只儲存了咱們的數據模型,並且還包含了更改數據的方法,並暴露出它想要暴露出的數據。

import 'package:flutter/material.dart';

class CounterModel with ChangeNotifier {
  int _count = 0;
  int get value => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}
複製代碼

這個類意圖很是清晰,咱們的數據就是一個 int 類型的 _count,下劃線表明私有。經過 get value_count 值暴露出來。並提供 increment 方法用於更改數據。

這裏使用了 mixin 混入了 ChangeNotifier,這個類可以幫駐咱們自動管理全部聽衆。當調用 notifyListeners() 時,它會通知全部聽衆進行刷新。

若是你對 mixin 這個概念還不是很清楚的話,能夠看我以前翻譯的這篇 【譯】Dart | 什麼是Mixin

第三步:建立頂層共享數據

咱們在 main 方法中初始化全局數據。

void main() {
  final counter = CounterModel();
  final textSize = 48;

  runApp(
    Provider<int>.value(
      value: textSize,
      child: ChangeNotifierProvider.value(
        value: counter,
        child: MyApp(),
      ),
    ),
  );
}
複製代碼

經過 Provider<T>.value 可以管理一個恆定的數據,並提供給子孫節點使用。咱們只須要將數據在其 value 屬性中聲明便可。在這裏咱們將 textSize 傳入。

ChangeNotifierProvider<T>.value 不只可以提供數據供子孫節點使用,還能夠在數據改變的時候通知全部聽衆刷新。(經過以前咱們說過的 notifyListeners)

此處的 <T> 範型可省略。可是我建議你們仍是進行聲明,這會使你的應用更加健壯。

除了上述幾個屬性以外 Provider<T>.value 還提供了 UpdateShouldNotify Function,用於控制刷新時機。

typedef UpdateShouldNotify<T> = bool Function(T previous, T current);

咱們能夠在這裏傳入一個方法 (T previous, T current){...} ,並得到先後兩個 Model 的實例,而後經過比較兩個 Model 以自定義刷新規則,返回 bool 表示是否須要刷新。默認爲 previous != current 則刷新。

固然,key 屬性是確定有的,常規操做。若是你還不太清楚的話,建議閱讀我以前的這篇文章 [Flutter | 深刻淺出Key] (juejin.im/post/5ca215…)

爲了讓各位思惟連貫,我仍是在這裏放上這個平淡無奇的 MyApp Widget 代碼。😑

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark(),
      home: FirstScreen(),
    );
  }
}
複製代碼

第四步:在子頁面中獲取狀態

在這裏咱們有兩個頁面,FirstScreen 和 SecondScreen。咱們先來看 FirstScreen 的代碼。

Provider.of(context)

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _counter = Provider.of<CounterModel>(context);
    final textSize = Provider.of<int>(context).toDouble();

    return Scaffold(
      appBar: AppBar(
        title: Text('FirstPage'),
      ),
      body: Center(
        child: Text(
          'Value: ${_counter.value}',
          style: TextStyle(fontSize: textSize),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.of(context)
            .push(MaterialPageRoute(builder: (context) => SecondPage())),
        child: Icon(Icons.navigate_next),
      ),
    );
  }
}
複製代碼

獲取頂層數據最簡單的方法就是 Provider.of<T>(context); 這裏的範型 <T> 指定了獲取 FirstScreen 向上尋找最近的儲存了 T 的祖先節點的數據。

咱們經過這個方法獲取了頂層的 CounterModel 及 textSize。並在 Text 組件中進行使用。

floatingActionButton 用來點擊跳轉到 SecondScreen 頁面,和咱們的主題無關。

Consumer

看到這裏你可能會想,兩個頁面都是獲取頂層狀態,代碼不都同樣嗎,弄啥捏。🤨 別忙着跳到下一節,咱們來看另一種獲取狀態的方式,這將會影響你的 app performance。

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Page'),
      ),
      body: Consumer2<CounterModel,int>(
        builder: (context, CounterModel counter, int textSize, _) => Center(
              child: Text(
                'Value: ${counter.value}',
                style: TextStyle(
                  fontSize: textSize.toDouble(),
                ),
              ),
            ),
      ),
      floatingActionButton: Consumer<CounterModel>(
        builder: (context, CounterModel counter, child) => FloatingActionButton(
              onPressed: counter.increment,
              child: child,
            ),
        child: Icon(Icons.add),
      ),
    );
  }
}
複製代碼

這裏咱們要介紹的是第二種方式,使用 Consumer 獲取祖先節點中的數據。

在這個頁面中,咱們有兩處使用到了公共 Model。

  • 應用中心的文字:使用 CounterModel 在 Text 中展現文字,以及經過 textSize 定義自身的大小。一共使用到了兩個 Model。
  • 浮動按鈕:使用 CounterModel 的 increment 方法觸發計數器的值增長。使用到了一個 Model。

Single Model Consumer

咱們先看 floatingActionButton,使用了一個 Consumer 的狀況。

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

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

  • context: context 就是 build 方法傳進來的 BuildContext 在這裏就不細說了,若是有興趣能夠看我以前這篇文章 Flutter | 深刻理解BuildContext
  • T:T也很簡單,就是獲取到的最近一個祖先節點中的數據模型。
  • child:它用來構建那些與 Model 無關的部分,在屢次運行 builder 中,child 不會進行重建。

而後它會返回一個經過這三個參數映射的 Widget 用於構建自身。

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

並且咱們成功抽離出 Consumer 中不變的部分,也就是浮動按鈕中心的 Icon 並將其做爲 child 參數傳入 builder 方法中。

Consumer2

如今咱們再來看中心的文字部分。這時候你可能會有疑惑了,剛纔咱們講的 Consumer 獲取的只有一個 Model,而如今 Text 組件不只須要 CounterModel 用以顯示計數器,並且還須要得到 textSize 以調整字體大小,咋整捏。

遇到這種狀況你可使用 Consumer2<A,B>。使用方式基本上和 Consumer<T> 一致,只不過範型改成了兩個,而且 builder 方法也變成了 Function(BuildContext context, A value, B value2, Widget child)

我勒個去...假如我要得到 100 個 Model,那豈不是得搞個 Consumer100 (???黑人問號.jpg)

然而並無 😏。

從源碼裏面能夠看到,做者只爲咱們搞到了 Consumer6。emmmmm.....還要要求更多就只有自力更生嘍。

順手幫做者修復了一個 clerical error。

區別

咱們來看 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 的時候通知其刷新。

舉個例子來講,咱們的 FirstScreen 使用了 Provider.of<T>(context) 來獲取數據,SecondScreen 則沒有。

  • 你在 FirstScreen 中的 build 方法中添加一個 print('first screen rebuild');
  • 而後在 SecondScreen 中的 build 方法中添加一個 print('second screen rebuild');
  • 點擊第二個頁面的浮動按鈕,那麼你會在控制檯看到這句輸出。

first screen rebuild

首先這證實了 Provider.of<T>(context) 會致使調用的 context 頁面範圍的刷新。

那麼第二個頁面刷新沒有呢? 刷新了,可是隻刷新了 Consumer 的部分,甚至連浮動按鈕中的 Icon 的不刷新咱們都給控制了。你能夠在 Consumer 的 builder 方法中驗證,這裏再也不囉嗦

假如你在你的應用的 頁面級別 的 Widget 中,使用了 Provider.of<T>(context)。會致使什麼後果已經顯而易見了,每當其狀態改變的時候,你都會從新刷新整個頁面。雖然你有 Flutter 的自動優化算法給你撐腰,但你確定沒法得到最好的性能

因此在這裏我建議各位儘可能使用 Consumer 而不是 Provider.of<T>(context) 獲取頂層數據。

以上即是一個最簡單的使用 Provider 的例子。

You also need to know

合理選擇使用 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,
        );
複製代碼

常規的 key/child 屬性咱們不在這裏囉嗦。咱們先來看這個看上去相對教複雜一點的 builder。

ValueBuilder

相比起 .value 構造方式中直接傳入一個 value 就 ok,這裏的 builder 要求咱們傳入一個 ValueBuilder。WTF?

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>,而這個其實也是一個回調。

若是你以前使用過 BLoC 的話,相信你確定遇到過一個頭疼的問題。我應該在何時釋放資源呢? BloC 使用了觀察者模式,它旨在替代 StatefulWidget。然而大量的流使用完畢以後必須 close 掉,以釋放資源。

然而 Stateless Widget 並無給咱們相似於 dispose 之類的方法,這即是 BLoC 的硬傷。你不得不爲了釋放資源而使用 StatefulWidget,這與咱們的本意相違。而 Provider 則爲咱們解決了這一點。

當 Provider 所在節點被移除的時候,它就會啓動 Disposer<T>,而後咱們即可以在這裏釋放資源。

舉個例子,假如咱們有這樣一個 BLoC。

class ValidatorBLoC {
  StreamController<String> _validator = StreamController<String>.broadcast();

  get validator => _validator.stream;

  validateAccount(String text) {
    //Processing verification text ...
  }

  dispose() {
    _validator.close();
  }
}
複製代碼

這時候咱們想要在某個頁面提供這個 BLoC 可是又不想使用 StatefulWidget。這時候咱們能夠在頁面頂層套上這個 Provider。

Provider(
    builder:(_) => ValidatorBLoC(),
    dispose:(_, ValidatorBLoC bloc) => bloc.dispose(),
    }
)
複製代碼

這樣就完美解決了數據釋放的問題!🤩

如今咱們能夠放心的結合 BLoC 一塊兒使用了,很贊有沒有。可是如今你可能又有疑問了,在使用 Provider 的時候,我應該選擇哪一種構造方法呢。

個人推薦是,簡單模型就選擇 Provider<T>.value,好處是能夠精確控制刷新時機。而須要對資源進行釋放處理等複雜模型的時候,Provider() 默認構造方式絕對是你的最佳選擇。

其餘幾種 Provider 也遵循該模式,須要的時候能夠自行查看源碼。

我該使用哪一種 Provider

若是你在 Provider 中提供了可監聽對象(Listenable 或者 Stream)及其子類的話,那麼你會獲得下面這個異常警告。

你能夠將本文中所使用到的 CounterModel 放入 Provider 進行提供(記得 hot restart 而不是 hot reload),那麼你就能看到上面這個 FlutterError 了。

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

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

  • ListenableProvider
  • ChangeNotifierProvider
  • ValueListenableProvider
  • StreamProvider

你可能會在這裏產生一個疑問,不是說(Listenable 或者 Stream)纔不行嗎,爲何咱們的 CounterModel 混入的是 ChangeNotifier 可是仍是出現了這個 FlutterError 呢。

class ChangeNotifier implements Listenable

咱們再來看上面的這幾個 Provider 有什麼異同。先關注 ListenableProvider / ChangeNotifierProvider 這兩個類。

ListenableProvider 提供(provide)的對象是繼承了 Listenable 抽象類的子類。因爲沒法混入,因此經過繼承來得到 Listenable 的能力,同時必須實現其 addListener / removeListener 方法,手動管理收聽者。顯然,這樣太過複雜,咱們一般都不須要這樣作。

而混入了 ChangeNotifier 的類自動幫咱們實現了聽衆管理,因此 ListenableProvider 一樣也能夠接收混入了 ChangeNotifier 的類。

ChangeNotifierProvider 則更爲簡單,它可以對子節點提供一個 繼承 / 混入 / 實現 了 ChangeNotifier 的類。一般咱們只須要在 Model 中 with ChangeNotifier ,而後在須要刷新狀態的時候調用 notifyListeners 便可。

那麼 ChangeNotifierProviderListenableProvider 究竟區別在哪呢,ListenableProvider 不是也能夠提供(provide)混入了 ChangeNotifier 的 Model 嗎。

仍是那個你須要思考的問題。你在這裏的 Model 到底是一個簡單模型仍是複雜模型。這是由於 ChangeNotifierProvider 會在你須要的時候,自動調用其 _disposer 方法。

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

咱們能夠在 Model 中重寫 ChangeNotifier 的 dispose 方法,來釋放其資源。這對於複雜 Model 的狀況下十分有用。

如今你應該已經十分清楚 ListenableProvider / ChangeNotifierProvider 的區別了。下面咱們來看 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 提供給子孫節點。

除了上面這五種已經提到過的 Provider,還有一種 FutureProvider,它提供了一個 Future 給其子孫節點,並在 Future 完成時,通知依賴的子孫節點進行刷新,這裏再也不詳細介紹,須要的話自行查看 api 文檔。

優雅地處理多個 Provider

在咱們以前的例子中,咱們使用了嵌套的方式來組合多個 Provider。這樣看上去有些傻瓜(我就是有一百個 Model 🙃)。

這時候咱們就可使用一個很是 sweet 的組件 —— 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 函數將會被調用。我能夠說隨時。每當你的 build 函數被調用,那麼都會產生一個反作用。這將會發生很是恐怖的事情。🤯

我這樣說你確定會感到比較抽象,咱們來舉一個例子。

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

這時候,咱們假設你在 build 函數中作了下面這些事情。

@override
  Widget build(BuildContext context) {
      final articleModel = Provider.of<ArticleModel>(context);
      mainCategoryModel.getPage(); // By requesting data from the server
      return XWidget(...);
  }
複製代碼

咱們在 build 函數中得到了祖先節點中的 articleModel,隨後調用了 getPage 方法。

這時候會發生什麼事情呢,當咱們請求成功得到告終果的時候,根據以前咱們已經介紹過的,調用了 Provider.of<T>(context); 會從新運行其 build。這樣 getPage 就又被執行了一次。

而你的 Model 中每次請求 getPage 都會致使 Model 中保存的當前請求頁自增(第一次請求第一頁的數據,第二次請求第二頁的數據以此類推),那麼每次 build 都會致使新的一次數據請求,並在新的數據 get 的時候請求下一頁的數據。你的服務器掛掉那是早晚的事情。(come on baby!

因爲 didChangeDependence 方法也會隨着依賴改變而被調用,因此也須要保證它沒有反作用。具體解釋參見下面單頁面數據初始化。

因此你應該嚴格遵照這項原則,不然會致使一系列糟糕的後果。

那麼怎麼解決數據初始化這個問題呢,請看 Q&A 部分。

不要全部狀態都放在全局

第二個小貼士是不要把你的全部狀態都放在頂層。開發者爲了圖方便省事,再接觸了狀態管理以後常常喜歡把全部東西都放在頂層 MaterialApp 之上。這樣看上去就很方便共享數據了,我要數據就直接去獲取。

不要這麼作。嚴格區分你的全局數據與局部數據,資源不用了就要釋放!不然將會嚴重影響你的應用 performance。

儘可能在 Model 中使用私有變量「_」

這多是咱們每一個人在新手階段都會出現的疑問。爲何要用私有變量呢,我在任何地方都可以操做成員不是很方便嗎。

一個應用須要大量開發人員參與,你寫的代碼也許在幾個月以後被另一個開發看到了,這時候假如你的變量沒有被保護的話,也許一樣是讓 count++,他會用 countController.sink.add(++_count) 這種原始方法,而不是調用你已經封裝好了的 increment 方法。

雖然兩種方式的效果徹底同樣,可是第二種方式將會讓咱們的business logic零散的混入其餘代碼中。長此以往項目中就會大量充斥着這些垃圾代碼增長項目代碼耦合程度,很是不利於代碼的維護以及閱讀。

因此,請務必使用私有變量保護你的 Model。

控制你的刷新範圍

在 Flutter 中,組合大於繼承的特性隨處可見。常見的 Widget 實際上都是由更小的 Widget 組合而成,直到基本組件爲止。爲了使咱們的應用擁有更高的性能,控制 Widget 的刷新範圍便顯得相當重要。

咱們已經經過前面的介紹瞭解到了,在 Provider 中獲取 Model 的方式會影響刷新範圍。全部,請儘可能使用 Consumer 來獲取祖先 Model,以維持最小刷新範圍。

Q&A

在這裏對一些你們可能會有疑問的常見問題作一個回答,若是你還有這以外的疑問的話,歡迎在下方評論區一塊兒討論。

Provider 是如何作到狀態共享的

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

獲取頂層數據

實際上在祖先節點中共享數據這件事咱們已經在以前的文章中接觸過不少次了,都是經過系統的 InheritedWidget 進行實現的。

Provider 也不例外,在全部 Provider 的 build 方法中,返回了一個 InheritedProvider。

class InheritedProvider<T> extends InheritedWidget

Flutter 經過在每一個 Element 上維護一個 InheritedWidget 哈希表來向下傳遞 Element 樹中的信息。一般狀況下,多個 Element 引用相同的哈希表,而且該表僅在 Element 引入新的 InheritedWidget 時改變。

因此尋找祖先節點的時間複雜度爲 O(1) 😎

通知刷新

通知刷新這一步實際上在講各類 Provider 的時候已經講過了,其實就是使用了 Listener 模式。Model 中維護了一堆聽衆,而後 notifiedListener 通知刷新。(空間換時間🤣

爲何全局狀態須要放在頂層 MaterialApp 之上

這個問題須要結合 Navigator 以及 BuildContext 來回答,在以前的文章中 Flutter | 深刻理解BuildContext 已經解釋過了,這裏再也不贅述。

我應該在哪裏進行數據初始化

對於數據初始化這個問題,咱們必需要分類討論。

全局數據

當咱們須要獲取全局頂層數據(就像以前 CounterApp 例子同樣)並須要作一些會產生額外結果的時候,main 函數是一個很好的選擇。

咱們能夠在 main 方法中建立 Model 並進行初始化的工做,這樣就只會執行一次。

單頁面

若是咱們的數據只是在這個頁面中須要使用,那麼你有這兩種方式能夠選擇。

StatefulWidget

這裏訂正一個錯誤,感謝 @曉傑的V笑 以及 @fantasy525 在討論中幫我指出。

在以前文章的版本中我推薦你們在 State 的 didChangeDependence 中進行數據初始化。這裏實際上是使用 BLoC 延續下來的習慣。由於使用了 InheritWidget 以後,只有在 State 的 didChangeDependence 階段進行 Inherit 初始化,initState 階段是拿不到數據的。而因爲 BLoC 是使用的 Stream,數據直接走 Stream 進來,由 StreamBuilder 去 listen,這樣 State 的依賴一直都只是這個 Stream 對象而已,不會再次觸發 didChangeDependence 方法。那 Provider 有何不一樣呢。

/// 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 的緣故不會當即執行。想要深刻了解的同窗能夠看閒魚技術的這篇文章:Flutter快速上車之Widget

感受到處都是坑啊,那該怎麼初始化呢。目前我找到的辦法是這樣,首先 要保證初始化數據不可以產生反作用,咱們須要找一個在 State 聲明週期內必定只會運行一次的方法。initState 就是爲此而生的。可是 initState 不是沒法獲取到 Inherit 嗎。可是咱們如今自己就在頁面頂層啊,頁面級別的 Model 就在頂層被建立,如今根本就不須要 Inherit。

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

頁面級別的 Model 數據都在頁面頂層 Widget 建立並初始化便可。

咱們還須要考慮一種狀況,假如這個操做是一個同步操做應該如何處理,就如咱們以前舉的 CounterModel.increment 這個操做同樣。

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 中的子頁面切換過程當中就可能會出現。

因此建議仍是使用第一種,在 State 中初始化數據。

我須要擔憂性能問題嗎

是的,不管 Flutter 再怎麼努力優化,Provider 考慮的狀況再多,咱們老是有辦法讓應用卡爆 😂(開個玩笑)

僅當咱們不遵照其行爲規範的時候,會出現這樣的狀況。性能會由於你的各類不當操做而變得很糟糕。個人建議是:遵照其規範,作任何事情都考慮對性能的影響,要知道 Flutter 把更新算法但是優化到了 O(N)。

Provider 僅僅是對 InheritedWidget 的一個升級,你沒必要擔憂引入 Provider 會對應用形成性能問題。

爲何選擇 Provider

Provider 不只作到了提供數據,並且它擁有着一套完整的解決方案,覆蓋了你會遇到的絕大多數狀況。就連 BLoC 未解決的那個棘手的 dispose 問題,和 ScopedModel 的侵入性問題,它也都解決了。

然而它就是完美的嗎,並非,至少如今來講。Flutter Widget 構建模式很容易在 UI 層面上組件化,可是僅僅使用 Provider,Model 和 View 之間仍是容易產生依賴。

咱們只有經過手動將 Model 轉化爲 ViewModel 這樣才能消除掉依賴關係,因此假如各位有組件化的需求,還須要另外處理。

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

我應該如何選擇狀態管理

介紹了這麼多狀態管理,你可能會發現,一些狀態管理之間職責並不衝突。例如 BLoC 能夠結合 RxDart 庫變得很強大,很好用。而 BLoC 也能夠結合 Provider / ScopedModel 一塊兒使用。那我應該選擇哪一種狀態管理方式呢。

個人建議是遵照如下幾點:

  1. 使用狀態管理的目的是爲了讓編寫代碼變得更簡單,任何會增長你的應用複雜度的狀態管理,通通都不要用。
  2. 選擇本身可以 hold 住的,BLoC / Rxdart / Redux / Fish-Redux 這些狀態管理方式都有必定上手難度,不要選本身沒法理解的狀態管理方式。
  3. 在作最終決定以前,敲一敲 demo,真正感覺各個狀態管理方式給你帶來的 好處/壞處 而後再作你的決定。

但願可以幫助到你。

源碼淺析

這裏在分享一點源碼淺析(真的很淺😅)

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();
  }
}
複製代碼

這裏就僅放 BuilderStateDelegate,其他的請自行查看源碼。

如何實現 MultiProvider

Widget build(BuildContext context) {
    var tree = child;
    for (final provider in providers.reversed) {
      tree = provider.cloneWithChild(tree);
    }
    return tree;
  }
複製代碼

MultiProvider 實際上就是經過每個 provider 都實現了的 cloneWithChild 方法把本身一層一層包裹起來。

MultiProvider(
    providers:[
        AProvider,
        BProvider,
        CProvider,
    ],
    child: child,
)
複製代碼

等價於

AProvider(
    child: BProvider(
        child: CProvider(
            child: child,
        ),
    ),
)
複製代碼

寫在最後

此次寫的太順暢,不當心就寫得過多了。能看到這裏的朋友,都很強 🤣。

與其說此次是 Provider 專場,更像是把狀態管理本身所遇到的心得都總結在這裏了。但願可以給各位有參考價值。

後期的 Tips 和 Q&A 有一部分實際上對大多數狀態管理都適用,我後面會考慮把這些專門拉出來說一篇。不過下篇文章主題已經決定了,在 Flutter 中實現無 context 導航 的。若是你感興趣的話必定不要錯過。

若是您對Provider還有任何疑問或者文章的建議,歡迎在下方評論區以及個人郵箱1652219550a@gmail.com與我聯繫,我會及時回覆!

相關文章
相關標籤/搜索