Flutter | 狀態(tài)管理指南篇——Provider

本文于 2019.7.8 日更新赎瑰,修正了關(guān)于數(shù)據(jù)初始化以及 保證 build 函數(shù)無(wú)副作用這兩部分的錯(cuò)誤羊瘩,若文章還存在任何問(wèn)題俩檬,請(qǐng)聯(lián)系我修復(fù)它。

前言

2019 Google I/O 大會(huì)纲熏,官方在 Pragmatic State Management in Flutter (Google I/O'19) 主題演講上正式介紹了 由社區(qū)作者 Remi Rousselet 與 Flutter Team 共同編寫(xiě)的 Provider 代替 Provide 成為官方推薦的狀態(tài)管理方式之一妆丘。

讀者老朋友應(yīng)該都知道,在之前的文章中我介紹了 Google 官方倉(cāng)庫(kù)下的一個(gè)狀態(tài)管理 Provide局劲。乍一看這倆玩意可能很容易就被認(rèn)為是同一個(gè)東西勺拣,仔細(xì)一看,這不就差了一個(gè)字嗎鱼填,有什么區(qū)別呢药有。??

首先,你要知道的最大的一個(gè)區(qū)別就是苹丸,Provide 被 Provider 干掉了...假如你就是用了 Provide 的幸運(yùn)鵝愤惰,你的內(nèi)心應(yīng)該已經(jīng)開(kāi)始 甘霖* 這不是坑爹嗎 ???♀?。我也在這先給這部分朋友說(shuō)聲抱歉嗷赘理,畢竟很多人是看了我之前那篇文章才入坑的宦言。不過(guò)幸運(yùn)的是,你要從 Provide 遷移到 Provider 并不是太難商模。

本文將基于最新 Provider v-3.0 進(jìn)行介紹奠旺,除了講解其使用方式之外蜘澜,我認(rèn)為更重要的是 Provider 不同“提供”方式的適用場(chǎng)景及使用原則。以及在使用狀態(tài)管理時(shí)候需要遵守的原則响疚,在編寫(xiě) Flutter App 的過(guò)程中減輕你的思考負(fù)擔(dān)鄙信。希望本文能給你帶來(lái)一些有價(jià)值的參考。(提前打個(gè)預(yù)防針忿晕,本文篇幅較長(zhǎng)装诡,建議馬住在看。)

推薦閱讀時(shí)間:1小時(shí)

What's the problem

在正式介紹 Provider 之前允許我再啰嗦兩句杏糙,為什么我們需要狀態(tài)管理慎王。如果你已經(jīng)對(duì)此十分清楚,那么建議直接跳過(guò)這一節(jié)宏侍。

如果我們的應(yīng)用足夠簡(jiǎn)單,F(xiàn)lutter 作為一個(gè)聲明式框架蜀漆,你或許只需要將 數(shù)據(jù) 映射成 視圖 就可以了谅河。你可能并不需要狀態(tài)管理,就像下面這樣确丢。

image

但是隨著功能的增加绷耍,你的應(yīng)用程序?qū)?huì)有幾十個(gè)甚至上百個(gè)狀態(tài)。這個(gè)時(shí)候你的應(yīng)用應(yīng)該會(huì)是這樣鲜侥。

image

WTF褂始,這是什么鬼。我們很難再清楚的測(cè)試維護(hù)我們的狀態(tài)描函,因?yàn)樗瓷先?shí)在是太復(fù)雜了崎苗!而且還會(huì)有多個(gè)頁(yè)面共享同一個(gè)狀態(tài),例如當(dāng)你進(jìn)入一個(gè)文章點(diǎn)贊舀寓,退出到外部縮略展示的時(shí)候胆数,外部也需要顯示點(diǎn)贊數(shù),這時(shí)候就需要同步這兩個(gè)狀態(tài)互墓。

Flutter 實(shí)際上在一開(kāi)始就為我們提供了一種狀態(tài)管理方式必尼,那就是 StatefulWidget。但是我們很快發(fā)現(xiàn)篡撵,它正是造成上述原因的罪魁禍?zhǔn)?/strong>判莉。

在 State 屬于某一個(gè)特定的 Widget,在多個(gè) Widget 之間進(jìn)行交流的時(shí)候育谬,雖然你可以使用 callback 解決券盅,但是當(dāng)嵌套足夠深的話,我們?cè)黾臃浅6嗫膳碌睦a斑司。

這時(shí)候渗饮,我們便迫切的需要一個(gè)架構(gòu)來(lái)幫助我們理清這些關(guān)系但汞,狀態(tài)管理框架應(yīng)運(yùn)而生。

What is Provider

那么我們?cè)撊绾谓鉀Q上面這種糟糕的情況呢互站。在上手這個(gè)庫(kù)之后我可以說(shuō) Provider 是一個(gè)相當(dāng)不錯(cuò)的解決方案私蕾。(你上次介紹 Provide 也這么說(shuō)??)我們先來(lái)簡(jiǎn)單說(shuō)一下 Provider 的基本作用。

Provider 從名字上就很容易理解胡桃,它就是用于提供數(shù)據(jù)踩叭,無(wú)論是在單個(gè)頁(yè)面還是在整個(gè) app 都有它自己的解決方案,我們可以很方便的管理狀態(tài)翠胰∪荼矗可以說(shuō),Provider 的目標(biāo)就是完全替代 StatefulWidget之景。

說(shuō)了很多還是很抽象斤富,我們先一起做一個(gè)最簡(jiǎn)單的例子。

How to do

這里我們還是用這個(gè) Counter App 為例锻狗,給大家介紹如何在兩個(gè)獨(dú)立的頁(yè)面中共享計(jì)數(shù)器(counter)的狀態(tài)應(yīng)該怎么做,具體長(zhǎng)這樣满力。

image

兩個(gè)頁(yè)面中心字體共用了同一個(gè)字體大小。第二個(gè)頁(yè)面的按鈕將會(huì)讓數(shù)字增加轻纪,第一個(gè)頁(yè)面的數(shù)字將會(huì)同步增加油额。

第一步:添加依賴

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

image

第二步:創(chuàng)建數(shù)據(jù) Model

這里的 Model 實(shí)際上就是我們的狀態(tài)刻帚,它不僅儲(chǔ)存了我們的數(shù)據(jù)模型潦嘶,而且還包含了更改數(shù)據(jù)的方法,并暴露出它想要暴露出的數(shù)據(jù)崇众。

import 'package:flutter/material.dart';

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

  void increment() {
    _count++;
    notifyListeners();
  }
}

這個(gè)類意圖非常清晰掂僵,我們的數(shù)據(jù)就是一個(gè) int 類型的 _count,下劃線代表私有校摩。通過(guò) get value_count 值暴露出來(lái)看峻。并提供 increment 方法用于更改數(shù)據(jù)。

這里使用了 mixin 混入了 ChangeNotifier衙吩,這個(gè)類能夠幫駐我們自動(dòng)管理所有聽(tīng)眾互妓。當(dāng)調(diào)用 notifyListeners() 時(shí),它會(huì)通知所有聽(tīng)眾進(jìn)行刷新坤塞。

如果你對(duì) mixin 這個(gè)概念還不是很清楚的話冯勉,可以看我之前翻譯的這篇 【譯】Dart | 什么是Mixin

第三步:創(chuàng)建頂層共享數(shù)據(jù)

我們?cè)?main 方法中初始化全局?jǐn)?shù)據(jù)摹芙。

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

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

通過(guò) Provider<T>.value 能夠管理一個(gè)恒定的數(shù)據(jù)灼狰,并提供給子孫節(jié)點(diǎn)使用。我們只需要將數(shù)據(jù)在其 value 屬性中聲明即可浮禾。在這里我們將 textSize 傳入交胚。

ChangeNotifierProvider<T>.value 不僅能夠提供數(shù)據(jù)供子孫節(jié)點(diǎn)使用份汗,還可以在數(shù)據(jù)改變的時(shí)候通知所有聽(tīng)眾刷新。(通過(guò)之前我們說(shuō)過(guò)的 notifyListeners)

此處的 <T> 范型可省略蝴簇。但是我建議大家還是進(jìn)行聲明杯活,這會(huì)使你的應(yīng)用更加健壯。

除了上述幾個(gè)屬性之外 Provider<T>.value 還提供了 UpdateShouldNotify Function熬词,用于控制刷新時(shí)機(jī)旁钧。

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

我們可以在這里傳入一個(gè)方法 (T previous, T current){...} ,并獲得前后兩個(gè) Model 的實(shí)例互拾,然后通過(guò)比較兩個(gè) Model 以自定義刷新規(guī)則歪今,返回 bool 表示是否需要刷新。默認(rèn)為 previous != current 則刷新颜矿。

當(dāng)然寄猩,key 屬性是肯定有的,常規(guī)操作或衡。如果你還不太清楚的話焦影,建議閱讀我之前的這篇文章 [Flutter | 深入淺出Key] (https://juejin.im/post/5ca2152f6fb9a05e1a7a9a26。)

為了讓各位思維連貫封断,我還是在這里放上這個(gè)平淡無(wú)奇的 MyApp Widget 代碼。??

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

第四步:在子頁(yè)面中獲取狀態(tài)

在這里我們有兩個(gè)頁(yè)面舶担,F(xiàn)irstScreen 和 SecondScreen坡疼。我們先來(lái)看 FirstScreen 的代碼。

Provider.of<T>(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),
      ),
    );
  }
}

獲取頂層數(shù)據(jù)最簡(jiǎn)單的方法就是 Provider.of<T>(context); 這里的范型 <T> 指定了獲取 FirstScreen 向上尋找最近的儲(chǔ)存了 T 的祖先節(jié)點(diǎn)的數(shù)據(jù)衣陶。

我們通過(guò)這個(gè)方法獲取了頂層的 CounterModel 及 textSize柄瑰。并在 Text 組件中進(jìn)行使用。

floatingActionButton 用來(lái)點(diǎn)擊跳轉(zhuǎn)到 SecondScreen 頁(yè)面剪况,和我們的主題無(wú)關(guān)教沾。

Consumer

看到這里你可能會(huì)想,兩個(gè)頁(yè)面都是獲取頂層狀態(tài)译断,代碼不都一樣嗎授翻,弄啥捏。?? 別忙著跳到下一節(jié)孙咪,我們來(lái)看另外一種獲取狀態(tài)的方式堪唐,這將會(huì)影響你的 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 獲取祖先節(jié)點(diǎn)中的數(shù)據(jù)淮菠。

在這個(gè)頁(yè)面中,我們有兩處使用到了公共 Model荤堪。

  • 應(yīng)用中心的文字:使用 CounterModel 在 Text 中展示文字合陵,以及通過(guò) textSize 定義自身的大小枢赔。一共使用到了兩個(gè) Model。
  • 浮動(dòng)按鈕:使用 CounterModel 的 increment 方法觸發(fā)計(jì)數(shù)器的值增加拥知。使用到了一個(gè) Model踏拜。

Single Model Consumer

我們先看 floatingActionButton,使用了一個(gè) Consumer 的情況举庶。

Consumer 使用了 Builder 模式执隧,收到更新通知就會(huì)通過(guò) builder 重新構(gòu)建。Consumer<T> 代表了它要獲取哪一個(gè)祖先中的 Model户侥。

Consumer 的 builder 實(shí)際上就是一個(gè) Function镀琉,它接收三個(gè)參數(shù) (BuildContext context, T model, Widget child)

  • context: context 就是 build 方法傳進(jìn)來(lái)的 BuildContext 在這里就不細(xì)說(shuō)了蕊唐,如果有興趣可以看我之前這篇文章 Flutter | 深入理解BuildContext屋摔。
  • T:T也很簡(jiǎn)單,就是獲取到的最近一個(gè)祖先節(jié)點(diǎn)中的數(shù)據(jù)模型替梨。
  • child:它用來(lái)構(gòu)建那些與 Model 無(wú)關(guān)的部分钓试,在多次運(yùn)行 builder 中,child 不會(huì)進(jìn)行重建副瀑。

然后它會(huì)返回一個(gè)通過(guò)這三個(gè)參數(shù)映射的 Widget 用于構(gòu)建自身弓熏。

在這個(gè)浮動(dòng)按鈕的例子中,我們通過(guò) Consumer 獲取到了頂層的 CounterModel 實(shí)例糠睡。并在浮動(dòng)按鈕 onTap 的 callback 中調(diào)用其 increment 方法挽鞠。

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

Consumer2

現(xiàn)在我們?cè)賮?lái)看中心的文字部分信认。這時(shí)候你可能會(huì)有疑惑了,剛才我們講的 Consumer 獲取的只有一個(gè) Model均抽,而現(xiàn)在 Text 組件不僅需要 CounterModel 用以顯示計(jì)數(shù)器嫁赏,而且還需要獲得 textSize 以調(diào)整字體大小,咋整捏油挥。

遇到這種情況你可以使用 Consumer2<A,B>潦蝇。使用方式基本上和 Consumer<T> 一致,只不過(guò)范型改為了兩個(gè)喘漏,并且 builder 方法也變成了 Function(BuildContext context, A value, B value2, Widget child)护蝶。

我勒個(gè)去...假如我要獲得 100 個(gè) Model,那豈不是得搞個(gè) Consumer100 (???黑人問(wèn)號(hào).jpg)

然而并沒(méi)有 ??翩迈。

從源碼里面可以看到持灰,作者只為我們搞到了 Consumer6。emmmmm.....還要要求更多就只有自力更生嘍负饲。

順手幫作者修復(fù)了一個(gè) clerical error堤魁。

image

區(qū)別

我們來(lái)看 Consumer 的內(nèi)部實(shí)現(xiàn)喂链。

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

可以發(fā)現(xiàn),Consumer 就是通過(guò) Provider.of<T>(context) 來(lái)實(shí)現(xiàn)的妥泉。但是從實(shí)現(xiàn)來(lái)講 Provider.of<T>(context)Consumer 簡(jiǎn)單好用太多椭微,為啥我要搞得那么復(fù)雜捏。

實(shí)際上 Consumer 非常有用盲链,它的經(jīng)典之處在于能夠在復(fù)雜項(xiàng)目中蝇率,極大地縮小你的控件刷新范圍Provider.of<T>(context) 將會(huì)把調(diào)用了該方法的 context 作為聽(tīng)眾刽沾,并在 notifyListeners 的時(shí)候通知其刷新本慕。

舉個(gè)例子來(lái)說(shuō),我們的 FirstScreen 使用了 Provider.of<T>(context) 來(lái)獲取數(shù)據(jù)侧漓,SecondScreen 則沒(méi)有锅尘。

  • 你在 FirstScreen 中的 build 方法中添加一個(gè) print('first screen rebuild');
  • 然后在 SecondScreen 中的 build 方法中添加一個(gè) print('second screen rebuild');
  • 點(diǎn)擊第二個(gè)頁(yè)面的浮動(dòng)按鈕,那么你會(huì)在控制臺(tái)看到這句輸出布蔗。

first screen rebuild

首先這證明了 Provider.of<T>(context) 會(huì)導(dǎo)致調(diào)用的 context 頁(yè)面范圍的刷新藤违。

那么第二個(gè)頁(yè)面刷新沒(méi)有呢? 刷新了纵揍,但是只刷新了 Consumer 的部分顿乒,甚至連浮動(dòng)按鈕中的 Icon 的不刷新我們都給控制了。你可以在 Consumer 的 builder 方法中驗(yàn)證泽谨,這里不再啰嗦

假如你在你的應(yīng)用的 頁(yè)面級(jí)別 的 Widget 中淆游,使用了 Provider.of<T>(context)。會(huì)導(dǎo)致什么后果已經(jīng)顯而易見(jiàn)了隔盛,每當(dāng)其狀態(tài)改變的時(shí)候,你都會(huì)重新刷新整個(gè)頁(yè)面拾稳。雖然你有 Flutter 的自動(dòng)優(yōu)化算法給你撐腰吮炕,但你肯定無(wú)法獲得最好的性能

所以在這里我建議各位盡量使用 Consumer 而不是 Provider.of<T>(context) 獲取頂層數(shù)據(jù)访得。

以上便是一個(gè)最簡(jiǎn)單的使用 Provider 的例子龙亲。

You also need to know

合理選擇使用 Provides 的構(gòu)造方法

在上面這個(gè)例子中??,我們選擇了使用 XProvider<T>.value 的構(gòu)造方法來(lái)創(chuàng)建祖先節(jié)點(diǎn)中的 提供者悍抑。除了這種方式鳄炉,我們還可以使用默認(rèn)構(gòu)造方法。

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,
        );

常規(guī)的 key/child 屬性我們不在這里啰嗦搜骡。我們先來(lái)看這個(gè)看上去相對(duì)教復(fù)雜一點(diǎn)的 builder拂盯。

ValueBuilder

相比起 .value 構(gòu)造方式中直接傳入一個(gè) value 就 ok,這里的 builder 要求我們傳入一個(gè) ValueBuilder记靡。WTF谈竿?

typedef ValueBuilder<T> = T Function(BuildContext context);

其實(shí)很簡(jiǎn)單团驱,就是傳入一個(gè) Function 返回一個(gè)數(shù)據(jù)而已。在上面這個(gè)例子中空凸,你可以替換成這樣嚎花。

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

由于是 Builder 模式,這里默認(rèn)需要傳入 context呀洲,實(shí)際上我們的 Model(textSize)與 context 并沒(méi)有關(guān)系紊选,所以你完全可以這樣寫(xiě)。

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

Disposer

現(xiàn)在我們知道了 builder道逗,那這個(gè) dispose 方法又用來(lái)做什么的呢兵罢。實(shí)際上這才是 Provider 的點(diǎn)睛之筆。

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

dispose 屬性需要一個(gè) Disposer<T>,而這個(gè)其實(shí)也是一個(gè)回調(diào)憔辫。

如果你之前使用過(guò) BLoC 的話趣些,相信你肯定遇到過(guò)一個(gè)頭疼的問(wèn)題。我應(yīng)該在什么時(shí)候釋放資源呢贰您? BloC 使用了觀察者模式坏平,它旨在替代 StatefulWidget。然而大量的流使用完畢之后必須 close 掉锦亦,以釋放資源舶替。

然而 Stateless Widget 并沒(méi)有給我們類似于 dispose 之類的方法,這便是 BLoC 的硬傷杠园。你不得不為了釋放資源而使用 StatefulWidget顾瞪,這與我們的本意相違。而 Provider 則為我們解決了這一點(diǎn)抛蚁。

當(dāng) Provider 所在節(jié)點(diǎn)被移除的時(shí)候陈醒,它就會(huì)啟動(dòng) Disposer<T>,然后我們便可以在這里釋放資源瞧甩。

舉個(gè)例子钉跷,假如我們有這樣一個(gè) BLoC。

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

  get validator => _validator.stream;

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

  dispose() {
    _validator.close();
  }
}

這時(shí)候我們想要在某個(gè)頁(yè)面提供這個(gè) BLoC 但是又不想使用 StatefulWidget肚逸。這時(shí)候我們可以在頁(yè)面頂層套上這個(gè) Provider爷辙。

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

這樣就完美解決了數(shù)據(jù)釋放的問(wèn)題!??

現(xiàn)在我們可以放心的結(jié)合 BLoC 一起使用了朦促,很贊有沒(méi)有膝晾。但是現(xiàn)在你可能又有疑問(wèn)了,在使用 Provider 的時(shí)候务冕,我應(yīng)該選擇哪種構(gòu)造方法呢血当。

我的推薦是,簡(jiǎn)單模型就選擇 Provider<T>.value,好處是可以精確控制刷新時(shí)機(jī)歹颓。而需要對(duì)資源進(jìn)行釋放處理等復(fù)雜模型的時(shí)候坯屿,Provider() 默認(rèn)構(gòu)造方式絕對(duì)是你的最佳選擇。

其他幾種 Provider 也遵循該模式巍扛,需要的時(shí)候可以自行查看源碼领跛。

我該使用哪種 Provider

如果你在 Provider 中提供了可監(jiān)聽(tīng)對(duì)象(Listenable 或者 Stream)及其子類的話,那么你會(huì)得到下面這個(gè)異常警告撤奸。

image

你可以將本文中所使用到的 CounterModel 放入 Provider 進(jìn)行提供(記得 hot restart 而不是 hot reload)吠昭,那么你就能看到上面這個(gè) FlutterError 了。

你也可以在 main 方法中通過(guò)下面這行代碼來(lái)禁用此提示胧瓜。
Provider.debugCheckInvalidValueType = null;

這是由于 Provider 只能提供恒定的數(shù)據(jù)矢棚,不能通知依賴它的子部件刷新。提示也說(shuō)的很清楚了府喳,假如你想使用一個(gè)會(huì)發(fā)生 change 的 Provider蒲肋,請(qǐng)使用下面的 Provider。

  • ListenableProvider
  • ChangeNotifierProvider
  • ValueListenableProvider
  • StreamProvider

你可能會(huì)在這里產(chǎn)生一個(gè)疑問(wèn)钝满,不是說(shuō)(Listenable 或者 Stream)才不行嗎兜粘,為什么我們的 CounterModel 混入的是 ChangeNotifier 但是還是出現(xiàn)了這個(gè) FlutterError 呢。

class ChangeNotifier implements Listenable

我們?cè)賮?lái)看上面的這幾個(gè) Provider 有什么異同弯蚜。先關(guān)注 ListenableProvider / ChangeNotifierProvider 這兩個(gè)類孔轴。

ListenableProvider 提供(provide)的對(duì)象是繼承了 Listenable 抽象類的子類。由于無(wú)法混入碎捺,所以通過(guò)繼承來(lái)獲得 Listenable 的能力路鹰,同時(shí)必須實(shí)現(xiàn)其 addListener / removeListener 方法,手動(dòng)管理收聽(tīng)者收厨。顯然晋柱,這樣太過(guò)復(fù)雜,我們通常都不需要這樣做诵叁。

而混入了 ChangeNotifier 的類自動(dòng)幫我們實(shí)現(xiàn)了聽(tīng)眾管理趣斤,所以 ListenableProvider 同樣也可以接收混入了 ChangeNotifier 的類。

ChangeNotifierProvider 則更為簡(jiǎn)單黎休,它能夠?qū)ψ庸?jié)點(diǎn)提供一個(gè) 繼承 / 混入 / 實(shí)現(xiàn) 了 ChangeNotifier 的類。通常我們只需要在 Model 中 with ChangeNotifier 玉凯,然后在需要刷新?tīng)顟B(tài)的時(shí)候調(diào)用 notifyListeners 即可势腮。

那么 ChangeNotifierProviderListenableProvider 究竟區(qū)別在哪呢,ListenableProvider 不是也可以提供(provide)混入了 ChangeNotifier 的 Model 嗎漫仆。

還是那個(gè)你需要思考的問(wèn)題捎拯。你在這里的 Model 究竟是一個(gè)簡(jiǎn)單模型還是復(fù)雜模型。這是因?yàn)?ChangeNotifierProvider 會(huì)在你需要的時(shí)候盲厌,自動(dòng)調(diào)用其 _disposer 方法署照。

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

我們可以在 Model 中重寫(xiě) ChangeNotifier 的 dispose 方法祸泪,來(lái)釋放其資源。這對(duì)于復(fù)雜 Model 的情況下十分有用建芙。

現(xiàn)在你應(yīng)該已經(jīng)十分清楚 ListenableProvider / ChangeNotifierProvider 的區(qū)別了没隘。下面我們來(lái)看 ValueListenableProvider。

ValueListenableProvider 用于提供實(shí)現(xiàn)了 繼承 / 混入 / 實(shí)現(xiàn) 了 ValueListenable 的 Model禁荸。它實(shí)際上是專門(mén)用于處理只有一個(gè)單一變化數(shù)據(jù)的 ChangeNotifier右蒲。

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

通過(guò) ValueListenable 處理的類不再需要數(shù)據(jù)更新的時(shí)候調(diào)用 notifyListeners

好了赶熟,終于只剩下最后一個(gè) StreamProvider 了瑰妄。

StreamProvider 專門(mén)用作提供(provide)一條 Single Stream。我在這里僅對(duì)其核心屬性進(jìn)行講解映砖。

  • T initialData:你可以通過(guò)這個(gè)屬性聲明這條流的初始值间坐。
  • ErrorBuilder<T> catchError:這個(gè)屬性用來(lái)捕獲流中的 error。在這條流 addError 了之后邑退,你會(huì)能夠通過(guò) T Function(BuildContext context, Object error) 回調(diào)來(lái)處理這個(gè)異常數(shù)據(jù)竹宋。實(shí)際開(kāi)發(fā)中它非常有用。
  • updateShouldNotify:和之前的回調(diào)一樣瓜饥,這里不再贅述逝撬。

除了這三個(gè)構(gòu)造方法都有的屬性以外,StreamProvider 還有三種不同的構(gòu)造方法乓土。

  • StreamProvider(...):默認(rèn)構(gòu)造方法用作創(chuàng)建一個(gè) Stream 并收聽(tīng)它宪潮。
  • StreamProvider.controller(...):通過(guò) builder 方式創(chuàng)建一個(gè) StreamController<T>。并且在 StreamProvider 被移除時(shí)趣苏,自動(dòng)釋放 StreamController狡相。
  • StreamProvider.value(...):監(jiān)聽(tīng)一個(gè)已有的 Stream 并將其 value 提供給子孫節(jié)點(diǎn)。

除了上面這五種已經(jīng)提到過(guò)的 Provider食磕,還有一種 FutureProvider尽棕,它提供了一個(gè) Future 給其子孫節(jié)點(diǎn),并在 Future 完成時(shí)彬伦,通知依賴的子孫節(jié)點(diǎn)進(jìn)行刷新滔悉,這里不再詳細(xì)介紹,需要的話自行查看 api 文檔单绑。

優(yōu)雅地處理多個(gè) Provider

在我們之前的例子中回官,我們使用了嵌套的方式來(lái)組合多個(gè) Provider。這樣看上去有些傻瓜(我就是有一百個(gè) Model ??)搂橙。

這時(shí)候我們就可以使用一個(gè)非常 sweet 的組件 —— MultiProvider歉提。

這時(shí)候我們剛才那個(gè)例子就可以改成這樣。

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

  runApp(
    MultiProvider(
      providers: [
        Provider.value(value: textSize),
        ChangeNotifierProvider.value(value: counter)
      ],
      child: MyApp(),
    ),
  );
}

我們的代碼瞬間清晰很多,而且與剛才的嵌套做法完全等價(jià)苔巨。

Tips

保證 build 方法無(wú)副作用

build 無(wú)副作用也通常被人叫做版扩,build 保持 pure,二者是一個(gè)意思侄泽。

通常我們經(jīng)常會(huì)看到礁芦,為了獲取頂層數(shù)據(jù)我們會(huì)在 build 方法中調(diào)用 XXX.of(context) 方法。你必須非常小心蔬顾,你的 build 函數(shù)不應(yīng)該產(chǎn)生任何副作用宴偿,包括新的對(duì)象(Widget 以外),請(qǐng)求網(wǎng)絡(luò)诀豁,或作出一個(gè)映射視圖以外的操作等窄刘。

這是因?yàn)椋愕母緹o(wú)法控制什么時(shí)候你的 build 函數(shù)將會(huì)被調(diào)用舷胜。我可以說(shuō)隨時(shí)娩践。每當(dāng)你的 build 函數(shù)被調(diào)用,那么都會(huì)產(chǎn)生一個(gè)副作用烹骨。這將會(huì)發(fā)生非撤牛恐怖的事情。??

我這樣說(shuō)你肯定會(huì)感到比較抽象沮焕,我們來(lái)舉一個(gè)例子吨岭。

假如你有一個(gè) ArticleModel 這個(gè) Model 的作用是 通過(guò)網(wǎng)絡(luò) 獲取一頁(yè) List 數(shù)據(jù),并用 ListView 顯示在頁(yè)面上峦树。

這時(shí)候辣辫,我們假設(shè)你在 build 函數(shù)中做了下面這些事情。

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

我們?cè)?build 函數(shù)中獲得了祖先節(jié)點(diǎn)中的 articleModel魁巩,隨后調(diào)用了 getPage 方法急灭。

這時(shí)候會(huì)發(fā)生什么事情呢,當(dāng)我們請(qǐng)求成功獲得了結(jié)果的時(shí)候谷遂,根據(jù)之前我們已經(jīng)介紹過(guò)的葬馋,調(diào)用了 Provider.of<T>(context); 會(huì)重新運(yùn)行其 build。這樣 getPage 就又被執(zhí)行了一次肾扰。

而你的 Model 中每次請(qǐng)求 getPage 都會(huì)導(dǎo)致 Model 中保存的當(dāng)前請(qǐng)求頁(yè)自增(第一次請(qǐng)求第一頁(yè)的數(shù)據(jù)畴嘶,第二次請(qǐng)求第二頁(yè)的數(shù)據(jù)以此類推),那么每次 build 都會(huì)導(dǎo)致新的一次數(shù)據(jù)請(qǐng)求集晚,并在新的數(shù)據(jù) get 的時(shí)候請(qǐng)求下一頁(yè)的數(shù)據(jù)掠廓。你的服務(wù)器掛掉那是遲早的事情。(come on baby甩恼!

由于 didChangeDependence 方法也會(huì)隨著依賴改變而被調(diào)用,所以也需要保證它沒(méi)有副作用。具體解釋參見(jiàn)下面單頁(yè)面數(shù)據(jù)初始化条摸。

所以你應(yīng)該嚴(yán)格遵守這項(xiàng)原則悦污,否則會(huì)導(dǎo)致一系列糟糕的后果。

那么怎么解決數(shù)據(jù)初始化這個(gè)問(wèn)題呢钉蒲,請(qǐng)看 Q&A 部分切端。

不要所有狀態(tài)都放在全局

第二個(gè)小貼士是不要把你的所有狀態(tài)都放在頂層。開(kāi)發(fā)者為了圖方便省事顷啼,再接觸了狀態(tài)管理之后經(jīng)常喜歡把所有東西都放在頂層 MaterialApp 之上踏枣。這樣看上去就很方便共享數(shù)據(jù)了,我要數(shù)據(jù)就直接去獲取钙蒙。

不要這么做茵瀑。嚴(yán)格區(qū)分你的全局?jǐn)?shù)據(jù)與局部數(shù)據(jù),資源不用了就要釋放躬厌!否則將會(huì)嚴(yán)重影響你的應(yīng)用 performance马昨。

盡量在 Model 中使用私有變量“_”

這可能是我們每個(gè)人在新手階段都會(huì)出現(xiàn)的疑問(wèn)。為什么要用私有變量呢扛施,我在任何地方都能夠操作成員不是很方便嗎鸿捧。

一個(gè)應(yīng)用需要大量開(kāi)發(fā)人員參與,你寫(xiě)的代碼也許在幾個(gè)月之后被另外一個(gè)開(kāi)發(fā)看到了疙渣,這時(shí)候假如你的變量沒(méi)有被保護(hù)的話匙奴,也許同樣是讓 count++,他會(huì)用 countController.sink.add(++_count) 這種原始方法妄荔,而不是調(diào)用你已經(jīng)封裝好了的 increment 方法泼菌。

雖然兩種方式的效果完全一樣,但是第二種方式將會(huì)讓我們的business logic零散的混入其他代碼中懦冰。久而久之項(xiàng)目中就會(huì)大量充斥著這些垃圾代碼增加項(xiàng)目代碼耦合程度灶轰,非常不利于代碼的維護(hù)以及閱讀。

所以刷钢,請(qǐng)務(wù)必使用私有變量保護(hù)你的 Model笋颤。

控制你的刷新范圍

在 Flutter 中,組合大于繼承的特性隨處可見(jiàn)内地。常見(jiàn)的 Widget 實(shí)際上都是由更小的 Widget 組合而成伴澄,直到基本組件為止。為了使我們的應(yīng)用擁有更高的性能阱缓,控制 Widget 的刷新范圍便顯得至關(guān)重要非凌。

我們已經(jīng)通過(guò)前面的介紹了解到了,在 Provider 中獲取 Model 的方式會(huì)影響刷新范圍荆针。所有敞嗡,請(qǐng)盡量使用 Consumer 來(lái)獲取祖先 Model颁糟,以維持最小刷新范圍。

Q&A

在這里對(duì)一些大家可能會(huì)有疑問(wèn)的常見(jiàn)問(wèn)題做一個(gè)回答喉悴,如果你還有這之外的疑問(wèn)的話棱貌,歡迎在下方評(píng)論區(qū)一起討論。

Provider 是如何做到狀態(tài)共享的

這個(gè)問(wèn)題實(shí)際上得分兩步箕肃。

獲取頂層數(shù)據(jù)

實(shí)際上在祖先節(jié)點(diǎn)中共享數(shù)據(jù)這件事我們已經(jīng)在之前的文章中接觸過(guò)很多次了婚脱,都是通過(guò)系統(tǒng)的 InheritedWidget 進(jìn)行實(shí)現(xiàn)的。

Provider 也不例外勺像,在所有 Provider 的 build 方法中障贸,返回了一個(gè) InheritedProvider。

class InheritedProvider<T> extends InheritedWidget

Flutter 通過(guò)在每個(gè) Element 上維護(hù)一個(gè) InheritedWidget 哈希表來(lái)向下傳遞 Element 樹(shù)中的信息吟宦。通常情況下篮洁,多個(gè)
Element 引用相同的哈希表,并且該表僅在 Element 引入新的 InheritedWidget 時(shí)改變督函。

所以尋找祖先節(jié)點(diǎn)的時(shí)間復(fù)雜度為 O(1) ??

通知刷新

通知刷新這一步實(shí)際上在講各種 Provider 的時(shí)候已經(jīng)講過(guò)了嘀粱,其實(shí)就是使用了 Listener 模式。Model 中維護(hù)了一堆聽(tīng)眾辰狡,然后 notifiedListener 通知刷新锋叨。(空間換時(shí)間??

為什么全局狀態(tài)需要放在頂層 MaterialApp 之上

這個(gè)問(wèn)題需要結(jié)合 Navigator 以及 BuildContext 來(lái)回答,在之前的文章中 Flutter | 深入理解BuildContext 已經(jīng)解釋過(guò)了宛篇,這里不再贅述娃磺。

我應(yīng)該在哪里進(jìn)行數(shù)據(jù)初始化

對(duì)于數(shù)據(jù)初始化這個(gè)問(wèn)題,我們必須要分類討論叫倍。

全局?jǐn)?shù)據(jù)

當(dāng)我們需要獲取全局頂層數(shù)據(jù)(就像之前 CounterApp 例子一樣)并需要做一些會(huì)產(chǎn)生額外結(jié)果的時(shí)候偷卧,main 函數(shù)是一個(gè)很好的選擇。

我們可以在 main 方法中創(chuàng)建 Model 并進(jìn)行初始化的工作吆倦,這樣就只會(huì)執(zhí)行一次听诸。

單頁(yè)面

如果我們的數(shù)據(jù)只是在這個(gè)頁(yè)面中需要使用,那么你有這兩種方式可以選擇蚕泽。

StatefulWidget

這里訂正一個(gè)錯(cuò)誤晌梨,感謝 @曉杰的V笑 以及 @fantasy525 在討論中幫我指出。

在之前文章的版本中我推薦大家在 State 的 didChangeDependence 中進(jìn)行數(shù)據(jù)初始化须妻。這里其實(shí)是使用 BLoC 延續(xù)下來(lái)的習(xí)慣仔蝌。因?yàn)槭褂昧?InheritWidget 之后,只有在 State 的 didChangeDependence 階段進(jìn)行 Inherit 初始化荒吏,initState 階段是拿不到數(shù)據(jù)的敛惊。而由于 BLoC 是使用的 Stream,數(shù)據(jù)直接走 Stream 進(jìn)來(lái)绰更,由 StreamBuilder 去 listen瞧挤,這樣 State 的依賴一直都只是這個(gè) Stream 對(duì)象而已锡宋,不會(huì)再次觸發(fā) didChangeDependence 方法。那 Provider 有何不同呢。

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

源碼中的注釋解釋了,如果這個(gè) Provider.of<T>(context) listen 了的話痛悯,那么當(dāng) notifyListeners 的時(shí)候嵌巷,就會(huì)觸發(fā) context 所對(duì)應(yīng)的 State 的 [State.build] 和 [State.didChangeDependencies] 方法。也就是說(shuō)丹皱,如果你使用了非 Provider 提供的數(shù)據(jù)妒穴,例如 ChangeNotifierProvider 這樣會(huì)改變依賴的類,并且獲取數(shù)據(jù)時(shí) Provider.of<T>(context, listen: true) 選擇 listen (默認(rèn)就為 listen)的話摊崭,數(shù)據(jù)刷新時(shí)會(huì)重新運(yùn)行 didChangeDependencies 和 build 兩個(gè)方法讼油。這樣一來(lái)對(duì) didChangeDependencies 也會(huì)產(chǎn)生副作用。假如在這里請(qǐng)求了數(shù)據(jù)呢簸,當(dāng)數(shù)據(jù)到來(lái)的時(shí)候矮台,又回觸發(fā)下一次請(qǐng)求,最終無(wú)限請(qǐng)求下去根时。

這里除了副作用以外還有一點(diǎn)瘦赫,假如數(shù)據(jù)改變是一個(gè)同步行為,例如這里的 counter.increment 這樣的方法蛤迎,在 didChangeDependencies 中調(diào)用的話确虱,就會(huì)造成下面這個(gè)錯(cuò)誤。

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 的構(gòu)建算法有關(guān)替裆。簡(jiǎn)單來(lái)說(shuō)校辩,就是不能夠在 State 的 build 期間調(diào)用 setState() 或者 markNeedsBuild(),在我們這里 didChangeDependence 的時(shí)候調(diào)用了此方法辆童,導(dǎo)致出現(xiàn)這個(gè)錯(cuò)誤宜咒。異步數(shù)據(jù)則會(huì)由于 event loop 的緣故不會(huì)立即執(zhí)行。想要深入了解的同學(xué)可以看閑魚(yú)技術(shù)的這篇文章:Flutter快速上車之Widget把鉴。

感覺(jué)處處都是坑啊故黑,那該怎么初始化呢。目前我找到的辦法是這樣纸镊,首先 要保證初始化數(shù)據(jù)不能夠產(chǎn)生副作用倍阐,我們需要找一個(gè)在 State 聲明周期內(nèi)一定只會(huì)運(yùn)行一次的方法。initState 就是為此而生的逗威。但是 initState 不是無(wú)法獲取到 Inherit 嗎峰搪。但是我們現(xiàn)在本身就在頁(yè)面頂層啊,頁(yè)面級(jí)別的 Model 就在頂層被創(chuàng)建凯旭,現(xiàn)在根本就不需要 Inherit概耻。

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

頁(yè)面級(jí)別的 Model 數(shù)據(jù)都在頁(yè)面頂層 Widget 創(chuàng)建并初始化即可使套。

我們還需要考慮一種情況,假如這個(gè)操作是一個(gè)同步操作應(yīng)該如何處理鞠柄,就如我們之前舉的 CounterModel.increment 這個(gè)操作一樣侦高。

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

我們通過(guò) addPostFrameCallback 回調(diào)中在第一幀 build 結(jié)束時(shí)調(diào)用 increment 方法,這樣就不會(huì)出現(xiàn)構(gòu)建錯(cuò)誤了厌杜。

cascade

你也可以在使用 dart 的級(jí)連語(yǔ)法 ..do() 直接在頁(yè)面的 StatelessWidget 成員變量聲明時(shí)進(jìn)行初始化奉呛。

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

使用這種方式需要注意,當(dāng)這個(gè) StatelessWidget 重新運(yùn)行 build 的時(shí)候夯尽,狀態(tài)會(huì)丟失瞧壮。這種情況在 TabBarView 中的子頁(yè)面切換過(guò)程中就可能會(huì)出現(xiàn)。

所以建議還是使用第一種匙握,在 State 中初始化數(shù)據(jù)咆槽。

我需要擔(dān)心性能問(wèn)題嗎

是的,無(wú)論 Flutter 再怎么努力優(yōu)化圈纺,Provider 考慮的情況再多秦忿,我們總是有辦法讓?xiě)?yīng)用卡爆 ??(開(kāi)個(gè)玩笑)

僅當(dāng)我們不遵守其行為規(guī)范的時(shí)候,會(huì)出現(xiàn)這樣的情況蛾娶。性能會(huì)因?yàn)槟愕母鞣N不當(dāng)操作而變得很糟糕灯谣。我的建議是:遵守其規(guī)范,做任何事情都考慮對(duì)性能的影響茫叭,要知道 Flutter 把更新算法可是優(yōu)化到了 O(N)酬屉。

Provider 僅僅是對(duì) InheritedWidget 的一個(gè)升級(jí),你不必?fù)?dān)心引入 Provider 會(huì)對(duì)應(yīng)用造成性能問(wèn)題揍愁。

為什么選擇 Provider

Provider 不僅做到了提供數(shù)據(jù)呐萨,而且它擁有著一套完整的解決方案,覆蓋了你會(huì)遇到的絕大多數(shù)情況莽囤。就連 BLoC 未解決的那個(gè)棘手的 dispose 問(wèn)題谬擦,和 ScopedModel 的侵入性問(wèn)題,它也都解決了朽缎。

然而它就是完美的嗎惨远,并不是,至少現(xiàn)在來(lái)說(shuō)话肖。Flutter Widget 構(gòu)建模式很容易在 UI 層面上組件化北秽,但是僅僅使用 Provider,Model 和 View 之間還是容易產(chǎn)生依賴最筒。

我們只有通過(guò)手動(dòng)將 Model 轉(zhuǎn)化為 ViewModel 這樣才能消除掉依賴關(guān)系贺氓,所以假如各位有組件化的需求,還需要另外處理床蜘。

不過(guò)對(duì)于大多數(shù)情況來(lái)說(shuō)辙培,Provider 足以優(yōu)秀蔑水,它能夠讓你開(kāi)發(fā)出簡(jiǎn)單高性能扬蕊、層次清晰 的應(yīng)用搀别。

我應(yīng)該如何選擇狀態(tài)管理

介紹了這么多狀態(tài)管理,你可能會(huì)發(fā)現(xiàn)尾抑,一些狀態(tài)管理之間職責(zé)并不沖突歇父。例如 BLoC 可以結(jié)合 RxDart 庫(kù)變得很強(qiáng)大,很好用再愈。而 BLoC 也可以結(jié)合 Provider / ScopedModel 一起使用庶骄。那我應(yīng)該選擇哪種狀態(tài)管理方式呢。

我的建議是遵守以下幾點(diǎn):

  1. 使用狀態(tài)管理的目的是為了讓編寫(xiě)代碼變得更簡(jiǎn)單践磅,任何會(huì)增加你的應(yīng)用復(fù)雜度的狀態(tài)管理,統(tǒng)統(tǒng)都不要用灸异。
  2. 選擇自己能夠 hold 住的府适,BLoC / Rxdart / Redux / Fish-Redux 這些狀態(tài)管理方式都有一定上手難度,不要選自己無(wú)法理解的狀態(tài)管理方式肺樟。
  3. 在做最終決定之前檐春,敲一敲 demo,真正感受各個(gè)狀態(tài)管理方式給你帶來(lái)的 好處/壞處 然后再做你的決定么伯。

希望能夠幫助到你疟暖。

源碼淺析

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

Flutter 中的 Builder 模式

在 Provider 中,各種 Provider 的原始構(gòu)造方法都有一個(gè) builder 參數(shù)田柔,這里一般就用 (_) => XXXModel() 就行了俐巴。感覺(jué)有點(diǎn)多次一舉,為什么不能像 .value() 構(gòu)造方法那樣簡(jiǎn)潔呢硬爆。

實(shí)際上欣舵,Provider 為了幫我們管理 Model,使用到了 delegation pattern缀磕。

builder 聲明的 ValueBuilder 最終被傳入代理類 BuilderStateDelegate / SingleValueDelegate缘圈。 然后通過(guò)代理類才實(shí)現(xiàn)的 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袜蚕,其余的請(qǐng)自行查看源碼糟把。

如何實(shí)現(xiàn) MultiProvider

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

MultiProvider 實(shí)際上就是通過(guò)每一個(gè) provider 都實(shí)現(xiàn)了的 cloneWithChild 方法把自己一層一層包裹起來(lái)。

MultiProvider(
    providers:[
        AProvider,
        BProvider,
        CProvider,
    ],
    child: child,
)

等價(jià)于

AProvider(
    child: BProvider(
        child: CProvider(
            child: child,
        ),
    ),
)

寫(xiě)在最后

這次寫(xiě)的太順暢牲剃,不小心就寫(xiě)得過(guò)多了遣疯。能看到這里的朋友,都很強(qiáng) ??颠黎。

與其說(shuō)這次是 Provider 專場(chǎng)另锋,更像是把狀態(tài)管理自己所遇到的心得都總結(jié)在這里了滞项。希望能夠給各位有參考價(jià)值。

后期的 Tips 和 Q&A 有一部分實(shí)際上對(duì)大多數(shù)狀態(tài)管理都適用夭坪,我后面會(huì)考慮把這些專門(mén)拉出來(lái)講一篇文判。不過(guò)下篇文章主題已經(jīng)決定了,在 Flutter 中實(shí)現(xiàn)無(wú) context 導(dǎo)航 的室梅。如果你感興趣的話一定不要錯(cuò)過(guò)戏仓。

如果您對(duì)Provider還有任何疑問(wèn)或者文章的建議,歡迎在下方評(píng)論區(qū)以及我的郵箱1652219550a@gmail.com與我聯(lián)系亡鼠,我會(huì)及時(shí)回復(fù)赏殃!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市间涵,隨后出現(xiàn)的幾起案子仁热,更是在濱河造成了極大的恐慌,老刑警劉巖勾哩,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抗蠢,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡思劳,警方通過(guò)查閱死者的電腦和手機(jī)迅矛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)潜叛,“玉大人秽褒,你說(shuō)我怎么就攤上這事⊥担” “怎么了销斟?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)椒舵。 經(jīng)常有香客問(wèn)我票堵,道長(zhǎng),這世上最難降的妖魔是什么逮栅? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任悴势,我火速辦了婚禮,結(jié)果婚禮上措伐,老公的妹妹穿的比我還像新娘特纤。我一直安慰自己,他們只是感情好侥加,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布捧存。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪昔穴。 梳的紋絲不亂的頭發(fā)上镰官,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音吗货,去河邊找鬼泳唠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛宙搬,可吹牛的內(nèi)容都是我干的笨腥。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼勇垛,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼脖母!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起闲孤,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤谆级,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后讼积,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體哨苛,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年币砂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片玻侥。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡决摧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出凑兰,到底是詐尸還是另有隱情掌桩,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布姑食,位于F島的核電站波岛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏音半。R本人自食惡果不足惜则拷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望曹鸠。 院中可真熱鬧煌茬,春花似錦、人聲如沸彻桃。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至眠屎,卻和暖如春剔交,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背改衩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工岖常, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人燎字。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓腥椒,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親候衍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子笼蛛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容