前言:Flutter 狀態管理的核心挑戰
在 Flutter 開發中,狀態管理是最重要也最複雜的主題之一。當應用程式規模增長,Widget 樹層級加深,如何讓不同層級的 Widget 能夠正確地監聽和響應狀態變化,成為每個 Flutter 開發者必須面對的挑戰。
選擇錯誤的狀態監聽方式,會導致:
- ❌ 不必要的重繪:整個 Widget 樹被重建,效能下降
- ❌ 記憶體洩漏:忘記釋放監聽器,導致記憶體持續增長
- ❌ 狀態不同步:多個 Widget 顯示不一致的資料
- ❌ 程式碼難以維護:狀態邏輯散落各處,難以追蹤
本文涵蓋內容
本文將深入探討 Flutter 四大主流狀態管理方案的監聽機制:
- Provider:Flutter 官方推薦的輕量級狀態管理
- Bloc:基於 Stream 的企業級狀態管理
- Riverpod:Provider 的改進版本,解決了 Provider 的核心問題
- GetX:高效能的響應式狀態管理
我們將從實際問題出發,比較這四種方案的:
- 🔍 核心原理:底層如何實現狀態監聽
- 💻 實戰範例:可執行的完整程式碼
- ⚡ 效能表現:重繪範圍、記憶體使用
- 🎯 適用場景:什麼情況下使用哪種方案
- ⚠️ 常見陷阱:實戰中容易踩的坑
Provider 狀態監聽機制
Provider 是 Flutter 官方推薦的狀態管理方案,基於 InheritedWidget 實現。它的核心優勢是簡單易用,適合中小型應用。
Provider 核心概念
Provider 使用 依賴注入 (Dependency Injection) 和 作用域 (Scope) 的概念來管理狀態:
關鍵概念:
- Provider 有作用域限制:只有在 Provider 子樹內的 Widget 能存取狀態
- 三種存取方式:
context.watch()、context.read()、context.select() - 自動重繪機制:使用
watch時,狀態改變會自動觸發 Widget 重繪
實戰問題:跨路由讀取 Provider
這是我在開發中遇到的真實問題:
// ❌ 錯誤做法:在不同路由中讀取 provider
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ChangeNotifierProvider(
create: (_) => MyAppState(),
child: HomePage(),
),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// 導航到新路由
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailPage()),
);
},
child: Text('Go to Detail'),
);
}
}
class DetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ❌ 錯誤:這裡會拋出 ProviderNotFoundException
var appState = context.watch<MyAppState>();
return Scaffold(
appBar: AppBar(title: Text(appState.title)),
);
}
}
錯誤訊息:

Error: Could not find the correct Provider<MyAppState> above this DetailPage Widget
This happens because you used a BuildContext that does not include the provider
of your choice.
為什麼會發生這個錯誤?
Provider 是有作用域的 (scoped)。當你在 home 屬性中插入 Provider 時,只有 home 的子樹能存取這個 Provider。
問題分析:
核心原因:
Navigator.push創建的新路由是MaterialApp的直接子節點- 而 Provider 是在
home屬性內,不是MaterialApp的頂層 - 因此新路由無法向上找到 Provider
解決方案:提升 Provider 作用域
將 Provider 提升到整個應用程式的頂層,確保所有路由都能存取:
// ✅ 正確做法:在 MaterialApp 之上插入 Provider
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyAppState(),
child: MaterialApp(
home: HomePage(),
),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ✅ 正確:可以存取 Provider
var appState = context.watch<MyAppState>();
return Scaffold(
appBar: AppBar(title: Text(appState.title)),
body: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailPage()),
);
},
child: Text('Go to Detail'),
),
);
}
}
class DetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ✅ 正確:可以存取 Provider
var appState = context.watch<MyAppState>();
return Scaffold(
appBar: AppBar(title: Text(appState.title)),
body: Text('Detail: ${appState.data}'),
);
}
}

改正後的作用域:
Provider 的三種監聽方式
Provider 提供三種監聽狀態的方法,各有不同的使用場景:
1. context.watch() - 自動重繪
class CounterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 監聽狀態,狀態改變時自動重繪整個 Widget
var counter = context.watch<CounterState>();
return Text('Count: ${counter.value}');
}
}
特性:
- ✅ 狀態改變時自動重繪
- ⚠️ 會重繪整個 Widget,可能影響效能
- 🎯 適用於需要響應狀態變化的 UI 元件
2. context.read() - 單次讀取
class IncrementButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// 只讀取一次,不監聽變化
var counter = context.read<CounterState>();
counter.increment();
},
child: Text('Increment'),
);
}
}
特性:
- ✅ 不會觸發重繪,效能最佳
- ❌ 狀態改變時不會更新
- 🎯 適用於只需要呼叫方法,不需要顯示狀態的場景
3. context.select<T, R>() - 精確監聽
class UsernameWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 只監聽 username 欄位,其他欄位改變時不重繪
var username = context.select<UserState, String>(
(state) => state.username
);
return Text('Username: $username');
}
}
特性:
- ✅ 只監聽特定欄位,減少不必要的重繪
- ✅ 效能優於
watch - 🎯 適用於大型狀態物件,只需要監聽部分欄位
Provider 監聽方式比較
Provider 完整範例
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// 1. 定義狀態類別
class CounterState extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // 通知所有監聽者
}
void decrement() {
_count--;
notifyListeners();
}
}
// 2. 在應用程式頂層提供 Provider
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterState(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterPage(),
);
}
}
// 3. 使用 watch 監聽狀態
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var counter = context.watch<CounterState>();
return Scaffold(
appBar: AppBar(title: Text('Provider Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Count: ${counter.count}',
style: TextStyle(fontSize: 48),
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 使用 read 呼叫方法
ElevatedButton(
onPressed: () => context.read<CounterState>().decrement(),
child: Icon(Icons.remove),
),
SizedBox(width: 20),
ElevatedButton(
onPressed: () => context.read<CounterState>().increment(),
child: Icon(Icons.add),
),
],
),
],
),
),
);
}
}
Bloc 狀態監聽機制
Bloc (Business Logic Component) 是一個基於 Stream 的狀態管理方案,由 Google 工程師開發,特別適合大型企業級應用。
Bloc 核心概念
Bloc 將應用程式分為三個層次:
- Presentation Layer(展示層):UI Widget
- Business Logic Layer(業務邏輯層):Bloc
- Data Layer(資料層):Repository
核心原則:
- 單向資料流:Widget 發送 Event → Bloc 處理 → 發出新 State
- 不可變狀態:每次狀態改變都創建新的 State 物件
- 業務邏輯隔離:UI 不包含業務邏輯,全部在 Bloc 中處理
Bloc 的 Stream-based 監聽
Bloc 使用 Dart 的 Stream 來實現狀態監聽:
// Bloc 內部實作概念
class CounterBloc {
// 輸入:Event Stream
final _eventController = StreamController<CounterEvent>();
Sink<CounterEvent> get eventSink => _eventController.sink;
// 輸出:State Stream
final _stateController = StreamController<CounterState>();
Stream<CounterState> get stateStream => _stateController.stream;
// 監聽 Event,處理後發送新 State
CounterBloc() {
_eventController.stream.listen((event) {
if (event is IncrementEvent) {
final newState = CounterState(count: currentState.count + 1);
_stateController.add(newState);
}
});
}
}
Stream 的優勢:
- ✅ 異步處理:天然支援 async/await
- ✅ 可測試性:Stream 可以輕易 mock
- ✅ 可追蹤性:所有狀態變化都通過 Stream
Bloc 的三種 Widget 監聽方式
1. BlocBuilder - 自動重繪
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
// 每次 state 改變時都會重建
return Text('Count: ${state.count}');
},
)
特性:
- ✅ 狀態改變時自動重繪
- ⚠️ 會重繪整個 builder 內的 Widget 樹
- 🎯 適用於需要根據狀態渲染 UI 的場景
2. BlocListener - 執行副作用
BlocListener<CounterBloc, CounterState>(
listener: (context, state) {
// 不重繪 Widget,只執行副作用
if (state.count >= 10) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Count reached 10!')),
);
}
},
child: SomeWidget(),
)
特性:
- ✅ 不會重繪 UI
- ✅ 適合執行導航、顯示對話框等副作用
- 🎯 適用於根據狀態執行一次性操作
3. BlocConsumer - 結合 Builder 和 Listener
BlocConsumer<CounterBloc, CounterState>(
listener: (context, state) {
// 執行副作用
if (state.isError) {
showDialog(/* ... */);
}
},
builder: (context, state) {
// 重繪 UI
return Text('Count: ${state.count}');
},
)
特性:
- ✅ 同時支援 UI 更新和副作用
- ⚠️ 不要在 listener 和 builder 中做相同的事
- 🎯 適用於複雜場景
Bloc 監聽方式比較
Bloc 完整範例
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// 1. 定義 Event
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
// 2. 定義 State
class CounterState {
final int count;
CounterState({required this.count});
}
// 3. 定義 Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(count: 0)) {
// 註冊 Event Handler
on<IncrementEvent>((event, emit) {
emit(CounterState(count: state.count + 1));
});
on<DecrementEvent>((event, emit) {
emit(CounterState(count: state.count - 1));
});
}
}
// 4. 在應用程式頂層提供 Bloc
void main() {
runApp(
BlocProvider(
create: (_) => CounterBloc(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterPage(),
);
}
}
// 5. 使用 BlocBuilder 監聽狀態
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Bloc Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// BlocBuilder 自動重繪
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text(
'Count: ${state.count}',
style: TextStyle(fontSize: 48),
);
},
),
SizedBox(height: 20),
// BlocListener 執行副作用
BlocListener<CounterBloc, CounterState>(
listener: (context, state) {
if (state.count >= 10) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('達到 10 了!')),
);
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
context.read<CounterBloc>().add(DecrementEvent());
},
child: Icon(Icons.remove),
),
SizedBox(width: 20),
ElevatedButton(
onPressed: () {
context.read<CounterBloc>().add(IncrementEvent());
},
child: Icon(Icons.add),
),
],
),
),
],
),
),
);
}
}
Bloc 非同步處理範例
Bloc 特別適合處理異步操作,例如 API 請求:
// Event
class FetchUserEvent extends UserEvent {
final String userId;
FetchUserEvent(this.userId);
}
// State
abstract class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final User user;
UserLoaded(this.user);
}
class UserError extends UserState {
final String message;
UserError(this.message);
}
// Bloc
class UserBloc extends Bloc<UserEvent, UserState> {
final UserRepository repository;
UserBloc(this.repository) : super(UserInitial()) {
on<FetchUserEvent>((event, emit) async {
emit(UserLoading());
try {
final user = await repository.fetchUser(event.userId);
emit(UserLoaded(user));
} catch (e) {
emit(UserError(e.toString()));
}
});
}
}
// UI
class UserProfile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<UserBloc, UserState>(
builder: (context, state) {
if (state is UserLoading) {
return CircularProgressIndicator();
} else if (state is UserLoaded) {
return Text('User: ${state.user.name}');
} else if (state is UserError) {
return Text('Error: ${state.message}');
} else {
return Text('Press button to load user');
}
},
);
}
}
Riverpod 狀態監聽機制
Riverpod 是 Provider 的作者 Remi Rousselet 開發的新一代狀態管理方案,解決了 Provider 的核心問題:依賴 BuildContext。
Riverpod 的核心改進
Riverpod 的主要優勢:
- ✅ 不依賴 BuildContext:可以在任何地方讀取 Provider
- ✅ 編譯期類型安全:錯誤在編譯時就能發現
- ✅ 更好的測試支援:可以輕易 override Provider
- ✅ 自動處理生命週期:不需要手動 dispose
Riverpod 的 Provider 種類
Riverpod 提供多種 Provider,各有不同的用途:
| Provider 類型 | 用途 | 範例 |
|---|---|---|
Provider | 不會改變的值 | 設定、常數 |
StateProvider | 簡單狀態 | 計數器、布林值 |
StateNotifierProvider | 複雜狀態 | 用戶資料、購物車 |
FutureProvider | 異步資料 | API 請求 |
StreamProvider | 即時資料流 | WebSocket、Firebase |
Riverpod 的三種監聽方式
1. ref.watch() - 自動重繪
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 監聽狀態,狀態改變時自動重繪
final count = ref.watch(counterProvider);
return Text('Count: $count');
}
}
特性:
- ✅ 狀態改變時自動重繪
- ✅ 自動管理依賴關係
- 🎯 適用於需要響應狀態變化的 UI
2. ref.read() - 單次讀取
class IncrementButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
// 只讀取一次,不監聽變化
ref.read(counterProvider.notifier).increment();
},
child: Text('Increment'),
);
}
}
特性:
- ✅ 不會觸發重繪
- ✅ 適合事件處理
- 🎯 適用於呼叫方法,不需要顯示狀態
3. ref.listen() - 執行副作用
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 監聽狀態變化,執行副作用
ref.listen<int>(counterProvider, (previous, next) {
if (next >= 10) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('達到 10 了!')),
);
}
});
final count = ref.watch(counterProvider);
return Text('Count: $count');
}
}
特性:
- ✅ 不會重繪 UI
- ✅ 可以比較 previous 和 next
- 🎯 適用於導航、對話框等副作用
Riverpod 監聽方式比較
Riverpod 完整範例
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. 定義 StateNotifier
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
}
// 2. 創建 Provider(全域定義)
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
// 3. 使用 ProviderScope 包裹應用程式
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterPage(),
);
}
}
// 4. 使用 ConsumerWidget 監聽狀態
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// ref.watch 自動監聽狀態
final count = ref.watch(counterProvider);
// ref.listen 執行副作用
ref.listen<int>(counterProvider, (previous, next) {
if (next >= 10 && (previous ?? 0) < 10) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('達到 10 了!')),
);
}
});
return Scaffold(
appBar: AppBar(title: Text('Riverpod Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Count: $count',
style: TextStyle(fontSize: 48),
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
// ref.read 呼叫方法
ref.read(counterProvider.notifier).decrement();
},
child: Icon(Icons.remove),
),
SizedBox(width: 20),
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).increment();
},
child: Icon(Icons.add),
),
],
),
],
),
),
);
}
}
Riverpod 異步資料載入
Riverpod 的 FutureProvider 和 AsyncValue 讓異步資料處理變得極其簡單:
// 定義 FutureProvider
final userProvider = FutureProvider.autoDispose.family<User, String>((ref, userId) async {
final repository = ref.watch(userRepositoryProvider);
return await repository.fetchUser(userId);
});
// UI
class UserProfile extends ConsumerWidget {
final String userId;
UserProfile({required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncUser = ref.watch(userProvider(userId));
// AsyncValue 自動處理 loading、data、error 狀態
return asyncUser.when(
loading: () => CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
data: (user) => Text('User: ${user.name}'),
);
}
}
AsyncValue 的優勢:
- ✅ 自動處理三種狀態:loading、data、error
- ✅ 類型安全,不需要手動類型判斷
- ✅ 減少 boilerplate 程式碼
GetX 狀態監聽機制
GetX 是一個極簡主義的狀態管理方案,以極低的學習成本和極高的效能著稱。它不僅是狀態管理,還包含路由管理、依賴注入等功能。
GetX 核心概念
GetX 的核心是 響應式編程 (Reactive Programming),使用 .obs 讓變數變成可觀察的:
// 傳統方式
int count = 0;
count++; // UI 不會更新
// GetX 響應式
var count = 0.obs;
count++; // UI 自動更新
GetX 的三大核心:
- 響應式狀態管理:
.obs變數自動通知 UI 更新 - 簡單狀態管理:
GetBuilder手動控制更新 - 依賴注入:
Get.put()和Get.find()管理依賴
GetX 響應式更新原理
GetX 的兩種監聽方式
1. Obx - 響應式自動更新
class CounterController extends GetxController {
var count = 0.obs;
void increment() => count++;
void decrement() => count--;
}
// UI
class CounterPage extends StatelessWidget {
final controller = Get.put(CounterController());
@override
Widget build(BuildContext context) {
return Obx(() => Text('Count: ${controller.count}'));
}
}
特性:
- ✅ 極簡語法,不需要 Builder pattern
- ✅ 自動追蹤依賴,只重繪 Obx 內的 Widget
- ✅ 效能極佳,精確更新
- 🎯 適用於響應式狀態
2. GetBuilder - 手動控制更新
class CounterController extends GetxController {
int count = 0;
void increment() {
count++;
update(); // 手動通知更新
}
}
// UI
class CounterPage extends StatelessWidget {
final controller = Get.put(CounterController());
@override
Widget build(BuildContext context) {
return GetBuilder<CounterController>(
builder: (controller) => Text('Count: ${controller.count}'),
);
}
}
特性:
- ✅ 不使用 Stream,記憶體佔用極低
- ✅ 手動控制更新時機
- ✅ 適合效能敏感的場景
- 🎯 適用於大量資料的列表
GetX vs Obx 比較
GetX 完整範例
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// 1. 定義 Controller
class CounterController extends GetxController {
// 響應式變數
var count = 0.obs;
void increment() => count++;
void decrement() => count--;
// 當 count 達到 10 時執行副作用
@override
void onInit() {
super.onInit();
ever(count, (value) {
if (value >= 10) {
Get.snackbar('提示', '達到 10 了!');
}
});
}
}
// 2. 初始化 GetX(不需要包裹 Provider)
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp( // 使用 GetMaterialApp
home: CounterPage(),
);
}
}
// 3. 使用 Obx 監聽狀態
class CounterPage extends StatelessWidget {
// 依賴注入
final CounterController controller = Get.put(CounterController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('GetX Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Obx 自動監聽 count 變化
Obx(() => Text(
'Count: ${controller.count}',
style: TextStyle(fontSize: 48),
)),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: controller.decrement,
child: Icon(Icons.remove),
),
SizedBox(width: 20),
ElevatedButton(
onPressed: controller.increment,
child: Icon(Icons.add),
),
],
),
],
),
),
);
}
}
GetX 的響應式 Worker
GetX 提供多種 Worker 來監聽響應式變數的變化:
class UserController extends GetxController {
var username = ''.obs;
var isLoggedIn = false.obs;
@override
void onInit() {
super.onInit();
// 1. ever - 每次變化都執行
ever(username, (value) {
print('Username changed to: $value');
});
// 2. once - 只執行一次
once(isLoggedIn, (value) {
if (value) {
Get.offAll(() => HomePage());
}
});
// 3. debounce - 防抖動(適合搜尋)
debounce(username, (value) {
searchUser(value);
}, time: Duration(milliseconds: 500));
// 4. interval - 固定間隔執行
interval(username, (value) {
saveToDatabase(value);
}, time: Duration(seconds: 5));
}
}
四種方案深度比較
現在我們來全面比較 Provider、Bloc、Riverpod、GetX 這四種狀態管理方案。
效能比較
| 方案 | 記憶體使用 | 重繪效率 | 初始化成本 |
|---|---|---|---|
| Provider | 低 | 中 | 低 |
| Bloc | 中 | 高 | 中 |
| Riverpod | 低 | 高 | 低 |
| GetX | 極低 | 極高 | 極低 |
學習曲線比較
學習難度排名(從易到難):
- 🥇 GetX:極簡語法,幾乎零學習成本
- 🥈 Provider:官方推薦,文件完整
- 🥉 Riverpod:Provider 的改進版,需要理解新概念
- Bloc:需要理解 Stream、Event、State,學習曲線最陡
程式碼複雜度比較
同樣的計數器功能,四種方案的程式碼量:
| 方案 | 檔案數量 | 總行數 | boilerplate 程度 |
|---|---|---|---|
| GetX | 1 個檔案 | ~40 行 | 極低 |
| Provider | 1 個檔案 | ~60 行 | 低 |
| Riverpod | 1 個檔案 | ~70 行 | 中 |
| Bloc | 3 個檔案 | ~120 行 | 高 |
適用場景比較
選擇建議:
| 場景 | 推薦方案 | 原因 |
|---|---|---|
| 個人專案、快速原型 | GetX | 開發速度最快,程式碼最簡潔 |
| 中小型應用 | Provider / Riverpod | 官方支援,社群資源豐富 |
| 大型企業應用 | Bloc | 架構清晰,可測試性最高 |
| 從 Provider 遷移 | Riverpod | 概念相似,遷移成本低 |
| 需要複雜異步處理 | Bloc / Riverpod | Stream 和 FutureProvider 支援完善 |
生態系統比較
| 方案 | 社群規模 | 官方支援 | 套件數量 | 文件品質 |
|---|---|---|---|---|
| Provider | ⭐⭐⭐⭐⭐ | ✅ Flutter 官方 | 極多 | 優秀 |
| Bloc | ⭐⭐⭐⭐⭐ | ✅ 社群官方 | 多 | 優秀 |
| Riverpod | ⭐⭐⭐⭐ | ✅ 作者維護 | 中等 | 良好 |
| GetX | ⭐⭐⭐⭐⭐ | ✅ 社群維護 | 多 | 良好 |
測試支援比較
// Provider 測試
testWidgets('Provider counter test', (tester) async {
await tester.pumpWidget(
ChangeNotifierProvider(
create: (_) => CounterState(),
child: MyApp(),
),
);
expect(find.text('0'), findsOneWidget);
});
// Bloc 測試(最容易測試)
blocTest<CounterBloc, CounterState>(
'emits [1] when IncrementEvent is added',
build: () => CounterBloc(),
act: (bloc) => bloc.add(IncrementEvent()),
expect: () => [CounterState(count: 1)],
);
// Riverpod 測試(支援 override)
testWidgets('Riverpod counter test', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
counterProvider.overrideWith((ref) => mockCounter),
],
child: MyApp(),
),
);
});
// GetX 測試
test('GetX counter test', () {
final controller = CounterController();
controller.increment();
expect(controller.count.value, 1);
});
測試難易度排名:
- 🥇 Bloc:專為測試設計,有
bloc_test套件 - 🥈 Riverpod:支援 override,輕鬆 mock
- 🥉 Provider:可測試,但需要包裹 Widget
- GetX:單元測試容易,Widget 測試較難
實戰經驗與常見陷阱
Provider 常見陷阱
1. 跨路由存取失敗
問題:
// ❌ Provider 在 home 內,新路由無法存取
MaterialApp(
home: ChangeNotifierProvider(
create: (_) => MyState(),
child: HomePage(),
),
)
解決:
// ✅ Provider 在 MaterialApp 外層
ChangeNotifierProvider(
create: (_) => MyState(),
child: MaterialApp(
home: HomePage(),
),
)
2. 忘記 notifyListeners
問題:
class CounterState extends ChangeNotifier {
int count = 0;
void increment() {
count++;
// ❌ 忘記呼叫 notifyListeners,UI 不會更新
}
}
解決:
void increment() {
count++;
notifyListeners(); // ✅ 通知監聽者
}
3. 過度使用 context.watch
問題:
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ❌ 整個 Widget 都會重繪
var state = context.watch<LargeState>();
return Column(
children: [
Text(state.title), // 只用到 title
ExpensiveWidget(), // 但這個也會重繪
],
);
}
}
解決:
// ✅ 使用 select 只監聽需要的欄位
var title = context.select<LargeState, String>((state) => state.title);
// ✅ 或分割成更小的 Widget
class TitleWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
var title = context.watch<LargeState>().title;
return Text(title);
}
}
Bloc 常見陷阱
1. 忘記關閉 Stream
問題:
class MyBloc extends Bloc<MyEvent, MyState> {
final StreamController _controller = StreamController();
// ❌ 忘記 close,造成記憶體洩漏
}
解決:
class MyBloc extends Bloc<MyEvent, MyState> {
final StreamController _controller = StreamController();
@override
Future<void> close() {
_controller.close(); // ✅ 記得關閉
return super.close();
}
}
2. 在 Builder 中發送 Event
問題:
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
// ❌ 在 builder 中發送 Event 會導致無限循環
context.read<CounterBloc>().add(IncrementEvent());
return Text('Count: ${state.count}');
},
)
解決:
// ✅ 使用 BlocListener 執行副作用
BlocListener<CounterBloc, CounterState>(
listener: (context, state) {
if (state.shouldIncrement) {
context.read<CounterBloc>().add(IncrementEvent());
}
},
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text('Count: ${state.count}');
},
),
)
3. 過度使用 BlocConsumer
問題:
// ❌ listener 和 builder 做相同的事
BlocConsumer<UserBloc, UserState>(
listener: (context, state) {
if (state is UserLoaded) {
print('User loaded');
}
},
builder: (context, state) {
if (state is UserLoaded) {
return Text(state.user.name);
}
return CircularProgressIndicator();
},
)
解決:
// ✅ 明確分工:listener 處理副作用,builder 處理 UI
BlocConsumer<UserBloc, UserState>(
listener: (context, state) {
if (state is UserError) {
// 副作用:顯示錯誤對話框
showDialog(/* ... */);
}
},
builder: (context, state) {
// UI 渲染
if (state is UserLoaded) {
return Text(state.user.name);
}
return CircularProgressIndicator();
},
)
Riverpod 常見陷阱
1. 混淆 watch 和 read
問題:
final myProvider = Provider((ref) {
// ❌ 在 Provider 建構式中使用 watch
final value = ref.watch(otherProvider);
// 這會導致 myProvider 在 otherProvider 改變時重建
return MyClass(value);
});
解決:
// ✅ 根據需求選擇正確的方法
final myProvider = Provider((ref) {
// 如果需要響應式依賴,使用 watch
final value = ref.watch(otherProvider);
// 如果只需要一次性讀取,使用 read
final config = ref.read(configProvider);
return MyClass(value, config);
});
2. 忘記使用 autoDispose
問題:
// ❌ 沒有使用 autoDispose,即使不再使用也不會釋放
final userProvider = FutureProvider<User>((ref) async {
return await fetchUser();
});
解決:
// ✅ 使用 autoDispose 自動清理
final userProvider = FutureProvider.autoDispose<User>((ref) async {
return await fetchUser();
});
3. 錯誤使用 StateProvider
問題:
// ❌ 複雜狀態使用 StateProvider
final userProvider = StateProvider<User>((ref) => User());
// 修改時很笨拙
ref.read(userProvider.notifier).state = user.copyWith(name: 'New Name');
解決:
// ✅ 複雜狀態使用 StateNotifierProvider
class UserNotifier extends StateNotifier<User> {
UserNotifier() : super(User());
void updateName(String name) {
state = state.copyWith(name: name);
}
}
final userProvider = StateNotifierProvider<UserNotifier, User>((ref) {
return UserNotifier();
});
// 使用時更直觀
ref.read(userProvider.notifier).updateName('New Name');
GetX 常見陷阱
1. 全域狀態污染
問題:
// ❌ 使用 Get.put 創建全域單例
class HomePage extends StatelessWidget {
final controller = Get.put(HomeController());
// 這個 Controller 會一直存在,即使頁面已經離開
}
解決:
// ✅ 使用 Get.lazyPut 或 GetView 自動管理生命週期
class HomePage extends GetView<HomeController> {
@override
Widget build(BuildContext context) {
// GetView 會自動查找 Controller
return Text(controller.title);
}
}
// 或在路由中註冊
GetPage(
name: '/home',
page: () => HomePage(),
binding: BindingsBuilder(() {
Get.lazyPut(() => HomeController());
}),
)
2. 過度使用 Obx
問題:
// ❌ 包裹太大的 Widget 樹
Obx(() => Column(
children: [
Text(controller.title.value),
ExpensiveWidget(), // 不相關的 Widget 也會重繪
AnotherExpensiveWidget(),
],
))
解決:
// ✅ 只包裹需要響應式更新的最小單位
Column(
children: [
Obx(() => Text(controller.title.value)), // 只包裹需要更新的部分
ExpensiveWidget(),
AnotherExpensiveWidget(),
],
)
3. 忘記 .value
問題:
class MyController extends GetxController {
var count = 0.obs;
void printCount() {
print(count); // ❌ 這會印出 Rx<int> 物件,不是值
}
}
解決:
void printCount() {
print(count.value); // ✅ 使用 .value 取得實際值
}
總結與建議
快速選擇指南
根據你的需求,使用以下決策樹選擇最適合的狀態管理方案:
最終建議
如果你是 Flutter 初學者:
- 🎯 從 Provider 開始,學習官方推薦的方式
- 📚 理解
context.watchvscontext.read的差異 - ⚠️ 注意 Provider 的作用域問題
如果你需要快速開發原型:
- 🎯 使用 GetX,極簡語法讓你專注在業務邏輯
- 📦 一個套件包含狀態管理、路由、依賴注入
- ⚡ 效能極佳,開發體驗流暢
如果你正在開發中大型應用:
- 🎯 選擇 Riverpod,現代化且類型安全
- 🔒 編譯期錯誤檢查,減少 runtime 錯誤
- 🧪 優秀的測試支援
如果你的團隊重視架構和可測試性:
- 🎯 採用 Bloc,清晰的架構分層
- 📋 強制業務邏輯與 UI 分離
- ✅ 最完善的測試工具鏈
混合使用策略
實際專案中,不同模組可以使用不同的狀態管理方案:
// 全域狀態:Riverpod(類型安全)
final themeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.light);
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>(...);
// 頁面狀態:GetX(快速開發)
class ShoppingCartController extends GetxController {
var items = <Item>[].obs;
}
// 複雜業務流程:Bloc(清晰架構)
class CheckoutBloc extends Bloc<CheckoutEvent, CheckoutState> {
// 處理支付流程
}
// 簡單 UI 狀態:setState(最簡單)
class ExpandableCard extends StatefulWidget {
// 展開/收合狀態不需要狀態管理
}
關鍵要點
- 沒有完美的方案:每種方案都有優缺點,根據需求選擇
- 不要過度設計:簡單的頁面狀態不需要複雜的狀態管理
- 保持一致性:團隊內部盡量使用統一的方案
- 效能優化:使用
select、Obx、BlocBuilder減少重繪範圍 - 測試覆蓋:狀態管理是最值得寫測試的部分
學習資源
Provider:
Bloc:
Riverpod:
GetX:
結語
Flutter 的狀態管理方案眾多,每種都有其存在的理由和適用場景。關鍵不是選擇「最好」的方案,而是選擇最適合你的專案和團隊的方案。
從實戰經驗來看:
- 🎯 小型專案:GetX 或 Provider 足夠
- 🎯 中型專案:Riverpod 是最佳平衡
- 🎯 大型專案:Bloc 提供最嚴謹的架構
記住:好的狀態管理不是技術堆疊,而是清晰的資料流和可預測的狀態變化。 無論選擇哪種方案,保持程式碼簡潔、測試覆蓋充足、團隊共識一致,才是成功的關鍵。
希望這篇文章能幫助你理解 Flutter 狀態管理的核心概念,並在實際專案中做出明智的選擇。Happy coding! 🚀
