Flutter實(shí)踐:深入探索 flutter 中的狀態(tài)管理方式(1)

利用 Flutter 內(nèi)置的許多控件我們可以打造出一款不僅漂亮而且完美跨平臺(tái)的 App 外殼偏序,我利用其特性完成了類似知乎App的UI界面筑煮,然而一款完整的應(yīng)用程序顯然不止有外殼這么簡單。填充在外殼里面的是數(shù)據(jù)盔腔,數(shù)據(jù)來源或從本地闸翅,或從云端贵扰,大量的數(shù)據(jù)處理很容易造成數(shù)據(jù)的混亂,耦合度提高奠衔,不便于維護(hù)谆刨,于是誕生了很多設(shè)計(jì)模式和狀態(tài)管理的方式塘娶。

目前 Flutter 常用狀態(tài)管理方式有如下幾種:

  • ScopedModel
  • BLoC (Business Logic Component) / Rx
  • Redux

這篇文章暫且不提這些比較復(fù)雜的模式。我們簡單的提出三個(gè)問題:

  • Flutter 中組件之間如何通信痊夭?
  • 更新 State 后組件以何種方式重新渲染刁岸?
  • 如何在路由轉(zhuǎn)換之間保持狀態(tài)同步?

初探 State

我以創(chuàng)建新項(xiàng)目 Flutter 給我們默認(rèn)的計(jì)數(shù)器應(yīng)用為例她我,通過路由我將其拆分為兩部分 MyHomePagePageTwo虹曙,

MyHomePage,持有一個(gè)_counter變量和一個(gè)增加計(jì)數(shù)的方法番舆;PageTwo酝碳,接收兩個(gè)參數(shù)(計(jì)數(shù)的至和增加計(jì)數(shù)的方法):

class PageTwo extends StatefulWidget {
  final int count;
  final Function increment;

  const PageTwo({Key key, this.count, this.increment}) : super(key: key);

  _PageTwoState createState() => _PageTwoState();
}

class _PageTwoState extends State<PageTwo> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Page Two"),
      ),
      body: Center(
        child: Text(widget.count.toString(), style: TextStyle(fontSize: 30.0),),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: widget.increment,
      ),
    );
  }
}

出現(xiàn)的狀況是:我們?cè)谑醉擖c(diǎn)擊按鈕觸發(fā)計(jì)數(shù)器增加,路由到 PageTwo 后合蔽,數(shù)值正常顯示击敌,然而點(diǎn)擊這個(gè)界面中的 add 按鈕該頁面的數(shù)值并未發(fā)生改變,通過觀察父頁面的 count 值確實(shí)發(fā)生了改變拴事,因此再次通過路由到第二個(gè)界面界面才顯示正常沃斤。解答上面三個(gè)問題:

  • Flutter 中組件之間如何通信?

    參數(shù)傳遞刃宵。

  • 更新 State 后組件以何種方式重新渲染衡瓶?

    只渲染當(dāng)前的組件(和子組件,這里暫未證明牲证,但確實(shí)是觸發(fā) SetSate() 后哮针,其所有子組件都將重新渲染。)

  • 如何在路由轉(zhuǎn)換之間保持狀態(tài)同步坦袍?

    父組件傳遞狀態(tài)值到子組件十厢,子組件拿到并顯示,但卻不能實(shí)時(shí)更改??捂齐,我一時(shí)半會(huì)還正沒想出什么解決方法蛮放,我相信即使能做到也不優(yōu)雅。

證明觸發(fā) SetSate() 后奠宜,其所有子組件都將重新渲染:我在父組件中添加兩個(gè)子組件包颁,一旦觸發(fā)渲染變打印相關(guān)數(shù)據(jù):

TestStateless(),
TestStateful()

class TestStateless extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('build TestStateless');
    return Text('TestStateless');
  }
}

class TestStateful extends StatefulWidget {
  @override
  _TestStatefulState createState() => _TestStatefulState();
}

class _TestStatefulState extends State<TestStateful> {
  @override
  Widget build(BuildContext context) {
    print('build TestStateful');
    return Text('_TestStatefulState');
  }
}

此時(shí)到 PageTwo 觸發(fā) add 事件,日志出來:

image

通過這種簡單的方式已經(jīng)可以說明一個(gè)問題压真,即以最簡單的方式我們已經(jīng)可以完成狀態(tài)傳遞和組件渲染娩嚼,而路由間保持狀態(tài)一致還不能解決。

image

InheritedWidget

Google 官方給我們的解決方案是 InheritedWidget滴肿,怎么理解他岳悟,我們可以稱它為“狀態(tài)樹”,它使得所有的 widget 的 State 來源統(tǒng)一泼差,這樣一旦有一處觸發(fā)狀態(tài)改變贵少,F(xiàn)lutter 以某種方式感應(yīng)到了(有個(gè)監(jiān)聽器)和屎,砍掉它,長出一個(gè)新樹春瞬,Perfect柴信!所有地方都能感受到他的變化。上面提到的第一種狀態(tài)管理方式 ScopedModel便是基于此而產(chǎn)生的一套第三方庫宽气。

其實(shí)現(xiàn)在看來 InheritedWidget 已經(jīng)非常簡單了随常,我們抓住兩個(gè)點(diǎn)即可完全掌握它:

  1. 狀態(tài)樹中的數(shù)據(jù)

    class MyInheritedValue extends InheritedWidget {
      const MyInheritedValue({
        Key key,
        @required this.value,
        @required Widget child,
      }) : assert(value != null),
           assert(child != null),
           super(key: key, child: child);
      final int value;
      static MyInheritedValue of(BuildContext context) {
        return context.inheritFromWidgetOfExactType(MyInheritedValue);
      }
      @override
      bool updateShouldNotify(MyInheritedValue old) => 
            value != old.value;
    }
    

    注入到根組件中:

    Widget build(BuildContext context) {
      return MyInheritedValue(
        value: 42,
        child: ...
      );
    }
    
  2. 使用狀態(tài)樹中數(shù)據(jù)的其他 Widget

    // 拿到狀態(tài)樹中的值
    MyInheritedValue.of(context).value
    

    請(qǐng)注意:這種情況下是不能改 InheritedWidget 中的值的,需要改也很簡單就是將 MyInheritedValue 的值封裝成一個(gè)對(duì)象萄涯,每次改變這個(gè)對(duì)象的值绪氛,具體法相看我的樣例代碼

上面所說砍掉整棵樹過于粗暴卻并不夸張涝影,因?yàn)橐惶幐淖兯鼘⒙?lián)動(dòng)整棵樹枣察,

ScopedModel 是基于 InheritedWidget 的庫,實(shí)現(xiàn)起來與 InheritedWidget 大同小異燃逻,而且其有一種可以讓局部組件不改變的方式:設(shè)置 rebuildOnChange 為 false序目。

return ScopedModelDescendant<CartModel>(
          rebuildOnChange: false,
          builder: (context, child, model) => ProductSquare(
                product: product,
                onTap: () => model.add(product),
              ),
        );

具體代碼請(qǐng)看 GitHub,ScopedModel 樣例截取一個(gè)老外給的實(shí)例伯襟,就是下方參考鏈接 Google 開發(fā)者大會(huì)上演講的那兩位其中之一猿涨。

image

這種方式顯然有點(diǎn)不足之處就是一旦遇到小規(guī)模變動(dòng)就要引起大規(guī)模重新渲染,所以當(dāng)項(xiàng)目達(dá)到一定的規(guī)哪饭郑考慮 Google 爸爸給我們的另一種解決方案叛赚。

Streams(流)

在 Android 開發(fā)中我們經(jīng)常會(huì)用到 RxJava 這類響應(yīng)式編程方法的框架,其強(qiáng)大之處無須多言稽揭,而 Stream 看上去就是在 Dart 語言中的響應(yīng)式編程的一種實(shí)現(xiàn)俺附。

  • Streams 是什么鬼?

    如果要具體把 Streams 說清楚溪掀,一篇文章絕對(duì)不夠事镣,這里先介紹一下其中的概念,這篇文章目的就是如此膨桥。待我后續(xù)想好怎么具體描述清楚蛮浑。

    你可以把它想象成一個(gè)管道唠叛,有入口(StreamSink)和出口()只嚣,我們將想要處理的數(shù)據(jù)從入口放入經(jīng)過該管道經(jīng)過一系列處理(經(jīng)由 StreamController)從出口中出來,而出口又有一個(gè)類似監(jiān)聽器之物艺沼,我們不知道它何時(shí)到來或者何時(shí)處理結(jié)束册舞。但是當(dāng)出口的監(jiān)聽器拿到東西便立即做出相應(yīng)的反應(yīng)。

  • 哪些東西可以放入管道障般?
    任何變量调鲸、對(duì)象盛杰、數(shù)組、甚至事件都可以被當(dāng)作數(shù)據(jù)源從入口放進(jìn)去藐石。

  • Streams 種類

    1. Single-subscription Stream即供,“單訂閱”流,這種類型的流只允許在該流的整個(gè)生命周期內(nèi)使用單個(gè)偵聽器于微。即使在第一個(gè)訂閱被取消后逗嫡,也無法在此類流上收聽兩次。
    2. Broadcast Streams株依,第二種類型的 Stream 允許任意數(shù)量的偵聽器驱证。可以隨時(shí)向廣播流添加偵聽器恋腕。 新的偵聽器將在它開始收聽 Stream 時(shí)收到事件抹锄。

例子:

第一個(gè)示例描述了“單訂閱”流,只打印輸入的數(shù)據(jù)荠藤。 你會(huì)發(fā)現(xiàn)是哪種數(shù)據(jù)類型無關(guān)緊要伙单。

import 'dart:async';

void main() {
  //
  // Initialize a "Single-Subscription" Stream controller
  //
  final StreamController ctrl = StreamController();
  
  //
  // Initialize a single listener which simply prints the data
  // as soon as it receives it
  //
  final StreamSubscription subscription = ctrl.stream.listen((data) => print('$data'));

  //
  // We here add the data that will flow inside the stream
  //
  ctrl.sink.add('my name');
  ctrl.sink.add(1234);
  ctrl.sink.add({'a': 'element A', 'b': 'element B'});
  ctrl.sink.add(123.45);
  
  //
  // We release the StreamController
  //
  ctrl.close();
}

第二個(gè)示例描述了“廣播”流,它傳達(dá)整數(shù)值并僅打印偶數(shù)哈肖。 我們用 StreamTransformer 來過濾(第14行)值车份,只讓偶數(shù)經(jīng)過。

import 'dart:async';

void main() {
  //
  // Initialize a "Broadcast" Stream controller of integers
  //
  final StreamController<int> ctrl = StreamController<int>.broadcast();
  
  //
  // Initialize a single listener which filters out the odd numbers and
  // only prints the even numbers
  //
  final StreamSubscription subscription = ctrl.stream
                          .where((value) => (value % 2 == 0))
                          .listen((value) => print('$value'));

  //
  // We here add the data that will flow inside the stream
  //
  for(int i=1; i<11; i++){
    ctrl.sink.add(i);
  }
  
  //
  // We release the StreamController
  //
  ctrl.close();
}

RxDart

RxDart包是 ReactiveX API 的 Dart 實(shí)現(xiàn)牡彻,它擴(kuò)展了原始的 Dart Streams API 以符合 ReactiveX 標(biāo)準(zhǔn)扫沼。

image

由于它最初并未由 Google 定義,因此它使用不同于 Dart 的變量庄吼。 下表給出了 Dart 和 RxDart 之間的關(guān)系缎除。

Dart RxDart
Stream Observable
StreamController Subject

RxDart 擴(kuò)展了原始的 Dart Streams API 并提供了 StreamController 的3個(gè)主要變體:

  1. PublishSubject

    PublishSubject 是一個(gè)普通的 broadcast StreamController ,有一點(diǎn)不同:stream 返回一個(gè) Observable 而不是一個(gè) Stream 总寻。

    image

    如您所見器罐,PublishSubject 僅向偵聽器發(fā)送在訂閱之后添加到 Stream 的事件。

  2. BehaviorSubject

    BehaviorSubject 也是一個(gè) broadcast StreamController渐行,它返回一個(gè) Observable 而不是一個(gè)Stream轰坊。

    image

    與 PublishSubject 的主要區(qū)別在于 BehaviorSubject 還將最后發(fā)送的事件發(fā)送給剛剛訂閱的偵聽器。

  3. ReplaySubject

    ReplaySubject 也是一個(gè)廣播 StreamController祟印,它返回一個(gè) Observable 而不是一個(gè) Stream肴沫。(蘿莉啰嗦)

    image

    默認(rèn)情況下,ReplaySubject 將Stream 已經(jīng)發(fā)出的所有事件作為第一個(gè)事件發(fā)送到任何新的偵聽器蕴忆。

BloC

BLoC 代表業(yè)務(wù)邏輯組件 (Business Logic Component)颤芬。一般的 Flutter 代碼業(yè)務(wù)邏輯和UI組件糅合在一起,不方便測(cè)試,不利于單獨(dú)的測(cè)試業(yè)務(wù)邏輯部分站蝠,不能更好的重用業(yè)務(wù)邏輯代碼汰具,體現(xiàn)在,如果網(wǎng)絡(luò)請(qǐng)求的邏輯有所變動(dòng)的話菱魔,加入這個(gè)業(yè)務(wù)功能被兩個(gè)端(web留荔、flutter)使用的話,是需要改動(dòng)兩個(gè)地方的澜倦。

簡而言之存谎,業(yè)務(wù)邏輯需要:

  • 被移植到一個(gè)或幾個(gè) BLoC 中,
  • 盡可能從表示層中刪除肥隆。 也就是說既荚,UI組件應(yīng)該只關(guān)心UI事物而不關(guān)心業(yè)務(wù),
  • 依賴 Streams 使用輸入(Sink)和輸出(stream)栋艳,
  • 保持平臺(tái)獨(dú)立恰聘,
  • 保持環(huán)境獨(dú)立。

事實(shí)上吸占,BLoC 模式最初的設(shè)想是實(shí)現(xiàn)允許獨(dú)立于平臺(tái)重用相同的代碼:Web應(yīng)用程序晴叨,移動(dòng)應(yīng)用程序,后端矾屯。

Bloc 的大概就是 Stream 在 Flutter 中的最佳實(shí)踐:

image
  • 組件通過 Sinks 向 BLoC 發(fā)送事件兼蕊,
  • BLoC 通過 stream 通知組件,
  • 由 BLoC 實(shí)現(xiàn)的業(yè)務(wù)邏輯件蚕。

將 BloC 應(yīng)用在計(jì)數(shù)器應(yīng)用中:

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
        title: 'Streams Demo',
        theme: new ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: BlocProvider<IncrementBloc>(
          bloc: IncrementBloc(),
          child: CounterPage(),
        ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);

    return Scaffold(
      appBar: AppBar(title: Text('Stream version of the Counter App')),
      body: Center(
        child: StreamBuilder<int>(
          stream: bloc.outCounter,
          initialData: 0,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot){
            return Text('You hit me: ${snapshot.data} times');
          }
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: (){
          bloc.incrementCounter.add(null);
        },
      ),
    );
  }
}

class IncrementBloc implements BlocBase {
  int _counter;

  //
  // Stream to handle the counter
  //
  StreamController<int> _counterController = StreamController<int>();
  StreamSink<int> get _inAdd => _counterController.sink;
  Stream<int> get outCounter => _counterController.stream;

  //
  // Stream to handle the action on the counter
  //
  StreamController _actionController = StreamController();
  StreamSink get incrementCounter => _actionController.sink;

  //
  // Constructor
  //
  IncrementBloc(){
    _counter = 0;
    _actionController.stream
                     .listen(_handleLogic);
  }

  void dispose(){
    _actionController.close();
    _counterController.close();
  }

  void _handleLogic(data){
    _counter = _counter + 1;
    _inAdd.add(_counter);
  }
}

你一定在說孙技,臥槽,哇靠~~什么吊玩意排作,那么就留著懸念吧牵啦,今天寫不動(dòng)了!

Bolc 的具體實(shí)現(xiàn)我在樣例代碼里分兩步走放在兩個(gè)文件夾里妄痪!如果需要可以先去看看嘗嘗鮮哈雏。

這篇文章的目的就是介紹一些概念給大家關(guān)于 Streams、RXDart 及 Bloc 詳細(xì)明了的解釋后續(xù)更新衫生!

樣例代碼

https://github.com/MeandNi/Flutter_StatePro

參考鏈接

Build reactive mobile apps with Flutter (Google I/O '18)

Reactive Programming - Streams - BLoC

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末裳瘪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子罪针,更是在濱河造成了極大的恐慌彭羹,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件站故,死亡現(xiàn)場(chǎng)離奇詭異皆怕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)西篓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門愈腾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人岂津,你說我怎么就攤上這事虱黄。” “怎么了吮成?”我有些...
    開封第一講書人閱讀 157,435評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵橱乱,是天一觀的道長。 經(jīng)常有香客問我粱甫,道長泳叠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,509評(píng)論 1 284
  • 正文 為了忘掉前任茶宵,我火速辦了婚禮危纫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘乌庶。我一直安慰自己种蝶,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,611評(píng)論 6 386
  • 文/花漫 我一把揭開白布瞒大。 她就那樣靜靜地躺著螃征,像睡著了一般。 火紅的嫁衣襯著肌膚如雪透敌。 梳的紋絲不亂的頭發(fā)上盯滚,一...
    開封第一講書人閱讀 49,837評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音酗电,去河邊找鬼淌山。 笑死,一個(gè)胖子當(dāng)著我的面吹牛顾瞻,可吹牛的內(nèi)容都是我干的泼疑。 我是一名探鬼主播,決...
    沈念sama閱讀 38,987評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼荷荤,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼退渗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蕴纳,我...
    開封第一講書人閱讀 37,730評(píng)論 0 267
  • 序言:老撾萬榮一對(duì)情侶失蹤会油,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后古毛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翻翩,經(jīng)...
    沈念sama閱讀 44,194評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡都许,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,525評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嫂冻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胶征。...
    茶點(diǎn)故事閱讀 38,664評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖桨仿,靈堂內(nèi)的尸體忽然破棺而出睛低,到底是詐尸還是另有隱情,我是刑警寧澤服傍,帶...
    沈念sama閱讀 34,334評(píng)論 4 330
  • 正文 年R本政府宣布钱雷,位于F島的核電站,受9級(jí)特大地震影響吹零,放射性物質(zhì)發(fā)生泄漏罩抗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,944評(píng)論 3 313
  • 文/蒙蒙 一灿椅、第九天 我趴在偏房一處隱蔽的房頂上張望澄暮。 院中可真熱鬧,春花似錦阱扬、人聲如沸泣懊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽馍刮。三九已至,卻和暖如春窃蹋,著一層夾襖步出監(jiān)牢的瞬間卡啰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評(píng)論 1 266
  • 我被黑心中介騙來泰國打工警没, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留匈辱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,389評(píng)論 2 360
  • 正文 我出身青樓杀迹,卻偏偏與公主長得像亡脸,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子树酪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,554評(píng)論 2 349

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