大家好,歡迎來到第六天!在 Day 5,我們建立了完整的導航架構。今天,我們將深入探討 Flutter 世界的核心議題:狀態管理。
狀態管理可以說是 Flutter App 的「神經系統」,負責協調各個元件之間的溝通和資料流動。在「Crew Up!」專案中,我們不僅要了解如何使用狀態管理工具,更要理解其背後的設計哲學。
還記得 Day 1 我們選擇的 Clean Architecture 嗎?狀態管理正是實現這個架構的關鍵工具。Riverpod 2.0 不只是狀態管理工具,更是實現 Day 1 設計的依賴反轉原則的最佳伙伴:
在 Flutter 社群中,狀態管理方案的選擇一直是個熱門話題。各種方案都有其特色:
在「Crew Up!」專案中,考慮到 Day 1 設計的 Clean Architecture,我們選擇 Riverpod 2.0 是因為:
@riverpod
註解大幅減少樣板程式碼從過去實際專案經驗來看,Riverpod 2.0 的引入帶來了顯著的開發體驗提升,因此這一次我們在 Crew Up 專案中選擇統一使用 2.0:
🔧 Riverpod 1.0 的挑戰:
final xxxProvider = Provider<Xxx>((ref) => ...)
⚡ Riverpod 2.0 的優勢:
riverpod_annotation
+ riverpod_generator
,@riverpod
註解自動產生最佳化的 ProviderautoDispose
與 family
模式,消除人為錯誤📊 實際專案效益:
在我們團隊的重構專案中,成功重構了 70+ providers,不僅統一了寫法,還建立了完整的 CI 流程與品質保證機制。UI 層面影響極小,主要只需更名呼叫點(如 xxxProviderFamily(...)
→ xxxProvider(...)
),ConsumerWidget 的用法完全不變。
⚖️ 效能考量與權衡:
雖然 @riverpod
程式碼產生帶來便利,但也有需要考量的面向:
🎯 為什麼 Crew Up 專案適合 Riverpod 2.0?
考慮到我們專案的特性,Riverpod 2.0 是最佳選擇:
💡 重要提醒:技術選擇沒有銀彈。如果您的專案是小型 MVP、團隊對 Riverpod 不熟悉、或需要極致的效能控制,可能 setState 或 1.0 的 StateNotifier 會是更好的選擇。選擇應該基於團隊能力、專案規模和長期目標。
在選擇狀態管理方案時,根據 Flutter Engineering 的建議,我們不應僅憑受歡迎程度做決定,而應考慮以下關鍵因素:
📋 選擇考量標準:
⭐ 基於 Flutter Engineering 架構特性評估:
狀態管理方案 | Simplicity簡潔性 | Productivity生產力 | Testability可測試性 | Agility敏捷性 | Adaptability適應性 | Scalability可擴展性 |
---|---|---|---|---|---|---|
Riverpod | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
BLoC | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
MobX | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
setState | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
評分範圍:⭐ (最低) 到 ⭐⭐⭐⭐⭐ (最高)
🎯 2024年推薦方案 (Flutter Engineering):
Riverpod, BLoC/Cubit, MobX, GetIt with GetItMixin, Signal, 和 Redux
🏗️ 架構考量與實作建議:
根據 Flutter Engineering 的建議,在選擇狀態管理方案時應該:
💡 重要提醒:沒有通用的解決方案,但良好的架構基礎有助於快速決策。在我們的「Crew Up!」專案中,基於以上評估標準,Riverpod 2.0 在多個維度都表現優異,特別是在可擴展性和適應性方面。
Riverpod 2.0 透過 @riverpod
註解自動產生不同類型的 Provider:
@riverpod
修飾回傳值的函數,適合提供 Repository 實例等依賴@riverpod
修飾 Future<T>
函數,自動產生 AutoDisposeFutureProvider<T>
@riverpod
修飾繼承 _$XXXNotifier
的類別,管理複雜狀態ref.watch()
依賴其他 Provider 來計算衍生狀態首先,我們在 Data 層實作 Domain 層定義的 Repository 介面:
// lib/features/home/data/repositories/index_repository_impl.dart
// (imports omitted)
class IndexRepositoryImpl implements IndexRepository {
@override
Future<List<Activity>> getIndexActivities() async {
// 實作資料獲取邏輯
}
}
// 通過 Riverpod 將實作注入到系統中
@riverpod
IndexRepository indexRepository(Ref ref) {
return IndexRepositoryImpl();
}
// lib/features/home/domain/usecases/get_activities_usecase.dart
// (imports omitted)
@riverpod
Future<List<Activity>> getActivitiesUseCase(Ref ref) async {
// 使用 ref.read 是因為我們只需要取得 repository 實例
// 而不需要監聽它的狀態變化
final repository = ref.read(indexRepositoryProvider);
return await repository.getIndexActivities();
}
💡 ref.read vs ref.watch 的使用時機:
ref.read
:用於一次性讀取,不會觸發重建,適合在事件處理或 Provider 內部使用ref.watch
:用於監聽狀態變化,當依賴的 Provider 更新時會觸發重建,適合在 UI 中使用
// lib/features/home/presentation/screens/index_screen.dart
// (imports omitted)
class IndexScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 直接使用 UseCase,實現依賴反轉
final activitiesAsync = ref.watch(getActivitiesUseCaseProvider);
return Scaffold(
body: activitiesAsync.when(
data: (activities) => ActivityList(activities: activities),
loading: () => CircularProgressIndicator(),
error: (error, _) => ErrorWidget(error),
),
);
}
}
📋 UseCase vs Notifier 的職責劃分:
UseCase
:負責單一的業務操作(如從後端獲取資料),通常是無狀態的Notifier
:更貼近 UI,管理特定畫面的狀態(如當前選中的 Tab),處理來自 UI 的事件
// lib/features/home/presentation/providers/index_view_model.dart
// (imports omitted)
@riverpod
class IndexViewModel extends _$IndexViewModel {
@override
IndexState build() {
return const IndexState(selectedActivityTab: ActivityStatus.inProgress);
}
void selectActivityTab(ActivityStatus status) {
state = state.copyWith(selectedActivityTab: status);
}
}
// 計算 Provider - 基於其他 Provider 的衍生狀態
@riverpod
List<Activity> filteredActivities(Ref ref) {
final selectedTab = ref.watch(indexViewModelProvider).selectedActivityTab;
final activitiesAsync = ref.watch(getActivitiesUseCaseProvider);
return activitiesAsync.when(
data: (activities) => activities
.where((activity) => activity.status == selectedTab)
.toList(),
loading: () => [], // 返回空列表,簡化 UI 處理
error: (_, __) => [], // 返回空列表,簡化 UI 處理
);
}
⚖️ 衍生 Provider 的 when 處理策略:
- 優點:UI 層拿到的永遠是
List<Activity>
型別,可以直接使用,非常方便- 權衡:UI 層無法直接得知底層的 loading 或 error 狀態
- 建議:如果 UI 需要根據載入或錯誤狀態顯示不同畫面(如 Loading 轉圈或錯誤提示),應直接 watch 底層的
activitiesAsync
📱 在 Widget 中的使用:
class IndexScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final activities = ref.watch(filteredActivitiesProvider);
final viewModel = ref.watch(indexViewModelProvider);
return Column(
children: [
TabBar(onTap: (tab) =>
ref.read(indexViewModelProvider.notifier).selectActivityTab(tab)
),
Expanded(child: ActivityList(activities: activities)),
],
);
}
}
透過 Riverpod,我們實現了 Day 1 設計的依賴反轉原則:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Presentation │───→│ Domain │←───│ Data │
│ IndexScreen │ │ GetActivities │ │ IndexRepository │
│ (ConsumerWidget)│ │ UseCase │ │ Impl │
└─────────────────┘ └─────────────────┘ └─────────────────┘
🎯 架構優勢:
@riverpod
註解簡化架構實作今天我們看到了 Riverpod 2.0 如何完美地實現 Day 1 設計的 Clean Architecture。關鍵重點:
@riverpod
註解簡化了架構實作Riverpod 2.0 不只是狀態管理工具,更是實現 Clean Architecture 的最佳伙伴。它讓我們在 Day 1 描繪的架構藍圖,真正在「Crew Up!」專案中運作起來。
明天,我們將深入探討 Repository 模式,學習如何在 Clean Architecture 中實作資料存取層,以及如何透過 Riverpod 2.0 實現依賴注入,建立可測試、可擴展的資料架構基礎。
期待與您在 Day 7 相見!