有經驗的前端工程師或多或少應該都有聽過 MVC、MVP、MVVM 架構的開發方式,這些開發方式可以讓我們達到觀注點分離(Separation of concerns,SoC)的設計原則讓開發團隊可以遵詢同一種模式進行開發工作。
今天我們就來看看目前在 Flutter 設計上經常聽到 Bloc 是什麼吧。
Bloc (Business Logic Component) 的設計理念會希望透過該設計原則將 View 的代碼與業務邏輯拆開, 易於程式碼的維護與開發、測試。
a predictable state management library for Dart.
Simple & Lightweight
Highly Testable
For Dart, Flutter, and AngularDart
從官網的架構圖上來看,從角色可以區分為三種類別

該圖片引用來自官網架構文件說明
負責處理資料來源的管理,通常會從 DB 或是 API 取得資料。
接收 UI 傳遞過來的事件(events)觸發業務邏輯的處理,可能會需要從 Data 取得相關資料,視邏輯有機會觸發狀態(states)的轉換。
負責處理畫面的呈現,畫面照業務邏輯的 states 而有不同狀態的顯示方式。
在開發前需要定義應用上可能的狀態以及會需要處理的事件為何!!!
先前的聊天室範例我們是使用 StatefulWidget 搭配 ViewModel 的寫法,接下來我們試著用 bloc 改寫看看。
與 bloc 相關的套件如下
dependencies:
bloc: ^7.2.0
flutter_bloc: ^7.3.0
equatable: ^2.0.3
負責處理聊天室WebSocket建立工作,新增一個類別繼承Bloc並定義對應的Event與State。
class ChatBloc extends Bloc<ChatEvent, ChatState> {
final Connection _connection;
ChatBloc(this._connection) : super(const ChatState()) {}
}
Connection 是先前範例中我們包裝用來建立 WebSocket 的類別,在 bloc 初始化時從外部注入。
在ChatState類別中我們定義兩個屬性
status - 記錄目前連線狀態data - 記錄聊天室訊息記錄enum SocketStatus { initial, open, closed }
class ChatState extends Equatable {
final SocketStatus status;
final List<Message> data;
const ChatState({
this.status = SocketStatus.initial,
this.data = const <Message>[],
});
ChatState copyWith({
SocketStatus? status,
List<Message>? data,
}) {
return ChatState(
status: status ?? this.status,
data: data ?? this.data,
);
}
@override
String toString() {
return '''ChatState { status: $status, data_length: ${data.length} }''';
}
@override
List<Object> get props => [data, status];
}
根據需求整理出聊天室會需要處理的事件內容
abstract class ChatEvent extends Equatable {
const ChatEvent();
@override
List<Object> get props => [];
}
class ChatReceiveMessage extends ChatEvent {
final Message msg;
const ChatReceiveMessage(this.msg);
}
class ChatSocketStatusChange extends ChatEvent {
final bool status;
const ChatSocketStatusChange(this.status);
}
class ChatDBInit extends ChatEvent {}
完成業務邏輯的基本定義後,接下來試著跟畫面結合在一下
使用 flutter_bloc 提供的 BlocProvider 提供聊天室 Bloc 的實例
class ChatPage extends StatelessWidget {
ChatPage({Key? key}) : super(key: key);
final Uri uri = Uri.parse('ws://test.dev.rde:8000/?token=sm2');
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ChatBloc(Connection(uri: uri)),
child: const ChatView(),
);
}
}
在 ChatBloc 建構式需定義事件設定,這邊我們可以透過 bloc 的 add 方法觸發事件 ChatDBInit。
早期 bloc 的寫法是在這邊定義
mapEventToState,不過語法比較難理解,後續已建議改成下列的寫法。bloc_issues
我們在接收到 ChatDBInit 事件後透過 on 綁定 _initDB 處理,我們先從 db 取回訊息資料,並透過 ChatState copyWith 產生一個新的 state,並以 bloc 的 emit 觸發狀態異動。
接著在綁定Connection相關的事件:ChatSocketStatusChange、ChatReceiveMessage
ChatBloc(this._connection) : super(const ChatState()) {
on<ChatReceiveMessage>(_onMessage);
on<ChatSocketStatusChange>(_onStateChange);
on<ChatDBInit>(_initDB);
add(ChatDBInit());
}
void _initDB(event, emit) async {
emit(state.copyWith(data: await db.query()));
_connectionSubscription = _connection.connected.listen((bool status) {
add(ChatSocketStatusChange(status));
});
_connectionSubscription = _connection.stream.listen((data) {
if (data["eventName"] == "chat:msg") {
add(ChatReceiveMessage(Message.fromJson(data)));
}
});
}
簡單來說:bloc 其實是有限狀態機的一種設計方式,根據業務邏輯的需要歸納出states,透過events觸發業務邏輯的處理,引發 state 的轉換。
在 View 的處理上,可使用 flutter_bloc 提供的 BlocBuilder 監控狀態的變換而重新渲染畫面,並使用 state 裡與畫面有關的資料。
例如:我們在 ChatState 中定義 status 屬性處理 WebSocket 的連線狀態
Widget build(BuildContext context) {
return Expanded(
flex: 1,
child: BlocBuilder<ChatBloc, ChatState>(
builder: (context, state) {
final btnTitle = state.status == SocketStatus.open ? "已連線" : "請重連";
var controller = context.read<ChatBloc>().controller;
return Column(
children: [
SizedBox(
width: double.infinity,
child: TextButton(
child: Text(btnTitle),
onPressed: () {
print(state.status);
if (state.status == SocketStatus.closed) {
context.read<ChatBloc>().reconnect();
}
},
),
),
使用bloc改寫後程式碼語意更易懂,也不用一直呼叫 setState,完整程式碼在這

我自己在研究bloc時初期遇到的狀況是不太曉得要怎麼將業務邏輯定義清楚以及states、events 的內容要怎麼寫。後來查看官網上的一些範例後才慢慢掌握。
心得如下:
業務邏輯的單位大小由你自己決定:在聊天室的範例中,初期我一直在糾結連線狀態與聊天室訊息記錄是要放在一起還是拆分成兩個bloc,其實沒有對與錯,就看自己怎麼寫符合當下狀況,有需要在拆分也行。
bloc、cubit 兩種寫法哪一種適合我:如果你只要處理狀態資料的轉換而不用事件的狀態那簡單應用 cubit 就好。例如:其實我可以用 cubit 處理接收到聊天室的訊息事件即可。
states 的寫法:定義狀態抽象類別然後在依需求實作不同狀態的子類別還是 使用單一狀態類別透過 copyWith 產生新的狀態類別實例。
我要使用什麼方式觸發states的轉換:在聊天室的範例我從 WebSocket 收到訊息時,我可以發出"接到訊息事件"對應後續的業務邏輯,也可以直接發出新的狀態,那我到底需不需要額外使用事件
follow 原則: 事件 > 業務邏輯 > 狀態
使用bloc將觀察點分離,在組件中我們只要處理與 UI 有關的邏輯,將複雜多變的業務邏輯放到 bloc,在開發與維護或是測試工作上都是很不錯的,建議花點時間研究。