前言:Flutter 狀態管理的核心挑戰

在 Flutter 開發中,狀態管理是最重要也最複雜的主題之一。當應用程式規模增長,Widget 樹層級加深,如何讓不同層級的 Widget 能夠正確地監聽和響應狀態變化,成為每個 Flutter 開發者必須面對的挑戰。

選擇錯誤的狀態監聽方式,會導致:

  • 不必要的重繪:整個 Widget 樹被重建,效能下降
  • 記憶體洩漏:忘記釋放監聽器,導致記憶體持續增長
  • 狀態不同步:多個 Widget 顯示不一致的資料
  • 程式碼難以維護:狀態邏輯散落各處,難以追蹤

本文涵蓋內容

本文將深入探討 Flutter 四大主流狀態管理方案的監聽機制:

  1. Provider:Flutter 官方推薦的輕量級狀態管理
  2. Bloc:基於 Stream 的企業級狀態管理
  3. Riverpod:Provider 的改進版本,解決了 Provider 的核心問題
  4. GetX:高效能的響應式狀態管理

我們將從實際問題出發,比較這四種方案的:

  • 🔍 核心原理:底層如何實現狀態監聽
  • 💻 實戰範例:可執行的完整程式碼
  • 效能表現:重繪範圍、記憶體使用
  • 🎯 適用場景:什麼情況下使用哪種方案
  • ⚠️ 常見陷阱:實戰中容易踩的坑

Provider 狀態監聽機制

Provider 是 Flutter 官方推薦的狀態管理方案,基於 InheritedWidget 實現。它的核心優勢是簡單易用,適合中小型應用。

Provider 核心概念

Provider 使用 依賴注入 (Dependency Injection)作用域 (Scope) 的概念來管理狀態:

Mermaid Diagram

關鍵概念:

  • 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)),
    );
  }
}

錯誤訊息:

Provider 跨路由錯誤

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。

問題分析:

Mermaid Diagram

核心原因:

  1. Navigator.push 創建的新路由是 MaterialApp 的直接子節點
  2. 而 Provider 是在 home 屬性內,不是 MaterialApp 的頂層
  3. 因此新路由無法向上找到 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}'),
    );
  }
}

修正後的程式碼

改正後的作用域:

Mermaid Diagram

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 監聽方式比較

Mermaid Diagram

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 將應用程式分為三個層次:

  1. Presentation Layer(展示層):UI Widget
  2. Business Logic Layer(業務邏輯層):Bloc
  3. Data Layer(資料層):Repository
Mermaid Diagram

核心原則:

  • 單向資料流: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 監聽方式比較

Mermaid Diagram

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 的核心改進

Mermaid Diagram

Riverpod 的主要優勢:

  1. 不依賴 BuildContext:可以在任何地方讀取 Provider
  2. 編譯期類型安全:錯誤在編譯時就能發現
  3. 更好的測試支援:可以輕易 override Provider
  4. 自動處理生命週期:不需要手動 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 監聽方式比較

Mermaid Diagram

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 的 FutureProviderAsyncValue 讓異步資料處理變得極其簡單:

// 定義 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 的三大核心:

  1. 響應式狀態管理.obs 變數自動通知 UI 更新
  2. 簡單狀態管理GetBuilder 手動控制更新
  3. 依賴注入Get.put()Get.find() 管理依賴

GetX 響應式更新原理

Mermaid Diagram

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 比較

Mermaid Diagram

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 這四種狀態管理方案。

效能比較

Mermaid Diagram
方案記憶體使用重繪效率初始化成本
Provider
Bloc
Riverpod
GetX極低極高極低

學習曲線比較

Mermaid Diagram

學習難度排名(從易到難):

  1. 🥇 GetX:極簡語法,幾乎零學習成本
  2. 🥈 Provider:官方推薦,文件完整
  3. 🥉 Riverpod:Provider 的改進版,需要理解新概念
  4. Bloc:需要理解 Stream、Event、State,學習曲線最陡

程式碼複雜度比較

同樣的計數器功能,四種方案的程式碼量:

方案檔案數量總行數boilerplate 程度
GetX1 個檔案~40 行極低
Provider1 個檔案~60 行
Riverpod1 個檔案~70 行
Bloc3 個檔案~120 行

適用場景比較

Mermaid Diagram

選擇建議:

場景推薦方案原因
個人專案、快速原型GetX開發速度最快,程式碼最簡潔
中小型應用Provider / Riverpod官方支援,社群資源豐富
大型企業應用Bloc架構清晰,可測試性最高
從 Provider 遷移Riverpod概念相似,遷移成本低
需要複雜異步處理Bloc / RiverpodStream 和 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);
});

測試難易度排名:

  1. 🥇 Bloc:專為測試設計,有 bloc_test 套件
  2. 🥈 Riverpod:支援 override,輕鬆 mock
  3. 🥉 Provider:可測試,但需要包裹 Widget
  4. 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 取得實際值
}

總結與建議

快速選擇指南

根據你的需求,使用以下決策樹選擇最適合的狀態管理方案:

Mermaid Diagram

最終建議

如果你是 Flutter 初學者:

  • 🎯 從 Provider 開始,學習官方推薦的方式
  • 📚 理解 context.watch vs context.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 {
  // 展開/收合狀態不需要狀態管理
}

關鍵要點

  1. 沒有完美的方案:每種方案都有優缺點,根據需求選擇
  2. 不要過度設計:簡單的頁面狀態不需要複雜的狀態管理
  3. 保持一致性:團隊內部盡量使用統一的方案
  4. 效能優化:使用 selectObxBlocBuilder 減少重繪範圍
  5. 測試覆蓋:狀態管理是最值得寫測試的部分

學習資源

Provider:

Bloc:

Riverpod:

GetX:


結語

Flutter 的狀態管理方案眾多,每種都有其存在的理由和適用場景。關鍵不是選擇「最好」的方案,而是選擇最適合你的專案和團隊的方案。

從實戰經驗來看:

  • 🎯 小型專案:GetX 或 Provider 足夠
  • 🎯 中型專案:Riverpod 是最佳平衡
  • 🎯 大型專案:Bloc 提供最嚴謹的架構

記住:好的狀態管理不是技術堆疊,而是清晰的資料流和可預測的狀態變化。 無論選擇哪種方案,保持程式碼簡潔、測試覆蓋充足、團隊共識一致,才是成功的關鍵。

希望這篇文章能幫助你理解 Flutter 狀態管理的核心概念,並在實際專案中做出明智的選擇。Happy coding! 🚀