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

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

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

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

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

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

初探 State

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

MyHomePage,持有一個_counter變量和一個增加計數(shù)的方法;PageTwo,接收兩個參數(shù)(計數(shù)的至和增加計數(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)的狀況是:我們在首頁點擊按鈕觸發(fā)計數(shù)器增加,路由到 PageTwo 后,數(shù)值正常顯示,然而點擊這個界面中的 add 按鈕該頁面的數(shù)值并未發(fā)生改變,通過觀察父頁面的 count 值確實發(fā)生了改變,因此再次通過路由到第二個界面界面才顯示正常。解答上面三個問題:

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

    參數(shù)傳遞。

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

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

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

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

證明觸發(fā) SetSate() 后,其所有子組件都將重新渲染:我在父組件中添加兩個子組件,一旦觸發(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');
  }
}

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

image

通過這種簡單的方式已經(jīng)可以說明一個問題,即以最簡單的方式我們已經(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)到了(有個監(jiān)聽器),砍掉它,長出一個新樹,Perfect!所有地方都能感受到他的變化。上面提到的第一種狀態(tài)管理方式 ScopedModel便是基于此而產(chǎn)生的一套第三方庫。

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

  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
    

    請注意:這種情況下是不能改 InheritedWidget 中的值的,需要改也很簡單就是將 MyInheritedValue 的值封裝成一個對象,每次改變這個對象的值,具體法相看我的樣例代碼

上面所說砍掉整棵樹過于粗暴卻并不夸張,因為一處改變它將聯(lián)動整棵樹,

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

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

具體代碼請看 GitHub,ScopedModel 樣例截取一個老外給的實例,就是下方參考鏈接 Google 開發(fā)者大會上演講的那兩位其中之一。

image

這種方式顯然有點不足之處就是一旦遇到小規(guī)模變動就要引起大規(guī)模重新渲染,所以當(dāng)項目達(dá)到一定的規(guī)??紤] Google 爸爸給我們的另一種解決方案。

Streams(流)

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

  • Streams 是什么鬼?

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

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

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

  • Streams 種類

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

例子:

第一個示例描述了“單訂閱”流,只打印輸入的數(shù)據(jù)。 你會發(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();
}

第二個示例描述了“廣播”流,它傳達(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 實現(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個主要變體:

  1. PublishSubject

    PublishSubject 是一個普通的 broadcast StreamController ,有一點不同:stream 返回一個 Observable 而不是一個 Stream 。

    image

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

  2. BehaviorSubject

    BehaviorSubject 也是一個 broadcast StreamController,它返回一個 Observable 而不是一個Stream。

    image

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

  3. ReplaySubject

    ReplaySubject 也是一個廣播 StreamController,它返回一個 Observable 而不是一個 Stream。(蘿莉啰嗦)

    image

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

BloC

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

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

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

事實上,BLoC 模式最初的設(shè)想是實現(xiàn)允許獨立于平臺重用相同的代碼:Web應(yīng)用程序,移動應(yīng)用程序,后端。

Bloc 的大概就是 Stream 在 Flutter 中的最佳實踐:

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

將 BloC 應(yīng)用在計數(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);
  }
}

你一定在說,臥槽,哇靠~~什么吊玩意,那么就留著懸念吧,今天寫不動了!

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

這篇文章的目的就是介紹一些概念給大家關(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)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容