iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
Mobile Development

我的 Flutter 進化論:30 天打造「Crew Up!」的架構之旅系列 第 6

Day 6 - Riverpod 2.0 實戰攻略:從架構設計到效能優化的完整指南

  • 分享至 

  • xImage
  •  

大家好,歡迎來到第六天!在 Day 5,我們建立了完整的導航架構。今天,我們將深入探討 Flutter 世界的核心議題:狀態管理

狀態管理可以說是 Flutter App 的「神經系統」,負責協調各個元件之間的溝通和資料流動。在「Crew Up!」專案中,我們不僅要了解如何使用狀態管理工具,更要理解其背後的設計哲學。

狀態管理與 Clean Architecture

還記得 Day 1 我們選擇的 Clean Architecture 嗎?狀態管理正是實現這個架構的關鍵工具。Riverpod 2.0 不只是狀態管理工具,更是實現 Day 1 設計的依賴反轉原則的最佳伙伴:

  • Domain Layer:定義 Repository 介面和 UseCase
  • Data Layer:實作 Repository,通過 Riverpod 注入
  • Presentation Layer:使用 Riverpod 管理 UI 狀態

各種狀態管理方案的特色

在 Flutter 社群中,狀態管理方案的選擇一直是個熱門話題。各種方案都有其特色:

  • setState: Flutter 最原始的狀態管理,簡單易懂,適合局部、簡單的狀態
  • Provider: 長期以來很受歡迎的方案,基於 InheritedWidget,表現不錯,但有一些使用上的挑戰
  • BLoC: 在大型專案中很受歡迎,結構清晰、可預測性高,但需要比較多的樣板程式碼
  • Riverpod: 由 Provider 的作者親自打造,被譽為「Provider 2.0」,完美契合 Clean Architecture

為什麼我們選擇 Riverpod 2.0?

在「Crew Up!」專案中,考慮到 Day 1 設計的 Clean Architecture,我們選擇 Riverpod 2.0 是因為:

  • 編譯時期檢查:Riverpod 在編譯時期就能發現大部分錯誤,避免執行時期閃退
  • 完全脫離 Context 束縛:Provider 不依賴 BuildContext,可以在任何地方存取,完美實現 Day 1 的依賴反轉原則
  • 程式碼自動產生@riverpod 註解大幅減少樣板程式碼
  • 與 Clean Architecture 完美契合:自然地支援三層架構的依賴注入

Riverpod 1.0 vs 2.0:從手工到自動化的演進

從過去實際專案經驗來看,Riverpod 2.0 的引入帶來了顯著的開發體驗提升,因此這一次我們在 Crew Up 專案中選擇統一使用 2.0:

🔧 Riverpod 1.0 的挑戰:

  • 手寫樣板程式碼:需要手動建立 final xxxProvider = Provider<Xxx>((ref) => ...)
  • 命名不一致問題:團隊成員可能使用不同的命名慣例,造成維護困擾
  • autoDispose 與 family 寫法複雜:容易出現人為錯誤,特別是在複雜的狀態管理場景
  • 型別安全性較低:缺乏專屬的 Ref 型別,IDE 智慧補全有限

⚡ Riverpod 2.0 的優勢:

  • 自動程式碼產生:透過 riverpod_annotation + riverpod_generator@riverpod 註解自動產生最佳化的 Provider
  • 統一且一致的寫法:自動處理 autoDisposefamily 模式,消除人為錯誤
  • 專屬 Ref 型別:每個 Provider 都有專屬的 Ref 型別,IDE 智慧補全與跳轉更精準
  • 開發效率提升 30-50%:新增或修改 Provider 的時間大幅縮短
  • 零風險遷移:可保留既有的 StateNotifier 架構,逐步切換

📊 實際專案效益:
在我們團隊的重構專案中,成功重構了 70+ providers,不僅統一了寫法,還建立了完整的 CI 流程與品質保證機制。UI 層面影響極小,主要只需更名呼叫點(如 xxxProviderFamily(...)xxxProvider(...)),ConsumerWidget 的用法完全不變。

⚖️ 效能考量與權衡:

雖然 @riverpod 程式碼產生帶來便利,但也有需要考量的面向:

  • 狀態控制精準度:1.0 的 StateNotifier 可以更精準地控制狀態更新時機,減少不必要的重建
  • Provider 自動釋放:2.0 預設的 autoDispose 機制雖然方便,但在複雜狀態依賴關係中可能造成預期外的重建
  • 程式碼產生開銷:build_runner 的執行時間在大型專案中可能影響開發體驗

🎯 為什麼 Crew Up 專案適合 Riverpod 2.0?

考慮到我們專案的特性,Riverpod 2.0 是最佳選擇:

  1. 中大型社交應用:Crew Up 預期有複雜的使用者互動、活動管理、即時通知等功能,需要穩固的狀態管理基礎
  2. 多人團隊開發:統一的程式碼產生機制能確保不同開發者寫出一致性的程式碼,降低 code review 成本
  3. 長期維護需求:社交應用的功能會持續擴展,2.0 的依賴注入機制讓我們能輕鬆添加新功能而不破壞既有架構
  4. 測試覆蓋要求:Clean Architecture + Riverpod 2.0 的組合讓單元測試變得更簡單,符合我們的品質要求
  5. 新專案優勢:不像既有專案需要考慮遷移成本,我們可以從一開始就享受 2.0 的所有優勢

💡 重要提醒:技術選擇沒有銀彈。如果您的專案是小型 MVP、團隊對 Riverpod 不熟悉、或需要極致的效能控制,可能 setState 或 1.0 的 StateNotifier 會是更好的選擇。選擇應該基於團隊能力、專案規模和長期目標。

狀態管理方案比較:Flutter Engineering 評估標準

在選擇狀態管理方案時,根據 Flutter Engineering 的建議,我們不應僅憑受歡迎程度做決定,而應考慮以下關鍵因素:

📋 選擇考量標準:

  • 活躍的開源儲存庫:活躍的問題解決和社群參與
  • 測試和測試覆蓋率:完整的測試和覆蓋率
  • 單一職責:遵循 Unix 哲學,專注於一件事
  • 活躍使用者數量:GitHub 星星、pub.dev 上的讚、社群討論
  • 文件完整性:全面文件、範例程式碼或範例應用程式
  • 良好文件的 API:清晰且良好文件的 API 節省時間並減少學習曲線

⭐ 基於 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 2.0 的工具箱

Riverpod 2.0 透過 @riverpod 註解自動產生不同類型的 Provider:

  • 函數式 Provider:用 @riverpod 修飾回傳值的函數,適合提供 Repository 實例等依賴
  • Future Provider:用 @riverpod 修飾 Future<T> 函數,自動產生 AutoDisposeFutureProvider<T>
  • Notifier Provider:用 @riverpod 修飾繼承 _$XXXNotifier 的類別,管理複雜狀態
  • 衍生狀態:任何 Provider 都可透過 ref.watch() 依賴其他 Provider 來計算衍生狀態

1. Data Layer:Repository 的依賴注入

首先,我們在 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();
}

2. Domain Layer:UseCase 的實作

// 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 中使用

3. Presentation Layer: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),
      ),
    );
  }
}

4. 複雜狀態管理:NotifierProvider

📋 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);
  }
}

5. 計算式狀態:衍生 Provider

// 計算 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)),
      ],
    );
  }
}

Clean Architecture 的完美實現

透過 Riverpod,我們實現了 Day 1 設計的依賴反轉原則:

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│ Presentation    │───→│ Domain          │←───│ Data            │
│ IndexScreen     │    │ GetActivities   │    │ IndexRepository │
│ (ConsumerWidget)│    │ UseCase         │    │ Impl            │
└─────────────────┘    └─────────────────┘    └─────────────────┘

🎯 架構優勢:

  • 依賴反轉:所有依賴都指向 Domain 層,完全符合 Day 1 的 Clean Architecture 原則
  • 分層清晰:Data、Domain、Presentation 各司其職
  • 無 Context 束縛:業務邏輯層徹底與 UI 解耦,提升可測試性
  • 自動依賴注入@riverpod 註解簡化架構實作

結語

今天我們看到了 Riverpod 2.0 如何完美地實現 Day 1 設計的 Clean Architecture。關鍵重點:

  • 依賴反轉的實現:通過 Riverpod 讓所有依賴指向 Domain 層
  • 三層架構的落地:Data、Domain、Presentation 各司其職
  • 無 Context 設計:業務邏輯層徹底與 UI 解耦
  • 自動依賴注入@riverpod 註解簡化了架構實作

Riverpod 2.0 不只是狀態管理工具,更是實現 Clean Architecture 的最佳伙伴。它讓我們在 Day 1 描繪的架構藍圖,真正在「Crew Up!」專案中運作起來。

下一步

明天,我們將深入探討 Repository 模式,學習如何在 Clean Architecture 中實作資料存取層,以及如何透過 Riverpod 2.0 實現依賴注入,建立可測試、可擴展的資料架構基礎。

期待與您在 Day 7 相見!


📋 相關資源

📝 專案資訊

  • 專案名稱: Crew Up!
  • 開發日誌: Day 6 - Riverpod 2.0 實戰攻略:從架構設計到效能優化的完整指南
  • 文章日期: 2025-09-20
  • 技術棧: Flutter, Riverpod 2.0, 狀態管理, Code Generation

上一篇
Day 5 - 導航不再迷路:go_router 實戰心得與架構演進
下一篇
Day 7 - Repository 模式:軟體架構的自由基石
系列文
我的 Flutter 進化論:30 天打造「Crew Up!」的架構之旅10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言