源碼地址
flutter_bloc
是基于 BLoC(Business Logic Component)模式的 Flutter 狀態管理庫,它封裝了 bloc
package,幫助我們更清晰地組織業務邏輯與 UI 的分離。核心思想是 事件驅動 和 狀態響應。
🧠 原理簡介
1. 核心概念
- Event(事件):用戶的輸入或其他外部觸發,比如按鈕點擊。
- State(狀態):界面狀態的表現,比如加載中、成功、失敗。
- Bloc(邏輯組件):接收事件 -> 處理邏輯 -> 發出新狀態。
流程圖如下:
UI → Bloc.add(Event) → Bloc → emit(State) → UI rebuild
🛠? 如何使用
1. 安裝依賴
dependencies:flutter_bloc: ^8.1.3 # 檢查 pub.dev 上的最新版本
2. 創建 Event & State
// counter_event.dart
abstract class CounterEvent {}class Increment extends CounterEvent {}class Decrement extends CounterEvent {}
// counter_state.dart
class CounterState {final int count;CounterState(this.count);
}
3. 創建 Bloc
// counter_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_event.dart';
import 'counter_state.dart';class CounterBloc extends Bloc<CounterEvent, CounterState> {CounterBloc() : super(CounterState(0)) {on<Increment>((event, emit) => emit(CounterState(state.count + 1)));on<Decrement>((event, emit) => emit(CounterState(state.count - 1)));}
}
4. 在 UI 中使用 Bloc
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_bloc.dart';
import 'counter_event.dart';
import 'counter_state.dart';void main() {runApp(MyApp());
}class MyApp extends StatelessWidget {Widget build(BuildContext context) {return BlocProvider(create: (_) => CounterBloc(),child: MaterialApp(home: CounterPage()),);}
}class CounterPage extends StatelessWidget {Widget build(BuildContext context) {final bloc = context.read<CounterBloc>();return Scaffold(appBar: AppBar(title: Text("BLoC Counter")),body: Center(child: BlocBuilder<CounterBloc, CounterState>(builder: (context, state) => Text('Count: ${state.count}', style: TextStyle(fontSize: 30)),),),floatingActionButton: Column(mainAxisAlignment: MainAxisAlignment.end,children: [FloatingActionButton(onPressed: () => bloc.add(Increment()), child: Icon(Icons.add)),SizedBox(height: 10),FloatingActionButton(onPressed: () => bloc.add(Decrement()), child: Icon(Icons.remove)),],),);}
}
? 總結優點
- UI 和業務邏輯分離,利于測試和維護
- 明確的事件-狀態流轉,代碼可讀性強
- 強類型 + 自動重建 UI,適用于中大型項目
和 getx 相比有什么優缺點
flutter_bloc
和 GetX
是 Flutter 中兩種常見的狀態管理方案,各有優缺點,適用于不同的場景。下面是二者的詳細對比:
🆚 flutter_bloc
vs GetX
維度 | flutter_bloc | GetX |
---|---|---|
設計思想 | 響應式 + 明確事件流轉(事件 -> 狀態) | 響應式 + 最小 API(簡潔直接) |
代碼結構 | 規范、結構清晰(Event / State / Bloc) | 極簡、靈活(Controller + Observable) |
學習曲線 | 中等偏陡,概念較多 | 非常簡單,上手極快 |
樣板代碼(boilerplate) | 多,需要定義多個類 | 極少,一個控制器基本搞定 |
可維護性(大型項目) | 高,適合多人協作和規范化開發 | 靈活但風險高,依賴命名和使用習慣 |
社區和文檔 | 強,廣泛用于企業項目,如 Google 官方推薦 | 非官方但很流行,文檔充足 |
依賴注入(DI) | 不強制內置,可配合其他包(如 get_it ) | 內置自動依賴注入 |
導航/路由管理 | 依賴其他包(如 go_router ) | 自帶強大的路由系統 |
性能 | 非常高效,基于 Stream 和 emit | 極高,Reactive 系統 + 最小重建 |
測試友好性 | 強,適合單元測試/集成測試 | 一般,較多手動控制 |
異步處理 | 基于事件流程,清晰且易測試 | 支持 Future/async,但自由度高可能導致不規范 |
? 何時選 flutter_bloc
適合你如果:
- 需要嚴格分離業務邏輯(企業/團隊協作項目)
- 希望狀態流轉可追蹤、可測試
- 項目規模較大或可預期擴展
- 你熟悉 Redux / Rx 風格編程
? 何時選 GetX
適合你如果:
- 項目較小或是個人開發
- 優先快速開發、上手快
- 喜歡靈活、少寫代碼
- 同時希望統一路由/DI/狀態/國際化等功能
🏁 舉例對比:計數器
Bloc 實現
bloc.add(Increment());
// 多個文件:event.dart, state.dart, bloc.dart
GetX 實現
controller.count++;
// 只要一個 Controller 類,UI 使用 Obx 自動監聽
📌 總結一句話:
Bloc
注重規范和可維護性,適合大型工程;GetX
注重極簡和開發效率,適合快速開發。
BuildContext 上的一些擴展方法
在使用 flutter_bloc
(或 provider
)時,BuildContext
上的一些擴展方法是關鍵,它們幫助你從上下文中訪問 Bloc、監聽狀態、或者進行條件性重建。
以下是常見的幾種方法的詳細解釋和對比:
? 1. context.read<T>()
功能:
- 從 widget tree 中 讀取 一個
Bloc
(或其他 Provider 提供的對象),不會監聽其狀態變化。
用途:
- 通常用于觸發事件,如
.add(Event)
,因為不需要監聽。
示例:
context.read<CounterBloc>().add(Increment());
? 2. context.watch<T>()
功能:
- 讀取并訂閱監聽
Bloc
或狀態變化。 - 狀態變化會導致 widget rebuild。
用途:
- 通常用于 UI 需要根據 Bloc 狀態實時刷新。
示例:
final state = context.watch<CounterBloc>().state;
return Text('Count: ${state.count}');
? 3. context.select<T, R>(R Function(T value))
功能:
- 讀取
Bloc
(或 Provider)中某個字段的值,并監聽它的變化。 - 只有當該字段的值發生變化時,才會觸發 rebuild。
用途:
- 精細控制重建,避免無謂的 UI 更新。
示例:
final count = context.select<CounterBloc, int>((bloc) => bloc.state.count);
return Text('Count: $count');
? 4. BlocProvider.of<T>(context)
- 等價于
context.read<T>()
- 是舊寫法,推薦使用
context.read<T>()
更簡潔。
? 5. BlocBuilder<T, S>
功能:
- 監聽
Bloc<T>
的狀態S
并根據狀態變化 rebuild UI。
示例:
BlocBuilder<CounterBloc, CounterState>(builder: (context, state) => Text('Count: ${state.count}'),
);
? 6. BlocListener<T, S>
功能:
- 用于監聽狀態變化并做一次性副作用操作(如彈窗、跳轉)。
示例:
BlocListener<LoginBloc, LoginState>(listener: (context, state) {if (state is LoginSuccess) {Navigator.pushNamed(context, '/home');}},child: ...
);
? 7. BlocConsumer<T, S>
- 相當于
BlocBuilder
+BlocListener
的組合。 - 同時用于 build UI 和執行副作用。
🔁 方法使用場景對比表
方法名 | 是否 rebuild | 是否監聽狀態變化 | 用途 |
---|---|---|---|
read<T>() | ? | ? | 獲取 Bloc 實例、添加事件 |
watch<T>() | ? | ? | 獲取 Bloc 狀態,狀態變就重建 |
select<T, R>() | ? (條件) | ? (某字段變) | 精細控制重建,提高性能 |
BlocBuilder | ? | ? | 渲染 UI |
BlocListener | ? | ? | 處理一次性副作用 |
BlocConsumer | ? | ? | UI 和副作用一起處理 |
使用場景舉例子
好的,我們用一個異步 API 請求的完整例子來演示 flutter_bloc
中各類常用方法的實際應用,包括:
context.read
context.watch
context.select
BlocBuilder
BlocListener
BlocConsumer
🌐 場景描述:請求用戶信息
模擬從網絡請求一個用戶信息(名字、郵箱),展示加載中、成功、失敗三種狀態。
📦 第一步:定義狀態和事件
// user_event.dart
abstract class UserEvent {}class FetchUser extends UserEvent {}
// user_state.dart
abstract class UserState {}class UserInitial extends UserState {}class UserLoading extends UserState {}class UserLoaded extends UserState {final String name;final String email;UserLoaded({required this.name, required this.email});
}class UserError extends UserState {final String message;UserError(this.message);
}
?? 第二步:創建 UserBloc
// user_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'user_event.dart';
import 'user_state.dart';class UserBloc extends Bloc<UserEvent, UserState> {UserBloc() : super(UserInitial()) {on<FetchUser>((event, emit) async {emit(UserLoading());await Future.delayed(Duration(seconds: 2)); // 模擬網絡延遲try {// 模擬 API 成功返回final name = 'Alice';final email = 'alice@example.com';emit(UserLoaded(name: name, email: email));} catch (e) {emit(UserError('Failed to fetch user'));}});}
}
🖼? 第三步:UI 中使用 Bloc
// user_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'user_bloc.dart';
import 'user_event.dart';
import 'user_state.dart';class UserPage extends StatelessWidget {Widget build(BuildContext context) {return BlocProvider(create: (_) => UserBloc(),child: UserView(),);}
}class UserView extends StatelessWidget {Widget build(BuildContext context) {final bloc = context.read<UserBloc>(); // 獲取 Bloc 實例return Scaffold(appBar: AppBar(title: Text('User Info')),body: BlocConsumer<UserBloc, UserState>(listener: (context, state) {if (state is UserError) {ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.message)),);}},builder: (context, state) {if (state is UserInitial) {return Center(child: ElevatedButton(onPressed: () => bloc.add(FetchUser()),child: Text('Load User'),),);} else if (state is UserLoading) {return Center(child: CircularProgressIndicator());} else if (state is UserLoaded) {return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [Text('Name: ${state.name}'),Text('Email: ${state.email}'),],),);} else {return Center(child: Text('Unknown state'));}},),);}
}
🧠 其他方法用法補充
context.watch
示例(只監聽狀態)
final userState = context.watch<UserBloc>().state;
if (userState is UserLoaded) {print(userState.name);
}
context.select
示例(只監聽 name 變化)
final name = context.select<UserBloc, String?>((bloc) {final state = bloc.state;return state is UserLoaded ? state.name : null;
});
? 總結
這個例子完整地展示了:
- 如何使用
Bloc
組織異步邏輯 - 如何使用
context.read
觸發事件 - 如何用
BlocConsumer
分離 UI 構建和副作用(如錯誤提示) - 如何用
watch
/select
實現更細粒度監聽
## 標題RepositoryProvider
RepositoryProvider
是 flutter_bloc
提供的一個工具類,作用是將“非 Bloc 的對象(如 Repository、Service、API 客戶端)注入到 widget tree 中”,供 Bloc 或其他組件使用。它本質上是一個語義化的 Provider
,目的是讓依賴注入更加清晰和語義化。
🧱 1. RepositoryProvider 是什么?
它是 Provider 的語法糖,用于提供數據訪問層(Repository),讓 Bloc 通過依賴注入獲取它。
RepositoryProvider(create: (context) => UserRepository(),child: BlocProvider(create: (context) => UserBloc(context.read<UserRepository>()),child: MyApp(),),
)
🧩 2. 為什么需要 RepositoryProvider?
分離關注點 + 提高可測試性
在 BLoC 架構中,Bloc 只處理業務邏輯,而 Repository 專注于數據來源(數據庫/API/本地緩存等)。
- 🧼 清晰的依賴層次結構
- 🔌 可插拔(方便 mock 測試)
- ?? 依賴共享(避免重復創建對象)
🧪 3. 實戰示例
用戶倉庫
class UserRepository {Future<String> fetchUserName() async {await Future.delayed(Duration(seconds: 1));return 'Alice';}
}
注入層級
void main() {runApp(RepositoryProvider(create: (context) => UserRepository(),child: BlocProvider(create: (context) => UserBloc(context.read<UserRepository>()),child: MyApp(),),),);
}
在 Bloc 中使用
class UserBloc extends Bloc<UserEvent, UserState> {final UserRepository userRepository;UserBloc(this.userRepository) : super(UserInitial()) {on<FetchUser>((event, emit) async {emit(UserLoading());final name = await userRepository.fetchUserName();emit(UserLoaded(name));});}
}
? 4. 優點總結
優點 | 說明 |
---|---|
👓 語義清晰 | 明確指出這是 Repository,不是 Bloc |
?? 對象共享 | 上層創建、下層 Bloc 可復用 |
🧪 易測試 | Bloc 接收參數,可以注入 mock |
🧩 解耦結構 | Bloc 不負責創建數據層 |
?? 5. 注意事項 / 潛在缺點
缺點或注意點 | 說明 |
---|---|
?? 濫用嵌套 | BlocProvider 和 RepositoryProvider 層級深容易混亂,建議封裝為模塊 |
?? 生命周期問題 | 如果作用域太小,Bloc 中引用的 Repository 可能會被提前銷毀 |
?? 多倉庫依賴復雜度提升 | Bloc 依賴多個 Repository 時,構造函數會變長,可考慮封裝為 Service 層或使用 DI 工具(如 get_it) |
📌 何時使用 RepositoryProvider?
? 使用場景:
- 需要復用或共享 Repository 實例(如網絡請求、數據庫訪問)
- Bloc 不應該直接創建依賴對象
- 需要單元測試 Bloc 時方便注入 mock
? 不建議使用場景:
- 簡單小項目可以直接在 Bloc 中創建對象(僅限臨時用)
- 對象生命周期不需要跨多個組件
一個多倉庫 + 多 Bloc 的組合使用場景實例
好的,我們來構建一個中型項目的多倉庫 + 多 Bloc + 多 Provider 示例結構,結合 RepositoryProvider
和 BlocProvider
,實現高內聚、低耦合的結構設計。
🧱 示例需求:用戶信息 + 設置模塊
有兩個功能模塊:
- 用戶模塊(User):從 API 獲取用戶信息
- 設置模塊(Settings):管理本地配置(如暗黑模式)
模塊依賴:
模塊 | 依賴內容 |
---|---|
UserBloc | 依賴 UserRepository |
SettingsBloc | 依賴 SettingsRepository |
📦 項目結構建議
lib/
├── main.dart
├── repositories/
│ ├── user_repository.dart
│ └── settings_repository.dart
├── blocs/
│ ├── user/
│ │ ├── user_bloc.dart
│ │ ├── user_event.dart
│ │ └── user_state.dart
│ └── settings/
│ ├── settings_bloc.dart
│ ├── settings_event.dart
│ └── settings_state.dart
├── pages/
│ ├── user_page.dart
│ └── settings_page.dart
└── app.dart
1?? Repository 實現
user_repository.dart
class UserRepository {Future<String> fetchUserName() async {await Future.delayed(Duration(seconds: 1));return 'Alice';}
}
settings_repository.dart
class SettingsRepository {bool _darkMode = false;bool get isDarkMode => _darkMode;void toggleDarkMode() => _darkMode = !_darkMode;
}
2?? Bloc 實現(以 User 為例)
user_bloc.dart
class UserBloc extends Bloc<UserEvent, UserState> {final UserRepository userRepository;UserBloc(this.userRepository) : super(UserInitial()) {on<FetchUser>((event, emit) async {emit(UserLoading());final name = await userRepository.fetchUserName();emit(UserLoaded(name));});}
}
3?? 組合使用(main.dart)
void main() {runApp(MultiRepositoryProvider(providers: [RepositoryProvider(create: (_) => UserRepository()),RepositoryProvider(create: (_) => SettingsRepository()),],child: MultiBlocProvider(providers: [BlocProvider(create: (context) => UserBloc(context.read<UserRepository>()),),BlocProvider(create: (context) => SettingsBloc(context.read<SettingsRepository>()),),],child: MyApp(),),),);
}
4?? 頁面中使用
user_page.dart
class UserPage extends StatelessWidget {Widget build(BuildContext context) {return BlocBuilder<UserBloc, UserState>(builder: (context, state) {if (state is UserInitial) {return ElevatedButton(onPressed: () => context.read<UserBloc>().add(FetchUser()),child: Text("Load User"),);} else if (state is UserLoaded) {return Text('Hello, ${state.name}');} else {return CircularProgressIndicator();}},);}
}
settings_page.dart
class SettingsPage extends StatelessWidget {Widget build(BuildContext context) {return BlocBuilder<SettingsBloc, SettingsState>(builder: (context, state) {return SwitchListTile(title: Text('Dark Mode'),value: state.isDarkMode,onChanged: (_) => context.read<SettingsBloc>().add(ToggleTheme()),);},);}
}
? 優點總結
- 清晰的依賴注入結構:Bloc 和 Repository 解耦,易維護
- 復用性強:多個 Bloc 可共用同一個 Repository 實例
- 測試友好:可輕松 mock Repository 進行單元測試
- 清晰語義:RepositoryProvider 表明它不是 Bloc,而是數據提供者
🚀 Bonus:封裝更進一步
? 已經成功將 MultiRepositoryProvider
和 MultiBlocProvider
封裝到了 AppProviderWrapper
組件中,結構清晰、職責分離得非常好!
下面是總結和優化建議:
? 目前結構回顧
// main.dart
runApp(AppProviderWrapper(child: MyApp()));
// provider_wrapper.dart
class AppProviderWrapper extends StatelessWidget {final Widget child;const AppProviderWrapper({required this.child});...
}
這非常適合中大型項目,可以讓 main.dart
邏輯更純粹,同時也方便后續擴展全局錯誤監聽、日志注入等中間件。
💡 可選優化建議
1. 使用 lazy: false
控制立即初始化 Bloc
有些 Bloc(如 SettingsBloc)可能希望 App 啟動時就立即初始化:
BlocProvider(lazy: false,create: (context) => SettingsBloc(context.read<SettingsRepository>()),
),
2. 使用 const 構造節省 rebuild
如果子 Widget 和 Bloc 無關,記得加 const
以避免重建。
3. 模塊化進一步優化(可選)
可以把 Bloc、Repository 的創建封裝成方法,增強可維護性:
List<RepositoryProvider> buildRepositories() => [RepositoryProvider(create: (_) => UserRepository()),RepositoryProvider(create: (_) => SettingsRepository()),
];List<BlocProvider> buildBlocs(BuildContext context) => [BlocProvider(create: (_) => UserBloc(context.read())),BlocProvider(create: (_) => SettingsBloc(context.read())),
];
這樣 AppProviderWrapper
中就可以這么寫:
child: MultiBlocProvider(providers: buildBlocs(context),child: child,
),
? 總結
現在的結構已經非常標準、清晰,完全符合企業級 Flutter 項目推薦架構。
源碼地址