Flutter輕量級狀態(tài)管理

響應(yīng)式的編程框架中都會有一個永恒的主題——“狀態(tài)(State)管理”,無論是在React/Vue(兩者都是支持響應(yīng)式編程的Web開發(fā)框架)還是Flutter中,他們討論的問題和解決的思想都是一致的。言歸正傳叁执,我們想一個問題款违,StatefulWidget的狀態(tài)應(yīng)該被誰管理?Widget本身隆夯?父Widget?都會别伏?還是另一個對象蹄衷?答案是取決于實際情況!以下是管理狀態(tài)的最常見的方法:

  • Widget管理自己的狀態(tài)厘肮。
  • Widget管理子Widget狀態(tài)愧口。
  • 混合管理(父Widget和子Widget都管理狀態(tài))。

如何決定使用哪種管理方法类茂?下面是官方給出的一些原則可以幫助你做決定:

  • 如果狀態(tài)是用戶數(shù)據(jù)耍属,如復(fù)選框的選中狀態(tài)、滑塊的位置巩检,則該狀態(tài)最好由父Widget管理厚骗。
  • 如果狀態(tài)是有關(guān)界面外觀效果的,例如顏色兢哭、動畫领舰,那么狀態(tài)最好由Widget本身來管理。
  • 如果某一個狀態(tài)是不同Widget共享的則最好由它們共同的父Widget管理。

在Widget內(nèi)部管理狀態(tài)封裝性會好一些冲秽,而在父Widget中管理會比較靈活舍咖。有些時候,如果不確定到底該怎么管理狀態(tài)锉桑,那么推薦的首選是在父widget中管理(靈活會顯得更重要一些)谎仲。

一、狀態(tài)管理的現(xiàn)狀

由于flutter發(fā)展的時間不長刨仑,狀態(tài)管理方案各家也都在探索郑诺,目前主流的狀態(tài)管理,scope_model杉武、redux辙诞、fish_redux、BloC轻抱、rxDart飞涂、provider等等,還有一些探索中的模式祈搜,融合多個模式的優(yōu)點较店,比如reBloc,它們都各具優(yōu)勢容燕,也都不完美梁呈。

目前工程中使用的是fish_redux,使用redux的理念對業(yè)務(wù)層再做了一層封裝蘸秘,對于我們現(xiàn)在的項目來說官卡,太重,學(xué)習(xí)成本也很高醋虏,不利于項目開發(fā)的介入寻咒,再者,現(xiàn)在flutter版本更新頻繁颈嚼,三方的更新速度過慢毛秘,跟不上業(yè)務(wù)的發(fā)展。

flutter的狀態(tài)管理分類

按使用的范圍來分阻课,flutter的狀態(tài)管理分為兩種:局部狀態(tài)和全局狀態(tài)叫挟。

  • 局部狀態(tài):flutter原生提供了InheritWidget控件來實現(xiàn)局部狀態(tài)的控制。當(dāng)InheritedWidget發(fā)生變化時柑肴,它的子樹中所有依賴它數(shù)據(jù)的Widget都會進行rebuild霞揉。但當(dāng)業(yè)務(wù)復(fù)雜時旬薯,邏輯與UI耦合嚴(yán)重晰骑,變的難以維護,復(fù)用性也會非常差。

  • 全局狀態(tài):Flutter沒有提供原生的全局狀態(tài)管理硕舆,基本上是需要依賴第三方庫來實現(xiàn)秽荞。雖然在根控件上使用InheritedWidget也可以實現(xiàn),不過會帶來很多的問題抚官,比如狀態(tài)傳遞過深扬跋,難以維護等。

個人推薦狀態(tài)管理

要應(yīng)對如上的狀態(tài)管理凌节,由于主流方案都各具優(yōu)勢钦听,也都不完美,必然是組合使用倍奢,個人覺得目前最好的方案是RxBloc和provider的組合使用:

  • RxBloc在處理大量異步事件以及分離業(yè)務(wù)邏輯上表現(xiàn)很優(yōu)秀朴上,APP業(yè)務(wù)異步事件非常多,復(fù)雜業(yè)務(wù)也很多卒煞,UI和業(yè)務(wù)邏輯分離是家常便飯痪宰,需要優(yōu)秀的設(shè)計來支持,但是在共享狀態(tài)上還有一些缺陷
  • Provider是官方團隊推薦的狀態(tài)管理包畔裕,內(nèi)部封裝了InheritedWidget衣撬,RxBloc在共享狀態(tài)上還有一些缺陷由Provider來彌補
  • Rx社區(qū)活躍,對Stream做了擴展扮饶,變的更好用功能也更強大具练,Provider是由官方開發(fā),該組合持續(xù)穩(wěn)定
  • 局部狀態(tài)使用InheritedWidget實現(xiàn)是沒有問題的甜无,不使用是因為會產(chǎn)生很多的膠水的代碼

Tips:

具體每個方案的優(yōu)劣就不在本文中詳述靠粪,自行g(shù)oogle即可,這里著重介紹RxBloc和Provider的流程和使用毫蚓。

在這之前占键,你需要了解如下概念:

  • Dart中的Stream是什么,StreamBuilder是什么元潘,怎么使用
  • Rx的基本概念及對Stream封裝后的基本使用

二畔乙、局部狀態(tài)管理 —— RxBLoC

局部狀態(tài)管理,其實flutter自身已經(jīng)為我們提供了狀態(tài)管理翩概,而且你經(jīng)常都在用到牲距,它就是 Stateful widget。當(dāng)我們接觸到flutter的時候钥庇,首先需要了解的就是有些小部件是有狀態(tài)的牍鞠,有些則是無狀態(tài)的。StatelessWidget 與StatefulWidget评姨。

在stateful widget中难述,我們widget的描述信息被放進了State,而stateful widget只是持有一些immutable的數(shù)據(jù)以及創(chuàng)建它的狀態(tài)而已。它的所有成員變量都應(yīng)該是final的胁后,當(dāng)狀態(tài)發(fā)生變化的時候店读,我們需要通知視圖重新繪制,這個過程就是setState攀芯。

這看上去很不錯屯断,我們改變狀態(tài)的時候setState一下就可以了。

在我們一開始構(gòu)建應(yīng)用的時候侣诺,也許很簡單殖演,我們這時候可能并不需要狀態(tài)管理。如下圖年鸳,setState就足夠了剃氧。

simple.png

但是隨著功能的增加,應(yīng)用程序?qū)袔资畟€甚至上百個狀態(tài)阻星。這個時候應(yīng)用應(yīng)該會是這樣朋鞍。

nan.png

一旦當(dāng)app的交互變得復(fù)雜,setState出現(xiàn)的次數(shù)便會顯著增加妥箕,每次setState都會重新調(diào)用build方法滥酥,這勢必對于性能、代碼的可讀性和維護性帶來一定的影響畦幢。

那我們就會希望有一種更加強大的方式坎吻,來管理我們的狀態(tài):

  • 能不能不使用setState就能刷新頁面呢?
  • 頁面足夠復(fù)雜的話宇葱,能否將業(yè)務(wù)和UI分離瘦真,提升可讀性和可維護性?
  • 如果頁面足夠復(fù)雜的話黍瞧,能不能盡量少重新調(diào)用子widget的build方法诸尽,提升性能?
  • 即使頁面簡單印颤,該方式也能勝任您机,并且不會造成麻煩(比如不像fish_redux有那么多的模板代碼)

于是BLoC呼之欲出,來幫我們處理這些問題年局。

BLoC是什么

BLoC代表業(yè)務(wù)邏輯組件(Business Logic Component)际看,由來自Google的兩位工程師 Paolo Soares和Cong Hui設(shè)計,并在2018年DartConf期間(2018年1月23日至24日)首次展示矢否。有興趣的話可以點擊觀看Youtube視頻仲闽。

BLoC是一種利用reactive programming方式構(gòu)建應(yīng)用的方法,這是一個由流構(gòu)成的完全異步的世界僵朗。

bloc流程圖.png

BLoC工作流程如下:

  • 用StreamBuilder包裹有狀態(tài)的部件赖欣,streambuilder將會監(jiān)聽一個流
  • 這個流來自于BLoC
  • 有狀態(tài)小部件中的數(shù)據(jù)來自于監(jiān)聽的流屑彻。
  • 用戶交互手勢被檢測到,產(chǎn)生了事件畏鼓。例如按了一下按鈕酱酬。
  • 調(diào)用bloc的功能來處理這個事件
  • 在bloc中處理完畢后將會吧最新的數(shù)據(jù)add進流的sink中
  • StreamBuilder監(jiān)聽到新的數(shù)據(jù)壶谒,產(chǎn)生一個新的snapshot云矫,并重新調(diào)用build方法
  • Widget被重新構(gòu)建

BLoC能夠允許我們完美的分離業(yè)務(wù)邏輯!再也不用考慮什么時候需要刷新了(setState不需要我們顯示調(diào)用)汗菜,一切交給StreamBuilder和BLoC让禀!

Tips:

通過上面的分析,也許我們會說那我們就可以跟StatefulWidget說88了陨界,但通過測試后巡揍,準(zhǔn)確地描述,應(yīng)該是可以和大部分StatefulWidget說88菌瘪,至少保持一個StatefulWidget腮敌,使用其state來保存BLoC實例,Stream在不需要使用的時候俏扩,需要顯示的調(diào)用close方法,不然會造成內(nèi)存泄露或循環(huán)引用

使用RxDart

ReactiveX是一個強大的庫债鸡,用于通過使用可觀察序列來編寫異步和基于事件的程序亏娜。它突破了語言和平臺的限制,為我們編寫異步程序提供了極大的便利嫉戚。

如果之前接觸過Rx系列刨裆,相信已收獲Rx帶來的便利。

僅使用flutter提供的Stream足夠我們實現(xiàn)BLoC彬檀,但RxDart豐富和擴展了Stream帆啃,使BLoC更簡單更強大。

RxDart對Stream做了哪些封裝窍帝,不是本文的重點链瓦,需要了解的話自行Google,RxDart具體的API到github自行查看RxDart盯桦。

舉個栗子

我們使用BLoC來實現(xiàn)如下這個功能慈俯,簡單的一個登陸(忽略丑巨的UI,測試而已哈...)拥峦,需求如下:

登錄demo.png
  • 輸入的賬號顯示在頭部已輸入賬號text中
  • 賬號在620位贴膘,密碼在612位,符合條件略号,登錄按鈕才可用
  • 點擊登錄刑峡,3s后洋闽,修改底部登錄狀態(tài)為已登錄
定義BLoC 抽象類

BLoC中無論是直接使用Stream還是RxDart,本質(zhì)都是Stream突梦,在Stream不需要使用的時候诫舅,我們需要顯示地調(diào)用close方法,所以寫一個簡單的抽象類宫患,所有的BLoC對象都繼承該抽象類刊懈,Stream的close都在dispose方法中實現(xiàn)。

// 所有Bloc的基類
abstract class BlocProviderBase {
  // 銷毀stream
  void dispose();
}
創(chuàng)建 LoginBLoC 登錄BLoC
/// 登錄 bloc
class LoginBlocProvider extends BlocProviderBase {
  String _account = '';
  String _password = '';

  final PublishSubject<String> _accountSub = PublishSubject<String>();
  PublishSubject<String> get accountSub => _accountSub;

  final PublishSubject<String> _passwordSub = PublishSubject<String>();
  PublishSubject<String> get passwordSub => _passwordSub;

  final PublishSubject<bool> _validSub = PublishSubject<bool>();
  PublishSubject<bool> get validSub => _validSub;

  final PublishSubject<String> _loginSub = PublishSubject<String>();
  PublishSubject<String> get loginSub => _loginSub;

  // 構(gòu)造方法
  LoginBlocProvider() {
    _handleSubscript();
  }

  // 登錄操作
  void doLogin() async {
    await Future.delayed(Duration(seconds: 3));

    print('登錄成功 => 用戶名:$_account, 密碼:$_password');

    _loginSub.add('登錄成功~');
  }

  // 處理訂閱
  void _handleSubscript() {
    CombineLatestStream<String, bool>([_accountSub, _passwordSub], (values) {
      return values.first.length >= 6 &&
          values.first.length <= 20 &&
          values.last.length >= 6 &&
          values.last.length <= 12;
    }).listen((value) {
      _validSub.sink.add(value);
    });

    _accountSub.listen((value) {
      _account = value;
    });

    _passwordSub.listen((value) {
      _password = value;
    });
  }

  // 銷毀
  void dispose() {
    _accountSub.close();
    _passwordSub.close();
    _validSub.close();
    _loginSub.close();
  }
}

為什么要使用私有變量“_”娃闲,提供get方法

一個應(yīng)用需要大量開發(fā)人員參與虚汛,你寫的代碼也許在幾個月之后被另外一個開發(fā)看到了,這時候假如你的變量沒有被保護的話皇帮,那么是可以隨意改變其中的屬性的卷哩,比如_account,如果直接進行賦值属拾,那么就破壞了整個BLoC的流程将谊。

雖然兩種方式的效果完全一樣,但是第二種方式將會讓我們的business logic零散的混入其他代碼中渐白,提高了代碼耦合程度尊浓,非常不利于代碼的維護以及閱讀,所以為了讓BLoC完全分離我們的業(yè)務(wù)邏輯礼预,請務(wù)必使用私有變量眠砾。

創(chuàng)建 LoginBLoC 實例

flutter常被人詬病的一點是嵌套過深,我們可以通過抽取子widget來一定程度上規(guī)避嵌套地獄托酸,本例中抽取了多個子Widget褒颈,一會詳細(xì)看代碼,但同時也就會帶來一個BLoC實例從父widget傳遞到子widget的問題励堡,這里我們使用Provider來實現(xiàn)局部共享谷丸,不使用InheritWidget的原因,上文中已說明应结,就不贅述了刨疼,Provider的具體使用,后面會詳解鹅龄,這里先主要說明RxBLoC揩慕。

上文中也提到,我們通過Stream和StreamBuilder實現(xiàn)局部刷新扮休,完全不需要使用setState了迎卤,那也就不需要使用StatefulWidget,但是我們需要在頁面銷毀的時候玷坠,調(diào)用BLoC實例的dispose方法蜗搔,我們就至少需要一個頂層的StatefulWidget來保存BLoC實例劲藐。

于是我們在state中創(chuàng)建并保存BLoC實例,并在build的頂層樟凄,使用Provider來共享該實例聘芜,且在state的dispose中調(diào)用BLoC實例中的dispose方法,關(guān)閉Stream:

class _ProviderSharePageHomeState extends State<ProviderSharePageHome> {
  LoginBlocProvider _bloc;

  @override
  void initState() {
    super.initState();

    _bloc = LoginBlocProvider();
  }

  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (ctx) => _bloc,
      child: Column(
        children: <Widget>[
          SizedBox(
            height: 50,
          ),
          LoginAccountWidget(),
          SizedBox(
            height: 10,
          ),
          AccountWidget(),
          SizedBox(
            height: 10,
          ),
          PasswordWidget(),
          SizedBox(
            height: 10,
          ),
          LoginButtonWidget(),
          SizedBox(
            height: 10,
          ),
          LoginStateWidget(),
        ],
      ),
    );
  }

  @override
  void dispose() {
    super.dispose();

    _bloc.dispose();
  }
}
LoginBLoC 的使用
  • 輸入流

以賬號輸入為例缝龄,與BLoC的連接如下:

class AccountWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _bloc = Provider.of<LoginBlocProvider>(context);

    return TextField(
      onChanged: (value) {
        _bloc.accountSub.add('$value');
      },
      decoration: InputDecoration(
        labelText: '用戶名',
        filled: true,
      ),
    );
  }
}

通過對TextField的onChanged方法監(jiān)聽汰现,將新的輸入數(shù)據(jù)通過bloc中的對應(yīng)的stream,發(fā)送給bloc二拐,由bloc做對應(yīng)的邏輯處理服鹅。

  • 輸出流

以輸入的用戶名text為例凳兵,使用StreamBuilder構(gòu)建如下:

class LoginAccountWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _bloc = Provider.of<LoginBlocProvider>(context);

    return Container(
        width: double.infinity,
        height: 40,
        color: Colors.black12,
        child: Center(
          child: StreamBuilder(
            stream: _bloc.accountSub.where((origin) {
              // 丟棄
              return origin.length >= 6 && origin.length <= 20;
            }).debounceTime(Duration(milliseconds: 500)),
            initialData: '',
            builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
              return Text(
                "輸入的用戶名:${snapshot.data.isEmpty ? '' : snapshot.data}",
                style: TextStyle(color: Colors.red),
              );
            },
          ),
        ));
  }
}

當(dāng)輸入的賬號和密碼符合規(guī)則百新,登錄按鈕按鈕才會變得可用,同樣是是使用StreamBuilder:

class LoginButtonWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _bloc = Provider.of<LoginBlocProvider>(context);

    return Container(
        width: 128,
        height: 48,
        child: StreamBuilder(
          stream: _bloc.validSub,
          initialData: false,
          builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
            return FlatButton(
              color: Colors.blueAccent,
              disabledColor: Colors.blueAccent.withAlpha(50),
              child: Text(
                '登錄',
                style: TextStyle(color: Colors.white),
              ),
              onPressed: snapshot.data
                  ? () {
                      print('點擊了登錄');

                      _bloc.doLogin();
                    }
                  : null,
            );
          },
        ));
  }
}

如此庐扫,整個登錄功能就實現(xiàn)了饭望,BLoC的流程就是這樣,其他功能的代碼請詳見DEMO形庭。

大型應(yīng)用中應(yīng)該如何組織 BLoC

大型應(yīng)用程序需要多個BLoC铅辞。一個好的模式是為每個屏幕使用一個頂級組件,并為每個復(fù)雜足夠的小部件使用一個萨醒。但是斟珊,太多的BLoC會變得很麻煩。此外富纸,如果您的應(yīng)用中有數(shù)百個可觀察量(流)囤踩,則會對性能產(chǎn)生負(fù)面影響。換句話說:不要過度設(shè)計你的應(yīng)用程序晓褪。

——Filip Hracek

三堵漱、全局狀態(tài)管理 —— Provider

Provider是目前官方推薦的全局狀態(tài)管理工具,由社區(qū)作者Remi Rousselet 和 Flutter Team共同編寫涣仿。

3.1 Provider的基本使用

在使用Provider的時候勤庐,我們主要關(guān)心三個概念:

  • ChangeNotifier:真正數(shù)據(jù)(狀態(tài))存放的地方
  • ChangeNotifierProvider:Widget樹中提供數(shù)據(jù)(狀態(tài))的地方,會在其中創(chuàng)建對應(yīng)的ChangeNotifier
  • Consumer:Widget樹中需要使用數(shù)據(jù)(狀態(tài))的地方
3.1.1 創(chuàng)建自己的ChangeNotifier

我們需要一個ChangeNotifier來保存我們的狀態(tài)好港,所以創(chuàng)建它

  • 這里我們可以使用繼承自ChangeNotifier愉镰,也可以使用混入,這取決于概率是否需要繼承自其它的類
  • 我們使用一個私有的_counter钧汹,并且提供了getter和setter
  • 在setter中我們監(jiān)聽到_counter的改變丈探,就調(diào)用notifyListeners方法,通知所有的Consumer進行更新
class CounterProvider extends ChangeNotifier {
  int _counter = 100;
  intget counter {
    return _counter;
  }
  set counter(int value) {
    _counter = value;
    notifyListeners();
  }
}
3.1.2 在Widget Tree中插入ChangeNotifierProvider

我們需要在Widget Tree中插入ChangeNotifierProvider崭孤,以便Consumer可以獲取到數(shù)據(jù):

  • 將ChangeNotifierProvider放到了頂層类嗤,這樣方便在整個應(yīng)用的任何地方可以使用CounterProvider
void main() {
  runApp(ChangeNotifierProvider(
    create: (context) => CounterProvider(),
    child: MyApp(),
  ));
}
3.1.3 使用Consumer引入和修改狀態(tài)
  • 引入位置一:在body中使用Consumer糊肠,Consumer需要傳入一個builder回調(diào)函數(shù),當(dāng)數(shù)據(jù)發(fā)生變化時遗锣,就會通知依賴數(shù)據(jù)的Consumer重新調(diào)用builder方法來構(gòu)建货裹;
  • 引入位置二:在floatingActionButton中使用Consumer,當(dāng)點擊按鈕時精偿,修改CounterNotifier中的counter數(shù)據(jù)弧圆;
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("provider測試"),
      ),
      body: Center(
        child: Consumer<CounterProvider>(
          builder: (ctx, counterPro, child) {
            return Text("當(dāng)前計數(shù):${counterPro.counter}", style: TextStyle(fontSize: 20, color: Colors.red),);
          }
        ),
      ),
      floatingActionButton: Consumer<CounterProvider>(
        builder: (ctx, counterPro, child) {
          return FloatingActionButton(
            child: child,
            onPressed: () {
              counterPro.counter += 1;
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
3.1.4 創(chuàng)建一個新的頁面,在新的頁面中修改數(shù)據(jù)
class BasicProviderSecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二個頁面"),
      ),
      floatingActionButton: Consumer<CounterProvider>(
        builder: (ctx, counterPro, child) {
          return FloatingActionButton(
            child: child,
            onPressed: () {
              counterPro.counter += 1;
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
3.2 Provider詳解
3.2.1 Consumer的builder方法解析
  • 參數(shù)一:context笔咽,每個build方法都會有上下文搔预,目的是知道當(dāng)前樹的位置
  • 參數(shù)二:ChangeNotifier對應(yīng)的實例,也是我們在builder函數(shù)中主要使用的對象
  • 參數(shù)三:child叶组,目的是進行優(yōu)化拯田,如果builder下面有一顆龐大的子樹,當(dāng)模型發(fā)生改變的時候甩十,我們并不希望重新build這顆子樹船庇,那么就可以將這顆子樹放到Consumer的child中,在這里直接引入即可(注意我案例中的Icon所放的位置)
3.2.2 Provider.of解析

事實上侣监,因為Provider是基于InheritedWidget鸭轮,所以我們在使用ChangeNotifier中的數(shù)據(jù)時,我們可以通過Provider.of的方式來使用橄霉,比如下面的代碼:

Text("當(dāng)前計數(shù):${Provider.of<CounterProvider>(context).counter}",
  style: TextStyle(fontSize: 30, color: Colors.purple),
),

我們會發(fā)現(xiàn)很明顯上面的代碼會更加簡潔窃爷,那么開發(fā)中是否要選擇上面這種方式呢?

  • 答案是否定的姓蜂,更多時候我們還是要選擇Consumer的方式按厘。

為什么呢?因為Consumer在刷新整個Widget樹時覆糟,會盡可能少的rebuild Widget刻剥。

方式一:Provider.of的方式完整的代碼:

  • 當(dāng)我們點擊了floatingActionButton時,HomePage的build方法會被重新調(diào)用滩字。
  • 這意味著整個HomePage的Widget都需要重新build
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("調(diào)用了HomePage的build方法");
    return Scaffold(
      appBar: AppBar(
        title: Text("Provider"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("當(dāng)前計數(shù):${Provider.of<CounterProvider>(context).counter}",
              style: TextStyle(fontSize: 30, color: Colors.purple),
            )
          ],
        ),
      ),
      floatingActionButton: Consumer<CounterProvider>(
        builder: (ctx, counterPro, child) {
          return FloatingActionButton(
            child: child,
            onPressed: () {
              counterPro.counter += 1;
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

方式二:將Text中的內(nèi)容采用Consumer的方式修改如下:

  • 你會發(fā)現(xiàn)HomePage的build方法不會被重新調(diào)用造虏;
  • 設(shè)置如果我們有對應(yīng)的child widget,可以采用上面案例中的方式來組織麦箍,性能更高漓藕;
3.2.3 Selector的選擇

Consumer是否是最好的選擇呢?并不是挟裂,它也會存在弊端

  • 比如當(dāng)點擊了floatingActionButton時享钞,我們在代碼的兩處分別打印它們的builder是否會重新調(diào)用;
  • 我們會發(fā)現(xiàn)只要點擊了floatingActionButton诀蓉,兩個位置都會被重新builder栗竖;
  • 但是floatingActionButton的位置有重新build的必要嗎暑脆?沒有,因為它是否在操作數(shù)據(jù)狐肢,并沒有展示添吗;
  • 如何可以做到讓它不要重新build了?使用Selector來代替Consumer

直接上代碼:

floatingActionButton: Selector<CounterProvider, CounterProvider>(
  selector: (ctx, provider) => provider,
  shouldRebuild: (pre, next) => false,
  builder: (ctx, counterPro, child) {
    print("floatingActionButton展示的位置builder被調(diào)用");
    return FloatingActionButton(
      child: child,
      onPressed: () {
        counterPro.counter += 1;
      },
    );
  },
  child: Icon(Icons.add),
),

Selector和Consumer對比份名,不同之處主要是三個關(guān)鍵點:

  • 關(guān)鍵點1:泛型參數(shù)是兩個
    • 泛型參數(shù)一:我們這次要使用的Provider
    • 泛型參數(shù)二:轉(zhuǎn)換之后的數(shù)據(jù)類型碟联,比如我這里轉(zhuǎn)換之后依然是使用CounterProvider,那么他們兩個就是一樣的類型
  • 關(guān)鍵點2:selector回調(diào)函數(shù)
    • 轉(zhuǎn)換的回調(diào)函數(shù)僵腺,你希望如何進行轉(zhuǎn)換
    • S Function(BuildContext, A) selector
    • 我這里沒有進行轉(zhuǎn)換鲤孵,所以直接將A實例返回即可
  • 關(guān)鍵點3:是否希望重新rebuild
    • 這里也是一個回調(diào)函數(shù),我們可以拿到轉(zhuǎn)換前后的兩個實例辰如;
    • bool Function(T previous, T next);
    • 因為這里我不希望它重新rebuild普监,無論數(shù)據(jù)如何變化,所以這里我直接return false丧没;

這個時候鹰椒,我們重新測試點擊floatingActionButton锡移,floatingActionButton中的代碼并不會進行rebuild操作呕童。

所以在某些情況下,我們可以使用Selector來代替Consumer淆珊,性能會更高夺饲。

3.2.4 MultiProvider

在開發(fā)中,我們需要共享的數(shù)據(jù)肯定不止一個施符,并且數(shù)據(jù)之間我們需要組織到一起往声,所以一個Provider必然是不夠的。

我們再增加一個新的ChangeNotifier

import'package:flutter/material.dart';

class UserInfo {
  String nickname;
  int level;

  UserInfo(this.nickname, this.level);
}

class UserProvider extends ChangeNotifier {
  UserInfo _userInfo = UserInfo("test", 18);

  set userInfo(UserInfo info) {
    _userInfo = info;
    notifyListeners();
  }

  get userInfo {
    return _userInfo;
  }
}

如果在開發(fā)中我們有多個Provider需要提供應(yīng)該怎么做呢戳吝?

方式一:多個Provider之間嵌套

  • 這樣做有很大的弊端浩销,如果嵌套層級過多不方便維護,擴展性也比較差
runApp(ChangeNotifierProvider(
    create: (context) => CounterProvider(),
    child: ChangeNotifierProvider(
      create: (context) => UserProvider(),
      child: MyApp()
    ),
  ));

方式二:使用MultiProvider

runApp(MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (ctx) => CounterProvider()),
    ChangeNotifierProvider(create: (ctx) => UserProvider()),
  ],
  child: MyApp(),
));
3.3 RxBLoC+Provider 栗子

由于RxBLoC是使用StreamBuilder來連接BLoC中的Stream听哭,當(dāng)有新數(shù)據(jù)時慢洋,會自動刷新子Widget,在全局共享時陆盘,我們并不需要使用Provider的notify功能普筹,所以共享的數(shù)據(jù)直接使用我們定義好的BLoC就可以了。

由于我們不需要notify功能隘马,所以在APP頂層共享數(shù)據(jù)是太防,也不需要使用ChangeNotifierProvider,直接使用Provider即可酸员,當(dāng)共享多個BLoC時蜒车,使用MultiProvider讳嘱,這個例子即是演示共享多個狀態(tài)。

3.3.1 小需求

需要在全局共享一個count和一個name酿愧,count的初始值是10呢燥,name的初始值是name,在count頁面寓娩,點擊右下角的+叛氨,count累加,在name頁面點擊右下角的+棘伴,name在后面拼接一個1字符串寞埠,count頁面和name頁面,都顯示count+name的格式化字符串焊夸。

counter.png
name.png
3.3.2 創(chuàng)建共享的count和name的BLoC
/// 數(shù)值 bloc
class CounterBlocProvider extends BlocProviderBase {
  int _counter = 10;

  BehaviorSubject<int> _counterSub = BehaviorSubject.seeded(10);
  BehaviorSubject<int> get counterSub => _counterSub;

  // 構(gòu)造方法
  CounterBlocProvider() {
    _handleSubscript();
  }

  // 增加操作
  void doAdd() {
    print('執(zhí)行了 counter 增加操作');

    _counterSub.add(++_counter);
  }

  // 處理訂閱
  void _handleSubscript() {
    _counterSub.listen((value) {
      _counter = value;
    });
  }

  // 銷毀
  void dispose() {
    _counterSub.close();
  }
}

/// name bloc
class NameBlocProvider extends BlocProviderBase {
  String _name = 'name';

  BehaviorSubject<String> _nameSub = BehaviorSubject.seeded('name');
  BehaviorSubject<String> get nameSub => _nameSub;

  // 構(gòu)造方法
  NameBlocProvider() {
    _handleSubscript();
  }

  // 增加操作
  void doAdd() {
    print('執(zhí)行了 name 增加操作');

    _nameSub.add(_name + '1');
  }

  // 處理訂閱
  void _handleSubscript() {
    // _nameSub.add(_name);
    _nameSub.listen((value) {
      _name = value;
    });
  }

  // 銷毀
  void dispose() {
    _nameSub.close();
  }
}
3.3.3 在APP頂層共享全局狀態(tài)
void main() {
  runApp(MultiProvider(
    providers: [
      Provider(create: (ctx) => CounterBlocProvider()),
      Provider(create: (ctx) => NameBlocProvider()),
    ],
    child: MyApp(),
  ));
}
3.3.4 創(chuàng)建count page 和 name page仁连,使用全局共享狀態(tài)

由于我們需要顯示的內(nèi)容是共享的兩個BLoC狀態(tài),所以對兩個Stream進行了合并操作阱穗,使用了RxDart中的CombineLatestStream饭冬,無論是count還是name發(fā)生了變化,在顯示的地方都會實時刷新揪阶。

如果是單純使用Stream昌抠,這個功能實現(xiàn)會比較麻煩,這也是Rx帶來的便利的體現(xiàn)鲁僚。

class ProviderPage extends StatefulWidget {
  static const String routeName = "/providerPage";

  const ProviderPage({Key key}) : super(key: key);

  @override
  _ProviderPageState createState() => _ProviderPageState();
}

class _ProviderPageState extends State<ProviderPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('counter provider page'),
          actions: <Widget>[
            IconButton(
                icon: Icon(Icons.people),
                onPressed: () {
                  Navigator.pushNamed(context, ProviderPage2.routeName);
                })
          ],
        ),
        body: Center(
          child: Consumer2<CounterBlocProvider, NameBlocProvider>(
              builder: (context, cntProvider, nameProvider, child) {
            return StreamBuilder(
                initialData: '初始化',
                stream: CombineLatestStream<dynamic, dynamic>(
                    [cntProvider.counterSub, nameProvider.nameSub], (values) {
                  print('合并的值是啥:${values.join(' + ')}');
                  return values.join(' + ');
                }),
                builder: (context, snapshot) {
                  return Chip(label: Text(snapshot.data));
                });
          }),
        ),
        floatingActionButton: Consumer2<CounterBlocProvider, NameBlocProvider>(
          builder: (context, cntProvider, nameProvider, child) {
            return FloatingActionButton(
                child: Icon(Icons.add),
                onPressed: () {
                  cntProvider.doAdd();
                });
          },
        ));
  }
}
class ProviderPage2 extends StatefulWidget {
  static const String routeName = "/providerPage2";

  const ProviderPage2({Key key}) : super(key: key);

  @override
  _ProviderPage2State createState() => _ProviderPage2State();
}

class _ProviderPage2State extends State<ProviderPage2> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('name provider page'),
          actions: <Widget>[
            IconButton(
                icon: Icon(Icons.people),
                onPressed: () {
                  Navigator.pushNamed(context, ProviderPage.routeName);
                })
          ],
        ),
        body: Center(
          child: Consumer2<CounterBlocProvider, NameBlocProvider>(
              builder: (context, cntProvider, nameProvider, child) {
            return StreamBuilder(
                initialData: '初始化',
                stream: CombineLatestStream<dynamic, dynamic>(
                    [cntProvider.counterSub, nameProvider.nameSub], (values) {
                  print('合并的值是啥:${values.join(' + ')}');
                  return values.join(' + ');
                }),
                builder: (context, snapshot) {
                  return Chip(label: Text(snapshot.data));
                });
          }),
        ),
        floatingActionButton: Consumer2<CounterBlocProvider, NameBlocProvider>(
          builder: (context, cntProvider, nameProvider, child) {
            return FloatingActionButton(
                child: Icon(Icons.add),
                onPressed: () {
                  nameProvider.doAdd();
                });
          },
        ));
  }
}

注意點:

  • 這里我們使用Consumer2炊苫,其實就是Consumer的升級,支持2個泛型冰沙,Consumer2侨艾、3...6,以此類推拓挥,同理
  • Selector與Consumer一樣唠梨,也有Selector2...6,就是為了支持多個數(shù)據(jù)共享

問題點:

  • 由于Selector相對Consumer侥啤,能減少子Widget的build方法調(diào)用次數(shù)当叭,所以能使用Selector當(dāng)然使用Selector,但是這個需求我嘗試了很多次愿棋,只能使用Consumer科展,如果有大佬用Selector能夠?qū)崿F(xiàn),望不吝賜教?酚辍2哦谩!

四、后記

沒有最完美的代碼琅攘,也沒有最完美的框架垮庐,只有適合自己的框架,以上內(nèi)容僅供參考~

上述代碼的DEMO坞琴,傳送門

參考文檔:

https://mp.weixin.qq.com/s/ywGQnaYpioPxlYvYTSpR4w
http://www.reibang.com/p/7573dee97dbb
http://www.reibang.com/p/a5d7758938ef
http://www.reibang.com/p/e0b0169a742e

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末哨查,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子剧辐,更是在濱河造成了極大的恐慌寒亥,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荧关,死亡現(xiàn)場離奇詭異溉奕,居然都是意外死亡,警方通過查閱死者的電腦和手機忍啤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門加勤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人同波,你說我怎么就攤上這事鳄梅。” “怎么了未檩?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵戴尸,是天一觀的道長。 經(jīng)常有香客問我讹挎,道長校赤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任筒溃,我火速辦了婚禮,結(jié)果婚禮上沾乘,老公的妹妹穿的比我還像新娘怜奖。我一直安慰自己,他們只是感情好翅阵,可當(dāng)我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布歪玲。 她就那樣靜靜地躺著,像睡著了一般掷匠。 火紅的嫁衣襯著肌膚如雪滥崩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天讹语,我揣著相機與錄音钙皮,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛短条,可吹牛的內(nèi)容都是我干的导匣。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼茸时,長吁一口氣:“原來是場噩夢啊……” “哼贡定!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起可都,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤缓待,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后渠牲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體命斧,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年嘱兼,在試婚紗的時候發(fā)現(xiàn)自己被綠了国葬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡芹壕,死狀恐怖汇四,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情踢涌,我是刑警寧澤通孽,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站睁壁,受9級特大地震影響背苦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜潘明,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一行剂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钳降,春花似錦厚宰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吓坚,卻和暖如春撵幽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背礁击。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工盐杂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留逗载,地道東北人。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓况褪,卻偏偏與公主長得像撕贞,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子测垛,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,914評論 2 355

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