Flutter 狀態(tài)管理之Provider

flutter中狀態(tài)管理是重中之重趁舀,每當(dāng)談這個(gè)話題,總有說(shuō)不完的話惫霸。

在正式介紹 Provider 為什么我們需要狀態(tài)管理。如果你已經(jīng)對(duì)此十分清楚拄衰,那么建議直接跳過(guò)這一節(jié)它褪。
如果我們的應(yīng)用足夠簡(jiǎn)單饵骨,Flutter 作為一個(gè)聲明式框架翘悉,你或許只需要將 數(shù)據(jù) 映射成 視圖 就可以了。你可能并不需要狀態(tài)管理居触,就像下面這樣妖混。

image

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

這又是什么鬼抬旺。我們很難再清楚的測(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)住?br> 在 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)而生孵坚。

Provider 是什么

通過(guò)使用Provider而不用手動(dòng)編寫(xiě)InhertedWidget,您將獲取自動(dòng)分配窥淆、延遲加載卖宠、大大減少每次創(chuàng)建新類(lèi)的代碼。

首先在yaml中添加,具體版本號(hào)參考:官方Provider pub忧饭,當(dāng)前版本號(hào)是4.1.3.

  Provider: ^4.1.3

然后運(yùn)行

flutter pub get

獲取到最新的包到本地扛伍,在需要的文件夾內(nèi)導(dǎo)入

import 'package:provider/provider.dart';

簡(jiǎn)單例子

我們還用點(diǎn)擊按鈕新增數(shù)字的例子

首先創(chuàng)建存儲(chǔ)數(shù)據(jù)的Model

class ProviderModel extends ChangeNotifier {

    int _count=0;
    ProviderModel();
   void plus() {
   /// 在數(shù)據(jù)變動(dòng)的時(shí)候通知監(jiān)聽(tīng)者刷新UI
    _count = _count + 1;
    notifyListeners();
  }
}

構(gòu)造view

/// 使用Consumer來(lái)監(jiān)聽(tīng)全局刷新UI
Consumer<ProviderModel>(
        builder:
            (BuildContext context, ProviderModel value, Widget child) {
          print('Consumer 0 刷新');
          _string += 'c0 ';
          return _Row(
            value: value._count.toString(),
            callback: () {
              context.read<ProviderModel>().plus();
            },
          );
        },
        child: _Row(
          value: '0',
          callback: () {
            context.read<ProviderModel>().plus();
          },
        ),
      )

測(cè)試下看下效果:

image

單個(gè)Model多個(gè)小部件分別刷新(局部刷新)

單個(gè)model實(shí)現(xiàn)單個(gè)頁(yè)面多個(gè)小部件分別刷新,是使用Selector<Model,int>來(lái)實(shí)現(xiàn)词裤,首先看下構(gòu)造函數(shù):

class Selector<A, S> extends Selector0<S> {
  /// {@macro provider.selector}
  Selector({
    Key key,
    @required ValueWidgetBuilder<S> builder,
    @required S Function(BuildContext, A) selector,
    ShouldRebuild<S> shouldRebuild,
    Widget child,
  })  : assert(selector != null),
        super(
          key: key,
          shouldRebuild: shouldRebuild,
          builder: builder,
          selector: (context) => selector(context, Provider.of(context)),
          child: child,
        );
}

可以看到Selector繼承了Selector0,再看Selector關(guān)鍵build代碼:

class _Selector0State<T> extends SingleChildState<Selector0<T>> {
  T value;
  Widget cache;
  Widget oldWidget;

  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    final selected = widget.selector(context);

    var shouldInvalidateCache = oldWidget != widget ||
        (widget._shouldRebuild != null &&
            widget._shouldRebuild.call(value, selected)) ||
        (widget._shouldRebuild == null &&
            !const DeepCollectionEquality().equals(value, selected));
    if (shouldInvalidateCache) {
      value = selected;
      oldWidget = widget;
      cache = widget.builder(
        context,
        selected,
        child,
      );
    }
    return cache;
  }
}

根據(jù)我們傳入的_shouldRebuild來(lái)判斷是否需要更新刺洒,如果需要更新則執(zhí)行widget.build(context,selected,child),否則返回已經(jīng)緩存的cache.當(dāng)沒(méi)有_shouldRebuild參數(shù)時(shí)則根據(jù)widget.selector(ctx)的返回值判斷是否和舊值相等,不等則更新UI吼砂。

所以我們不寫(xiě)shouldRebuild也是可以的逆航。

局部刷新用法

  Widget build(BuildContext context) {
    print('page 1');
    _string += 'page ';
    return Scaffold(
      appBar: AppBar(
        title: Text('Provider 全局與局部刷新'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Text('全局刷新<Consumer>'),
            Consumer<ProviderModel>(
              builder:
                  (BuildContext context, ProviderModel value, Widget child) {
                print('Consumer 0 刷新');
                _string += 'c0 ';
                return _Row(
                  value: value._count.toString(),
                  callback: () {
                    context.read<ProviderModel>().plus();
                  },
                );
              },
              child: _Row(
                value: '0',
                callback: () {
                  context.read<ProviderModel>().plus();
                },
              ),
            ),
            SizedBox(
              height: 40,
            ),
            Text('局部刷新<Selector>'),
            Selector<ProviderModel, int>(
              builder: (ctx, value, child) {
                print('Selector 1 刷新');
                _string += 's1 ';
                return Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text('Selector<Model,int>次數(shù):' + value.toString()),
                    OutlineButton(
                      onPressed: () {
                        context.read<ProviderModel>().plus2();
                      },
                      child: Icon(Icons.add),
                    )
                  ],
                );
              },
              selector: (ctx, model) => model._count2,
              shouldRebuild: (m1, m2) {
                print('s1:$m1 $m2 ${m1 != m2 ? '不相等,本次刷新' : '數(shù)據(jù)相等渔肩,本次不刷新'}');
                return m1 != m2;
              },
            ),
            SizedBox(
              height: 40,
            ),
            Text('局部刷新<Selector>'),
            Selector<ProviderModel, int>(
              selector: (context, model) => model._count3,
              shouldRebuild: (m1, m2) {
                print('s2:$m1 $m2 ${m1 != m2 ? '不相等因俐,本次刷新' : '數(shù)據(jù)相等,本次不刷新'}');
                return m1 != m2;
              },
              builder: (ctx, value, child) {
                print('selector 2 刷新');
                _string += 's2 ';
                return Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text('Selector<Model,int>次數(shù):' + value.toString()),
                    OutlineButton(
                      onPressed: () {
                        ctx.read<ProviderModel>().plus3();
                      },
                      child: Icon(Icons.add),
                    )
                  ],
                );
              },
            ),
            SizedBox(
              height: 40,
            ),
            Text('刷新次數(shù)和順序:↓'),
            Text(_string),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                OutlineButton(
                  child: Icon(Icons.refresh),
                  onPressed: () {
                    setState(() {
                      _string += '\n';
                    });
                  },
                ),
                OutlineButton(
                  child: Icon(Icons.close),
                  onPressed: () {
                    setState(() {
                      _string = '';
                    });
                  },
                )
              ],
            )
          ],
        ),
      ),
    );
  }

效果:

image

當(dāng)我們點(diǎn)擊局部刷新s1,執(zhí)行s1build抹剩,s1不相等撑帖,s2相等不刷新。輸出:

flutter: s2:5 5 數(shù)據(jù)相等澳眷,本次不刷新
flutter: s1:6 7 不相等胡嘿,本次刷新
flutter: Selector 1 刷新
flutter: Consumer 0 刷新

當(dāng)點(diǎn)擊s2,s2的值不相等刷新UI,s1數(shù)據(jù)相等,不刷新UI.

flutter: s2:2 3 不相等钳踊,本次刷新
flutter: selector 2 刷新
flutter: s1:0 0 數(shù)據(jù)相等衷敌,本次不刷新
flutter: Consumer 0 刷新

可以看到上邊2次Consumer每次都刷新了,我們探究下原因拓瞪。

Consumer 全局刷新

Consumer繼承了SingleCHildStatelessWidget,當(dāng)我們?cè)?code>ViewModel中調(diào)用notification則當(dāng)前widget被標(biāo)記為dirty,然后在build中執(zhí)行傳入的builder函數(shù)逢享,在下幀則會(huì)刷新UI

Selector<T,S>則被標(biāo)記dirty時(shí)執(zhí)行_Selector0State中的buildWithChild(ctx,child)函數(shù)時(shí)吴藻,根據(jù)selected_shouldRebuild來(lái)判斷是否需要執(zhí)行widget.builder(ctx,selected,child)(刷新UI).

其他用法

多model寫(xiě)法

只需要在所有需要model的上級(jí)包裹即可瞒爬,當(dāng)我們一個(gè)page需要2個(gè)model的時(shí)候,我么通常這樣子寫(xiě):

class BaseProviderRoute extends StatelessWidget {
  BaseProviderRoute({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<ProviderModel>(
          create: (_) => ProviderModel(),
        ),
        ChangeNotifierProvider<ProviderModel2>(create: (_) => ProviderModel2()),
      ],
      child: BaseProvider(),
    );
  }
}

當(dāng)然是用的時(shí)候和單一model一致的沟堡。

  Selector<ProviderModel2, int>(
    selector: (context, model) => model.value,
    builder: (ctx, value, child) {
      print('model2 s1 刷新');
      _string += 'm2s1 ';
      return Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('Selector<Model2,int>次數(shù):' + value.toString()),
          OutlineButton(
            onPressed: () {
              ctx.read<ProviderModel2>().add(2);
            },
            child: Icon(Icons.add),
          )
        ],
      );
    },
  ),

watch && read

watch源碼是Provider.of<T>(this),默認(rèn)Provider.of<T>(this)listen=true.

static T of<T>(BuildContext context, {bool listen = true}){
final inheritedElement = _inheritedElementOf<T>(context);

    if (listen) {
      context.dependOnInheritedElement(inheritedElement);
    }

    return inheritedElement.value;
}

read源碼是Provider.of<T>(this, listen: false),watch/read只是寫(xiě)法簡(jiǎn)單一點(diǎn)侧但,并無(wú)高深結(jié)構(gòu)。

當(dāng)我們想要監(jiān)聽(tīng)值的變化則是用watch,當(dāng)想調(diào)用model的函數(shù)時(shí)則使用read

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末航罗,一起剝皮案震驚了整個(gè)濱河市禀横,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌粥血,老刑警劉巖柏锄,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異复亏,居然都是意外死亡趾娃,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)缔御,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)抬闷,“玉大人,你說(shuō)我怎么就攤上這事耕突◇猿桑” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵眷茁,是天一觀的道長(zhǎng)炕泳。 經(jīng)常有香客問(wèn)我,道長(zhǎng)上祈,這世上最難降的妖魔是什么培遵? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任挣磨,我火速辦了婚禮,結(jié)果婚禮上荤懂,老公的妹妹穿的比我還像新娘。我一直安慰自己塘砸,他們只是感情好节仿,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著掉蔬,像睡著了一般廊宪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上女轿,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天箭启,我揣著相機(jī)與錄音,去河邊找鬼蛉迹。 笑死傅寡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的北救。 我是一名探鬼主播荐操,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼珍策!你這毒婦竟也來(lái)了托启?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤攘宙,失蹤者是張志新(化名)和其女友劉穎屯耸,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蹭劈,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡疗绣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了铺韧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片持痰。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖祟蚀,靈堂內(nèi)的尸體忽然破棺而出工窍,到底是詐尸還是另有隱情,我是刑警寧澤前酿,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布患雏,位于F島的核電站,受9級(jí)特大地震影響罢维,放射性物質(zhì)發(fā)生泄漏淹仑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望匀借。 院中可真熱鬧颜阐,春花似錦、人聲如沸吓肋。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)是鬼。三九已至肤舞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間均蜜,已是汗流浹背李剖。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留囤耳,地道東北人篙顺。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像充择,于是被迫代替她去往敵國(guó)和親慰安。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345