iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 11
1
Mobile Development

30天手滑用Google Flutter解鎖Hybrid App成就系列 第 11

30天Flutter手滑系列 - 狀態管理 (State Management)

上一篇30天Flutter手滑系列 - 無狀態與有狀態Widgets (Stateless & Stateful widgets),算是基礎的狀態管理教學,今天開始才是正式進入進階的部分。

為什麼需要狀態管理

在開發初期,只需將狀態直接反映在View上即可:
https://ithelp.ithome.com.tw/upload/images/20190918/20120028Kr5haXIb62.png

但隨著應用程式的功能日益增多,很可能變成這樣子:
https://ithelp.ithome.com.tw/upload/images/20190918/201200287Rsfkg9pCI.png

一團混亂了!
這個問題不管在Web或是Mobile的開發,往往是個需要被克服的問題,像是React就衍伸出利用Redux這個第三方套件來有效管理組件間的資料變化,在Flutter中也借鏡了React一些觀念。在這個章節,會介紹Flutter內建的狀態管理方法,以及部分熱門狀態管理套件。


1. setState

如一章節提到的,內建的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會越多,相對效能會比較差,僅適合小規模的更新。

https://ithelp.ithome.com.tw/upload/images/20190918/20120028D5NdrYrsd1.jpg


2. InheritedWidget

單純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。

3. scoped_model

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而已。


4. Bloc

Bloc是Business Logic Component的縮寫,是一種設計模式。其核心思想是讓UI與數據分離,以數據驅動去渲染UI,概念跟Redux一樣。

https://ithelp.ithome.com.tw/upload/images/20190918/20120028hJwK0RXGFO.png

Bloc同時是一種以Reactive Programming去開發的方法,一切都是stream的概念。只需用StreamBuilderBloc,就可以讓業務邏輯被獨立出來,不用考慮何時需要去更新View的部分。

透過stream可以傳遞事件、值、對象或集合等。
當需要知道stream的某些內容時,只需要訂閱StreamController的stream對象,透過被訂閱的StreamSubscription對象,就可以知道stream發生變化進而觸發通知。

https://ithelp.ithome.com.tw/upload/images/20190919/20120028CAZbqoEF1S.png

stream可以想像成是一個水管,水通過水管,一路向下流。如果有學過RxJS的人應該不陌生。
在實作上可以直接使用flutter_bloc,而不需要自行定義stream的操作。

stream有兩種類型:

  • 單一訂閱流(Single-Subscription):只允許在該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();
}
  • 廣播流(Broadcast):可以隨時新增任何型態的監聽,
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/


上一篇
30天Flutter手滑系列 - 無狀態與有狀態Widgets (Stateless & Stateful widgets)
下一篇
30天Flutter手滑系列 - JSON與序列化(JSON and serialization)
系列文
30天手滑用Google Flutter解鎖Hybrid App成就30

尚未有邦友留言

立即登入留言