上一篇30天Flutter手滑系列 - 無狀態與有狀態Widgets (Stateless & Stateful widgets),算是基礎的狀態管理教學,今天開始才是正式進入進階的部分。
在開發初期,只需將狀態直接反映在View上即可:
但隨著應用程式的功能日益增多,很可能變成這樣子:
一團混亂了!
這個問題不管在Web或是Mobile的開發,往往是個需要被克服的問題,像是React就衍伸出利用Redux這個第三方套件來有效管理組件間的資料變化,在Flutter中也借鏡了React一些觀念。在這個章節,會介紹Flutter內建的狀態管理方法,以及部分熱門狀態管理套件。
如一章節提到的,內建的setState是其中一種狀態管理辦法。在flutter create app的預設程式碼中,就展示了setState的用法。
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), 
    );
  }
}
如果需要跨組件傳遞,需要把傳遞的任務放到parent的widget上。由於每次setState需要rebuild widgets,如果是隔很多層的傳遞,需要被rebuild的widgets會越多,相對效能會比較差,僅適合小規模的更新。

單純setState實在太過笨重沒效率,因此官方提出了InheritedWidget這個Widget。
其核心理念是在父層級的Widget加入一層InheritedWidget,讓在其底下的Widgets可以參考到這個InheritedWidget底下的節點。
來看一下以下的官方範例:
(1) 首先建立一個InheritedWidget
(2) 建立Data資料
(3) 加入of方法
(4) 加入updateShouldNotify方法:告訴flutter有資料更新時需要重新繪製
// (1)建立一個subclass InheritedWiget
class FrogColor extends InheritedWidget {  
  // (2)建立Data
  const FrogColor({   
    Key key,
    @required this.color,
    @required Widget child,
  }) : assert(color != null),
       assert(child != null),
       super(key: key, child: child);
  final Color color;  
  // (3)加入of方法
  static FrogColor of(BuildContext context) {  
    return context.inheritFromWidgetOfExactType(FrogColor) as FrogColor;
  }
  @override
  // (4)加入updateShouldNotify方法 
  // (5)並控制當接收到更新
  bool updateShouldNotify(FrogColor old) => color != old.color;  
}
InheritedWidget改善了撰寫的麻煩,不過同樣有些缺點:
- 無法將View與Logic部分分開。
- 無法定向通知。
- 每次更新都會通知所有Widgets,無法區分哪些Widgets需要被更新。解決辦法可以透過
StreamBuilder來監聽InheritedWidget中的stream的資料變化,然後判斷是否更新當前widget。
Scoped Model主要透過Model的概念實作資料的傳遞,類似於React中的context概念。本身提供的方法,可以讓子Widget存取父Widget的model功能。
簡單的三步驟可以創建一個scoped_model:
Model Class,然後自定義你的Model,像是CountModel或SearchModel,並在狀態改變時執行notifyListeners()。ScopedModel去包裝你的Model,這樣可以使這Model被所有Widgets使用。ScopedModelDescendant去找到Widget tree中目標的ScopedModel,使它可以根據狀態更新自動去rebuild widget。class CounterModel extends Model {
  int _counter = 0;
  int get counter => _counter;
  void increment() {
    _counter++;
    
    // 狀態改變通知所有listeners
    notifyListeners();
  }
}
// 創建一個CounterModel
class CounterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 首先創建一個ScopedModel讓model可以被所有子widgets存取
    return new ScopedModel<CounterModel>(
      model: new CounterModel(),
      child: new Column(children: [
        // 加入ScopedModelDescendant,
        // 它會從最近的ScopedModel<CounterModel>找到CounterModel。
        // 然後接收到CounterModel改變時去進行rebuild
        new ScopedModelDescendant<CounterModel>(
          builder: (context, child, model) => new Text('${model.counter}'),
        ),
        new Text("Another widget that doesn't depend on the CounterModel")
      ])
    );
  }
}
ScopedModel的優缺點跟InheritedWidget基本上一樣,因為他只是封裝自InheritedWidget而已。
Bloc是Business Logic Component的縮寫,是一種設計模式。其核心思想是讓UI與數據分離,以數據驅動去渲染UI,概念跟Redux一樣。

Bloc同時是一種以Reactive Programming去開發的方法,一切都是stream的概念。只需用StreamBuilder和Bloc,就可以讓業務邏輯被獨立出來,不用考慮何時需要去更新View的部分。
透過stream可以傳遞事件、值、對象或集合等。
當需要知道stream的某些內容時,只需要訂閱StreamController的stream對象,透過被訂閱的StreamSubscription對象,就可以知道stream發生變化進而觸發通知。

stream可以想像成是一個水管,水通過水管,一路向下流。如果有學過RxJS的人應該不陌生。
在實作上可以直接使用flutter_bloc,而不需要自行定義stream的操作。
stream有兩種類型:
import 'dart:async';
void main() {
  // 初始化一個Single-Subscription StreamController
  final StreamController ctrl = StreamController();
  
  // 定義一個監聽器,目的是在收到資料時候直接印出來
  final StreamSubscription subscription = ctrl.stream.listen((data) => print('$data'));
  / 新增流入stream的資料
  ctrl.sink.add('my name');
  ctrl.sink.add(1234);
  ctrl.sink.add({'a': 'element A', 'b': 'element B'});
  ctrl.sink.add(123.45);
  
  // 結束釋放StreamController
  ctrl.close();
}
import 'dart:async';
void main() {
  // 初始化一個int型態的Broadcast StreamController
  final StreamController<int> ctrl = StreamController<int>.broadcast();
  // 定義一個監聽器,目的是過濾掉奇數,只印出偶數。
  final StreamSubscription subscription = ctrl.stream
					      .where((value) => (value % 2 == 0))
					      .listen((value) => print('$value'));
  // 新增流入stream的資料
  for(int i=1; i<11; i++){
  	ctrl.sink.add(i);
  }
  
  // 結束釋放StreamController
  ctrl.close();
}
哪一個套件好用,我覺得見仁見智,挑適合專案或團隊的就好,或者說能解決問題的都是好方法。
https://flutter.dev/docs/development/data-and-backend/state-mgmt
https://hicc.me/flutter-state-management/
https://pub.flutter-io.cn/packages/scoped_model#-readme-tab-
https://juejin.im/post/5cd91bb0f265da034e7eaca3
https://juejin.im/post/5bb6f344f265da0aa664d68a
https://juejin.im/post/5b97fa0d5188255c5546dcf8
https://juejin.im/post/5c3ef2695188252547423234
https://wizardforcel.gitbooks.io/gsyflutterbook/content/Flutter-12.html
https://www.didierboelens.com/2018/08/reactive-programming---streams---bloc/