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

Meandni發表於2019-01-31

利用 Flutter 內建的許多控制元件我們可以打造出一款不僅漂亮而且完美跨平臺的 App 外殼,我利用其特性完成了類似知乎App的UI介面,然而一款完整的應用程式顯然不止有外殼這麼簡單。填充在外殼裡面的是資料,資料來源或從本地,或從雲端,大量的資料處理很容易造成資料的混亂,耦合度提高,不便於維護,於是誕生了很多設計模式和狀態管理的方式。

目前 Flutter 常用狀態管理方式有如下幾種:

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

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

  • Flutter 中元件之間如何通訊?
  • 更新 State 後元件以何種方式重新渲染?
  • 如何在路由轉換之間保持狀態同步?

初探 State

我以建立新專案 Flutter 給我們預設的計數器應用為例,通過路由我將其拆分為兩部分 MyHomePagePageTwo

MyHomePage,持有一個_counter變數和一個增加計數的方法;PageTwo,接收兩個引數(計數的至和增加計數的方法):

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

複製程式碼

出現的狀況是:我們在首頁點選按鈕觸發計數器增加,路由到 PageTwo 後,數值正常顯示,然而點選這個介面中的 add 按鈕該頁面的數值並未發生改變,通過觀察父頁面的 count 值確實發生了改變,因此再次通過路由到第二個介面介面才顯示正常。解答上面三個問題:

  • Flutter 中元件之間如何通訊?

    引數傳遞。

  • 更新 State 後元件以何種方式重新渲染?

    只渲染當前的元件(和子元件,這裡暫未證明,但確實是觸發 SetSate() 後,其所有子元件都將重新渲染。)

  • 如何在路由轉換之間保持狀態同步?

    父元件傳遞狀態值到子元件,子元件拿到並顯示,但卻不能實時更改?,我一時半會還正沒想出什麼解決方法,我相信即使能做到也不優雅。

證明觸發 SetSate() 後,其所有子元件都將重新渲染:我在父元件中新增兩個子元件,一旦觸發渲染變列印相關資料:

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 觸發 add 事件,日誌出來:

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

通過這種簡單的方式已經可以說明一個問題,即以最簡單的方式我們已經可以完成狀態傳遞和元件渲染,而路由間保持狀態一致還不能解決。

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

InheritedWidget

Google 官方給我們的解決方案是 InheritedWidget,怎麼理解他,我們可以稱它為“狀態樹”,它使得所有的 widget 的 State 來源統一,這樣一旦有一處觸發狀態改變,Flutter 以某種方式感應到了(有個監聽器),砍掉它,長出一個新樹,Perfect!所有地方都能感受到他的變化。上面提到的第一種狀態管理方式 ScopedModel便是基於此而產生的一套第三方庫。

其實現在看來 InheritedWidget 已經非常簡單了,我們抓住兩個點即可完全掌握它:

  1. 狀態樹中的資料

    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. 使用狀態樹中資料的其他 Widget

    // 拿到狀態樹中的值
    MyInheritedValue.of(context).value
    複製程式碼

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

上面所說砍掉整棵樹過於粗暴卻並不誇張,因為一處改變它將聯動整棵樹,

ScopedModel 是基於 InheritedWidget 的庫,實現起來與 InheritedWidget 大同小異,而且其有一種可以讓區域性元件不改變的方式:設定 rebuildOnChange 為 false。

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

具體程式碼請看 GitHub,ScopedModel 樣例擷取一個老外給的例項,就是下方參考連結 Google 開發者大會上演講的那兩位其中之一。

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

這種方式顯然有點不足之處就是一旦遇到小規模變動就要引起大規模重新渲染,所以當專案達到一定的規模考慮 Google 爸爸給我們的另一種解決方案。

Streams(流)

在 Android 開發中我們經常會用到 RxJava 這類響應式程式設計方法的框架,其強大之處無須多言,而 Stream 看上去就是在 Dart 語言中的響應式程式設計的一種實現。

  • Streams 是什麼鬼?

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

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

  • 哪些東西可以放入管道? 任何變數、物件、陣列、甚至事件都可以被當作資料來源從入口放進去。

  • Streams 種類

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

例子:

第一個示例描述了“單訂閱”流,只列印輸入的資料。 你會發現是哪種資料型別無關緊要。

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();
}
複製程式碼

第二個示例描述了“廣播”流,它傳達整數值並僅列印偶數。 我們用 StreamTransformer 來過濾(第14行)值,只讓偶數經過。

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 實現,它擴充套件了原始的 Dart Streams API 以符合 ReactiveX 標準。

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

由於它最初並未由 Google 定義,因此它使用不同於 Dart 的變數。 下表給出了 Dart 和 RxDart 之間的關係。

Dart RxDart
Stream Observable
StreamController Subject

RxDart 擴充套件了原始的 Dart Streams API 並提供了 StreamController 的3個主要變體:

  1. PublishSubject

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

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

    如您所見,PublishSubject 僅向偵聽器傳送在訂閱之後新增到 Stream 的事件。

  2. BehaviorSubject

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

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

    與 PublishSubject 的主要區別在於 BehaviorSubject 還將最後傳送的事件傳送給剛剛訂閱的偵聽器。

  3. ReplaySubject

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

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

    預設情況下,ReplaySubject 將Stream 已經發出的所有事件作為第一個事件傳送到任何新的偵聽器。

BloC

BLoC 代表業務邏輯元件 (Business Logic Component)。一般的 Flutter 程式碼業務邏輯和UI元件糅合在一起,不方便測試,不利於單獨的測試業務邏輯部分,不能更好的重用業務邏輯程式碼,體現在,如果網路請求的邏輯有所變動的話,加入這個業務功能被兩個端(web、flutter)使用的話,是需要改動兩個地方的。

簡而言之,業務邏輯需要:

  • 被移植到一個或幾個 BLoC 中,
  • 儘可能從表示層中刪除。 也就是說,UI元件應該只關心UI事物而不關心業務,
  • 依賴 Streams 使用輸入(Sink)和輸出(stream),
  • 保持平臺獨立,
  • 保持環境獨立。

事實上,BLoC 模式最初的設想是實現允許獨立於平臺重用相同的程式碼:Web應用程式,移動應用程式,後端。

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

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

  • 元件通過 Sinks 向 BLoC 傳送事件,
  • BLoC 通過 stream 通知元件,
  • 由 BLoC 實現的業務邏輯。

將 BloC 應用在計數器應用中:

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 的具體實現我在樣例程式碼裡分兩步走放在兩個資料夾裡!如果需要可以先去看看嚐嚐鮮。

這篇文章的目的就是介紹一些概念給大家關於 Streams、RXDart 及 Bloc 詳細明瞭的解釋後續更新!

樣例程式碼

github.com/MeandNi/Flu…

參考連結

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

Reactive Programming - Streams - BLoC

相關文章