tags: Flutter、Bloc Pattern、Bloc、Stream
[Note] BLoC Pattern
前言
BLoC(Business Logic Component)Pattern 是一種用於 Flutter 開發的設計模式,它將應用程序分成三個主要部分:界面、資料層和業務邏輯,並利用 Streams 管理資料流,以實現組件之間的解耦和資料共享。
用原生 widgets 就可以實作,也有基於 BLoC Pattern 而開發的套件 - flutter_bloc,將方法封裝更容易維護和開發。
首先要先了解 Stream 概念,因為 BLoC Pattern 核心是 Streams。
Stream
Flutter 用來管理非同步的處理事件序列的概念,可想像 Stream 是一條河流或是通道,Sender 經由 Stream 傳遞任何東西給 Receiver,Receiver 不會知道 Sender 送的東西什麼時候會到和送什麼東西,只有能被動等東西到達。
Shipped Data:可以是任何形式 data/eventSender:傳送資料(傳遞的源頭)Receiver:接收資料(傳遞的終點),無法預期何時接收到資料- 非同步行為,因為傳遞會需要消耗時間
 - 有順序性,先進先出
 

用 Stream 專有名詞表示:
- 這條河流 ➡️ 
StreamController Sender➡️SinkStreamController的入口- 使用 
add方法,將要傳送東西傳送出去 
Receiver➡️Stream,StreamController的出口- 使用 
listen方法,監聽是否接收到東西 

BLoC Pattern 優點
- 解耦:界面、業務邏輯和資料層分開,使它們可以獨立開發、測試和維護
 - 可測試性:業務邏輯分離到單獨的組件中,可以方便地進行 Unit Test
 - code 共用性:將業務邏輯分離到單獨的組件中,可以在多個 View 中重用
 - 易維護:有良好的結構和清晰的職責分工,易於維護和擴展功能
 
BLoC pattern 實作
範例原始碼:Repo 網址
目標

- 實作簡單的 Counter
 - 支援 
加一、減一、Reset,這三個功能 - 由兩個 
StreamController組成 (State StreamController&Event StreamController)。 
首先專注做 State StreamController 和加一 功能。


Step1. 新增 Bloc 檔案,建立 State StreamController
- 定義 
StreamController,因為預期 in/out 皆為數字, Type 為int _XXXStreamController.sink可取得 Input proterty_XXXStreamController.stream可取得 Output proterty
// counter_bloc.dart
class CounterBloc {
  // pipe
  final _stateStreamController = StreamController<int>();
  // input
  StreamSink<int> get counterSink => _stateStreamController.sink;
  // output
  Stream<int> get counterStream => _stateStreamController.stream;
}
Step2. 按鈕點擊時,利用 counterSink 的 add 方法,傳遞 _counter 值
counterBloc.counterSink.add(_counter);
此時點擊 “+”,數字不會增加,即使 onPressed 會觸發 _counter++,讓
_counter值增加,但因為沒有使用如setState等方法,去強迫 rebuild wiget,所以 Text 的內容永遠都會初次 mounted 的值,即為 0。
// main.dart
class Counter extends StatefulWidget { ... }
class _CounterState extends State<Counter> {
  int _counter = 0;
  // 1. 宣告 counterBloc
  final counterBloc = CounterBloc();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...,
      body: Center(
        child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
          Text('$_counter'),
          const SizedBox(
            height: 16,
          ),
          IconButton(
            onPressed: () {
              _counter++;
              // 2. add state
              counterBloc.counterSink.add(_counter);
            },
            icon: const Icon(Icons.add_circle),
            iconSize: 36,
          )
        ]),
      ),
    );
  }
}
Step3. 使用 StreamBuilder,監聽 _counter 值
StreamBuilder 中的參數:
stream:傳入(欲 listen) target stream,只要監聽的資料改變,會 rebuild widgetbuilder:回傳 widget
當在點擊 “+” 按鈕時,顯示的數字已經會如預期往上加了
// main.dart
class Counter extends StatefulWidget { ... }
class _CounterState extends State<Counter> {
  int _counter = 0;
  final counterBloc = CounterBloc();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...,
      body: Center(
        child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
          // 1. wrap with StreamBuilder
          StreamBuilder(
            // 2. use builder and return widget
            stream:
                counterBloc.counterStream,
            builder: (context, snapshot) => Text(
              '$_counter',
              style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
            ),
          ),
          const SizedBox(
            height: 16,
          ),
          IconButton(
            onPressed: () {
              _counter++;
              counterBloc.counterSink.add(_counter);
            },
            icon: const Icon(Icons.add_circle),
            iconSize: 36,
          )
        ]),
      ),
    );
  }
}
接下來是建立 Event StreamController,並實作 減一、Reset 倆功能。
大致流程如下:
- 點擊 Button 會傳送 
CounterAction(@ widget) eventStream監聽CounterAction(@ CounterBloc)counterSink傳遞counter值 (@ CounterBloc)- widget 使用 
snapshot.data,取得counter值 (@ widget) 
在Event StreamController stream 接收到的資料會直接由 State StreamController sink 傳入,這段是在 CounterBloc 內實作,外面使用的 widget 都不需要知道實作細節。


Step4. CounterBloc 內建立 Event StreamController
- 新增 action 的 enum
action 和 event 這邊概念是共通的,也可取名叫 CounterAction
 - 宣告 counter 變數
 eventStream使用 listen 方法,監聽CounterActioncounterSink使用 add 方法,傳遞運算過後counter值
// counter_bloc.dart
// 定義出 counter actions
enum CounterAction { INCREMENT, DECREMENT, RESET }
class CounterBloc {
  late int counter;
  final _stateStreamController = StreamController<int>();
  StreamSink<int> get counterSink => _stateStreamController.sink;
  Stream<int> get counterStream => _stateStreamController.stream;
  final _eventStreamController = StreamController<CounterAction>();
  StreamSink<CounterAction> get eventSink => _eventStreamController.sink;
  Stream<CounterAction> get eventStream => _eventStreamController.stream;
  CounterBloc() {
    counter = 0;
    // listen change in the stream (CounterAction)
    eventStream.listen((event) {
      if (event == CounterAction.INCREMENT) {
        counter++;
      } else if (event == CounterAction.DECREMENT) {
        counter--;
      } else if (event == CounterAction.RESET) {
        counter = 0;
      }
      // 傳遞運算過後的 counter 值
      counterSink.add(counter);
    });
  }
}
Step5. 按鈕點擊時,利用 eventSink 的 add 方法,傳遞 CounterAction
- 移除 widget 內的 
_counter,新增initialDatainitialData:初始值,不設定的話,一開始會為 null - 從 
StreamBuilder的snapshot.data取得counter值 - onPress 觸發的函式,改成由 
eventSink傳遞CounterAction 
// main.dart
class Counter extends StatefulWidget {...}
class _CounterState extends State<Counter> {
  final counterBloc = CounterBloc();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...,
      body: Center(
        child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
          StreamBuilder(
            stream: counterBloc.counterStream,
            // 不給 init data,一開始會拿到 null
            initialData: 0,
            // get value via snapshot.data
            builder: (context, snapshot) => Text(
              '${snapshot.data}',
              style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
            ),
          ),
          const SizedBox(
            height: 16,
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              IconButton(
                onPressed: () {
                  // INCREMENT 事件
                  counterBloc.eventSink.add(CounterAction.INCREMENT);
                },
                icon: const Icon(Icons.add_circle),
                iconSize: 36,
              ),
              IconButton(
                onPressed: () {
                  // DECREMENT 事件
                  counterBloc.eventSink.add(CounterAction.DECREMENT);
                },
                icon: const Icon(Icons.remove_circle),
                iconSize: 36,
              ),
              IconButton(
                onPressed: () {
                 // RESET 事件
                  counterBloc.eventSink.add(CounterAction.RESET);
                },
                icon: const Icon(Icons.loop_outlined),
                iconSize: 36,
              ),
            ],
          )
         ]
        ),
      ),
    );
  }
}