Stories

Detail Return Return

Flutter 工程構架設計(MVVM + Repository) - Stories Detail

認真對待每時、每刻每一件事,把握當下、立即去做。

移動應用開發領域的技術演進正持續推動着跨平台解決方案的創新。在 Android 與 iOS 等多平台並存的現狀下,傳統原生開發面臨‌代碼複用率低‌和‌開發效率瓶頸‌等核心挑戰。Flutter 作為 Google 推出的現代化 UI 工具包,通過‌自繪引擎‌和‌響應式框架‌實現了真正的跨平台一致性,其‌"一次編寫,處處運行"‌的理念已在全球範圍內得到驗證——根據往年 Dart 開發者調研,採用 Flutter 的企業項目平均縮短了40%左右的開發週期。本文基於 ‌MVVM+Repository ‌架構模式,系統闡述 Flutter 在工程化實踐中的解決方案。

這次公司在新項目技術再次選型的前景下,讓我對 Flutter 做一次技術構架分享。為了把 Flutter 説清楚,如何去做架構企業級項目,項目架構中應該包含哪些技術點,我做了下面結構性的技術總結,前面部分我會針對技術、工具鏈生態做一個系統解析,最後一部分詳細根據業務點來闡述 MVVM+Repository ‌架構。

特別地,本文方案融合了筆者在2022年主導公司的‌企業級移動應用重構經驗(Native + KMM + React 架構)‌,其中對狀態管理、模塊化解耦等關鍵問題的解決路徑,均在本架構中得到延續與升級。通過完整的代碼示例與架構圖解進行解析。

當然,在互相學習過程中歡迎指出其中的不足和改進意見,後續有時間會對基礎架構一些延續的東西我也會陸續補充進來。我們先看看基礎項目結構的定義,有個大概瞭解再往下看。

# 項目目錄結構定義
pubassistant/
├── android/                                # Android 平台代碼
├── ios/                                    # iOS 平台代碼
├── assets/                                 # 靜態資源
│   ├── images/                             # 圖片資源
│   ├── fonts/                              # 字體文件
│   └── json/                               # 本地JSON文件
├── lib/                                    # Flutter 源代碼
│   ├── generated/                          # 資源管理生成器
│   │   └── assets.dart                     # assets
│   ├── src/
│   │   ├── core/                           # 核心層
│   │   │   ├── constants/                  # 常量
│   │   │   │   ├── app_constants.dart      # 應用常量
│   │   │   │   ├── app_strings.dart        # 字符串常量
│   │   │   │   ├── app_layouts.dart        # 佈局尺寸常量
│   │   │   │   └── app_colors.dart         # 顏色常量
│   │   │   ├── di/                         # 依賴注入配置核心文件
│   │   │   │   └── injector.dart           # GetIt
│   │   │   ├── routes/                     # 路由配置
│   │   │   │   ├── app_pages.dart          # 頁面路由表
│   │   │   │   └── app_router.dart         # 路由生成器
│   │   │   ├── theme/                      # 主題配置
│   │   │   │   ├── app_theme.dart          # 主題配置
│   │   │   │   └── text_styles.dart        # 文本樣式規範
│   │   │   ├── network/                    # 網絡層封裝
│   │   │   │   ├── dio_client.dart         # Dio 實例配置
│   │   │   │   ├── exceptions/             # 自定義異常類
│   │   │   │   └── interceptors/           # 攔截器(日誌、Token刷新) 
│   │   │   ├── database/                   # 數據庫層封裝
│   │   │   └── utils/                      # 工具類
│   │   │       └── storage_util.dart       # 存儲工具
│   │   ├── features/                       # 業務功能模塊劃分層
│   │   │   ├── data/                       # 數據層:聚焦數據獲取與存儲邏輯
│   │   │   │   ├── models/                     # 數據模型
│   │   │   │   ├── repositories/               # 數據倉庫
│   │   │   │   └── services/                   # 數據服務(API接口)
│   │   │   ├── domain/                     # 業務層:處理業務規則與邏輯流轉,如數據驗證、流程編排、領域模型轉換
│   │   │   │   ├── entities/                   # 業務實體
│   │   │   │   ├── repositories/               # 抽象倉庫接口
│   │   │   │   └── use_cases/                  # 業務邏輯用例
│   │   │   └── presentation/               # 表現層
│   │   │       ├── pages/                      # UI 頁面
│   │   │       ├── widgets/                    # 模塊內複用組件
│   │   │       ├── view_models/                # 視圖模型
│   │   │       ├── router/                     # 模塊獨立路由
│   │   │       └── state/                      # 狀態管理
│   │   └── config/                         # 環境配置
│   │       └── app_config.dart
│   └── main.dart                           # 應用入口
├── test/                                   # 測試目錄
├── scripts/                                # 構建/部署腳本
├── environments/                           # 環境配置文件
│   ├── dev.env
│   ├── staging.env
│   └── prod.env
└── pubspec.yaml                            # 依賴管理

一. 環境配置

1. 環境配置的核心作用

  • 隔離環境,分離開發/演示/生產環境的配置
  • 敏感信息保護‌:避免硬編碼敏感 URL 到源碼中
  • 動態加載‌:通過構建腳本自動注入對應配置

2. 創建環境配置文件(environments/目錄)

這裏一般配置一個開發環境和一個生產環境就行了,目前我們公司涉及到大量客户演示,這裏增加一個演示環境,總的來説按需配置。

├── environments/                           # 環境配置文件
│   ├── dev.env
│   ├── staging.env
│   └── prod.env

dev.env 配置詳情示例:

API_BASE_URL=https://api.dev.example.com
ENV_NAME=Development
ENABLE_DEBUG_LOGS=true

3. 添加 flutter_dotenv 依賴

dependencies:
  flutter_dotenv: ^5.2.1

4. 創建配置加載器

配置文件路徑:lib/src/config/env_loader.dart

// 創建配置加載器
class EnvLoader {
  static Future<void> load() async {
    const env = String.fromEnvironment("ENV", defaultValue: 'dev');
    await dotenv.load(fileName: 'environments/$env.env');
  }

  static String get apiBaseUrl => dotenv.get('API_BASE_URL');
  static String get envName => dotenv.get('ENV_NAME');
  static bool get enableDebugLogs => dotenv.get('ENABLE_DEBUG_LOGS') == 'true';
}

5. main.dart 中初始化環境

void main() async {
  // 初始化環境配置
  await EnvLoader.load();
  runApp(const MyApp());
}

6. 啓動和打包時指定環境

6.1 調試開發環境

# 1. 命令啓動開發環境
flutter run --dart-define=ENV=dev
  
# 2. 配置IDE運行參數
# 在IDE的 "Run"->"Edit Configurations" 中:  
  - 找到 Flutter 運行配置
  - 在"Additional arguments"添加:--dart-define=ENV=dev

6.2 正式環境打包

Android APK:

# 生產環境
flutter build apk --dart-define=ENV=prod
# 演示環境
flutter build apk --dart-define=ENV=staging

iOS IPA:

  • 命令行打包:

    # 生產環境
    flutter build ipa --dart-define=ENV=prod --release
    # 演示環境
    flutter build ipa --dart-define=ENV=staging --release
    
  • Xcode 配置:

    打開 ios.Runner.xcworkspace,選擇 Target Build Settings,添加 DART_DEFINES 環境變量 DART_DEFINES=ENV=prod

7. 使用示例

Text(EnvLoader.envName) 

二. 靜態資源配置

1. 資源目錄結構設計

├── assets/                                 # 靜態資源
│   ├── images/                             # 圖片資源
│   ├── fonts/                              # 字體文件
│   └── json/                               # 本地JSON文件

2. pubspec.yaml 配置

flutter:
  assets:
    - assets/images/
    - assets/json/
  fonts:
    - family: Rbt
      fonts:
        - asset: assets/fonts/Rbt-Framework.ttf

3. 資源圖片引用類生成

這裏是自定義工具實現示例,其實我們可以直接使用通過資源代碼生成工具實現自動生成的 generated/assets.dart 工具類實現文件。該機制本質上是通過元編程手段,將文件系統的資源組織結構轉化為類型安全的編程接口,屬於 Flutter 現代化開發工具鏈的典型實踐,後面會具體介紹。

// lib/src/core/constants/assets_constants.dart
class AppAssets {
  static const String framework = 'assets/images/framework/home_head_image.jpg';
}

// 使用示例
Image.asset(AppAssets.framework)

4. 字體資源使用

全局應用:

MaterialApp(
  theme: ThemeData(
    fontFamily: 'Rbt',  // 使用聲明的字體家族名
  ),
);

局部應用:

Text(
  '自定義字體',
  style: TextStyle(
    fontFamily: 'Rbt',
    fontWeight: FontWeight.bold,  // 匹配配置的字重
  ),
);

5. json 文件使用

推薦使用 json_serializable、json_annotation、build_runner 庫,進行一個通用的封裝,這部分會在後續框架項目中進行開源,歡迎 star。

三. 資源管理生成器

在 Flutter 項目中,generated/assets.dart 是一個自動生成的文件,主要用於‌資源管理的代碼化‌和‌開發效率優化‌。以下是其核心作用與生成邏輯:

1. 核心作用

1)資源路徑的靜態化訪問

assets 目錄下的資源(如圖片、字體)轉換為 Dart 常量,避免手動輸入路徑字符串,減少拼寫錯誤。

// 示例:通過生成的常量訪問圖片
Image.asset(Assets.images.logo); 
// 替代 
Image.asset('assets/images/logo.png')

2)類型安全與智能提示

資源名稱通過代碼生成器映射為強類型屬性,IDE 可提供自動補全,提升開發體驗。

3)‌多分辨率資源適配

自動處理不同分辨率的資源文件(如 logo@2x.png),生成統一的訪問接口。

2. 自動生成的觸發機制

1)‌依賴插件

通常由 flutter_genflutter_generate_assets 等插件實現,這些插件基於 Dart 的 build_runner 工具鏈。

2)‌配置文件驅動

pubspec.yaml 中聲明資源後,插件會監聽文件變化並自動生成代碼:

flutter:
  assets:
    - assets/images/

3)編譯時生成

執行 flutter pub run build_runner build 命令觸發生成,結果保存在 lib/generated/ 目錄下。

3. 優勢對比手動管理

特性 手動管理 自動生成 (generated/assets.dart)
路徑準確性 易出錯 100% 準確
重構友好性 需全局搜索替換 自動同步修改
多語言支持 需額外工具 可整合國際化資源

4. 高級應用場景

1)與國際化結合

通過註解生成多語言資源的訪問代碼,例如 Assets.translations.homeTitle

2)‌自定義資源類型

擴展支持 JSON、音頻等非圖片資源,生成對應的解析方法。

四. 常量配置集

常用常量配置集合結構參考如下,當然我們在開發過程中應該根據具體實際情況進行增加和修改。

core/                           # 核心層
│   │   │   ├── constants/                  # 常量
│   │   │   │   ├── app_constants.dart      # 應用常量
│   │   │   │   ├── app_strings.dart        # 字符串常量
│   │   │   │   ├── app_layouts.dart        # 佈局尺寸常量
│   │   │   │   └── app_colors.dart         # 顏色常量
class AppConstants {
  // 應用基礎信息
  static const String appName = 'pubassistant';
  static const String appVersion = '1.0.0.0';
  static const int appBuildNumber = 1000;
}

五. Theme 主題配置

Theme 主題系統的核心文件,用於集中管理應用的視覺樣式和文本風格。

1. 全局主題配置

功能‌:定義應用的整體視覺風格,包括顏色、組件樣式、亮度模式等,通過 ThemeData 類實現統一管理。典型內容:

import 'package:flutter/material.dart';
import 'text_styles.dart';  // 關聯文本樣式

class AppTheme {
  // 明亮主題
  static ThemeData lightTheme = ThemeData(
    colorScheme: ColorScheme.light(
      primary: Colors.blueAccent,
      secondary: Colors.green,
    ),
    appBarTheme: AppBarTheme(
      backgroundColor: Colors.blueAccent,
      titleTextStyle: TextStyles.headlineMedium,
    ),
    buttonTheme: ButtonThemeData(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
    ),
    textTheme: TextTheme(
      displayLarge: TextStyles.displayLarge,  // 引用文本樣式
      bodyMedium: TextStyles.bodyMedium,
    ),
  );

  // 黑暗主題
  static ThemeData darkTheme = ThemeData.dark().copyWith(
    colorScheme: ColorScheme.dark(
      primary: Colors.indigo,
      secondary: Colors.tealAccent,
    ),
  );
}

關鍵點‌:

  • 使用 ColorScheme 定義主色、輔色等配色方案;
  • 通過 appBarThemebuttonTheme 等定製組件樣式;
  • 引用 text_styles.dart 中的文本樣式保持一致性;

2. 文本樣式規範

功能‌:集中管理所有文本樣式(如標題、正文、按鈕文字等),避免散落在各處重複定義。典型內容‌:

class TextStyles {
  // 標題樣式
  static const TextStyle displayLarge = TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Colors.black87,
  );

  // 正文字體
  static const TextStyle bodyMedium = TextStyle(
    fontSize: 16,
    height: 1.5,
    color: Color(0xFF424242),
  );

  // 按鈕文字
  static const TextStyle buttonLabel = TextStyle(
    fontSize: 14,
    fontWeight: FontWeight.w600,
    letterSpacing: 0.5
  );
}

關鍵點‌:

  • 使用 const 定義靜態樣式提升性能;
  • 包含字體大小、顏色、字重、行高等屬性;
  • 支持自定義字體(需在 pubspec.yaml 配置)。

3. 使用方式

main.dart 中應用主題‌:

MaterialApp(
  theme: AppTheme.lightTheme,  // 使用預定義主題
  darkTheme: AppTheme.darkTheme,
  home: MyApp(),
);

在組件中調用文本樣式‌:

Text('Hello', style: TextStyles.displayLarge);

4. 設計建議

  • 分層管理‌:將顏色、間距等基礎變量單獨提取(如 colors.dart),這一點就是常量配置集中提到的;
  • 擴展性‌:通過 copyWith 方法局部覆蓋主題;
  • 一致性‌:避免直接在組件內硬編碼樣式;

六. 網絡請求方案

dio 是一個強大的 HTTP 網絡請求庫,支持全局配置、Restful API、FormData、攔截器、 請求取消、Cookie 管理、文件上傳/下載、超時、自定義適配器、轉換器等。

項目裏通過封裝設計 http_exception、http_interceptor、http_options、http_request 類,適應於大型項目的開發應用。

七. 數據存儲方案

1. 偏好設置

推薦 shared_preferences 方案,項目裏進行了一層應用封裝。

2. 數據庫方案設計

2.1 核心設計原理

數據庫封裝採用了分層架構設計,主要由三個部分組成:基礎提供者類(DbBaseProvider)、數據庫助手類(DbHelper)和具體業務提供者(UserDbProvider)。

  1. 單一職責原則‌:每個類都有明確的職責劃分;
    • DbBaseProvider:提供基礎表操作能力;
    • DbHelper:管理數據庫連接和初始化;
    • UserDbProvider:實現具體業務表操作;
  2. 模板方法模式‌:DbBaseProvider 中定義了抽象方法(getTableName, createTableString),要求子類必須實現;
  3. 單例模式‌:DbHelper 採用單例確保全局只有一個數據庫連接;
  4. 懶加載‌:數據庫連接在首次使用時才初始化;

2.2 封裝優點

  1. 結構清晰‌:分層明確,職責分離;

  2. 複用性強‌:基礎功能封裝在父類,子類只需關注業務表結構;

  3. ‌性能優化:

    • 單例模式避免重複創建連接;
    • 表存在檢查避免重複建表;
  4. 擴展性好‌:新增表只需繼承 DbBaseProvider;

  5. 線程安全‌:所有操作都是異步的;

2.3 常見問題和改進注意點

注意事項:

  1. 數據庫版本管理前期設計不足‌:DbHelper 中雖然有 version 字段但沒有用於升級邏輯,缺少數據庫升級遷移機制。增強的版本管理‌:添加了 onUpgrade 和 onDowngrade 回調、每個 Provider 可定義升級 SQL;
  2. 事務支持不足‌:提供事務操作方法封裝;
  3. 錯誤處理缺失‌:沒有統一的對數據庫操作異常的捕獲和處理機制;
  4. SQL 注入風險‌:UserDbProvider 中直接拼接 SQL 字符串,部分 SQL 語句直接拼接字符串參數,使用參數化查詢防止 SQL 注入;
  5. 性能優化空間‌:數據庫連接沒有關閉機制;

最佳實踐建議:

  1. 增加模型層‌:建議添加User模型類,替代直接使用Map;
  2. 使用ORM框架‌:考慮使用floor或moor等Dart ORM框架;
  3. 日誌記錄‌:添加數據庫操作日誌;
  4. 備份機制‌:實現定期備份功能;
  5. 性能監控‌:添加查詢性能統計;

總結:封裝遵循了基本的軟件設計原則,提供了清晰的擴展接口。主要改進空間在於錯誤處理、類型安全和版本管理方面。通過引入模型層和 ORM 框架可以進一步提升代碼質量和開發效率。

八. 狀態管理

InheritedWidget 提供了在 Widget 樹中從上往下共享數據的能力;

全局事件總線(Event Bus)實現跨頁面、跨組件的通信,進行數據傳遞與交互。具體的實現封裝結合項目;

ChangeNotifier(provider) + ValueNotifier;

BLoC(推薦 bloc + flutter_bloc + Cubit);

九. 路由管理

在 Flutter 項目中,go_router 和 auto_route 都是優秀的第三方路由庫,但它們的定位和特性有所不同。以下是兩者的對比分析及選型建議:

1. 核心特性對比

go_router:

  • 基於 URL 的路由管理,支持深度鏈接和 Web 兼容性。
  • 提供路由守衞(如登錄驗證、權限控制)和重定向功能。
  • 支持嵌套路由和動態參數解析,語法簡潔。
  • 與 Navigator API 兼容,適合需要 Web 支持或複雜路由邏輯的項目。

auto_route:

  • 基於代碼生成的路由方案,通過註解自動生成路由代碼。
  • 強類型路由參數,編譯時檢查減少運行時錯誤。
  • 支持嵌套導航和自定義過渡動畫。
  • 適合追求類型安全和減少樣板代碼的團隊。

2. 性能與複雜度

  • go_router‌:運行時配置路由,靈活性高但可能增加運行時開銷。
  • auto_route‌:編譯時生成代碼,性能更優但需依賴代碼生成步驟。

3. 選型建議

選擇 go_router 的場景‌:

  • 需要深度鏈接或 Web 支持。
  • 項目中有複雜路由攔截需求(如動態權限控制)。
  • 團隊偏好聲明式配置而非代碼生成。

選擇 auto_route 的場景‌:

  • 追求類型安全和編譯時檢查。
  • 需要減少手動編寫路由樣板代碼。
  • 項目已使用其他代碼生成工具(如freezed)。

4. 混合使用方案

對於大型項目,可結合兩者優勢:

  • 使用 auto_route 管理基礎頁面路由;
  • 通過 go_router 處理需要動態攔截或 Web 集成的特殊路由;

建議根據團隊技術棧和項目需求(如是否跨平台、是否需要強類型支持)做出選擇。

5. go_router 示例

final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _homeNavigatorKey = GlobalKey<NavigatorState>();
final _residentNavigatorKey = GlobalKey<NavigatorState>();
final _mineNavigatorKey = GlobalKey<NavigatorState>();

class AppRouter {
  final GoRouter router = GoRouter(
    initialLocation: RoutePaths.login,
    navigatorKey: _rootNavigatorKey,
    redirect: (context, state) async {
      final authViewModel = Provider.of<AuthViewModel>(context, listen: false);
      final isLoggedIn = await authViewModel.isLoggedIn;
      // 非登錄保護邏輯
      if (!isLoggedIn && state.matchedLocation != RoutePaths.login) {
        return RoutePaths.login;
      }
      // 已登錄狀態下的路徑修正
      if (isLoggedIn && state.matchedLocation == RoutePaths.login) {
        return RoutePaths.home;
      }
      return null;
    },
    routes: <RouteBase>[
      GoRoute(
          path:  RoutePaths.login,
          builder: (context, state) => const LoginPage()
      ),
      StatefulShellRoute.indexedStack(
        builder: (context, state, navigationShell) =>
            MainPage(navigationShell: navigationShell),
        branches: [
          StatefulShellBranch(
            navigatorKey: _homeNavigatorKey,
            routes: [
              GoRoute(
                path:  RoutePaths.home,
                builder: (context, state) => const HomePage(),
                routes: [
                  // 首頁子路由
                ],
              ),
            ],
          ),
          StatefulShellBranch(
            navigatorKey: _residentNavigatorKey,
            routes: [
              GoRoute(
                path:  RoutePaths.resident,
                builder: (context, state) => const ResidentPage(),
              ),
            ],
          ),
          StatefulShellBranch(
            navigatorKey: _mineNavigatorKey,
            routes: [
              GoRoute(
                path:  RoutePaths.mine,
                builder: (context, state) => const MinePage(),
              ),
            ],
          ),
        ],
      ),
    ],
  );
}

十. Flutter MVVM + Repository 架構

以下是 Flutter MVVM + Repository 架構的業務示例解析。

1. 架構結構和各層職責

1.1 目錄架構結構

├── features/                 # 業務功能模塊劃分層
│   ├── data/                     # 數據層:聚焦數據獲取與存儲邏輯
│   │   ├── models/                     # 數據模型
│   │   ├── repositories/               # 數據倉庫
│   │   └── services/                   # 數據服務(API接口)
│   ├── domain/                     # 業務層:處理業務規則與邏輯流轉,如數據驗證、流程編排、領域模型轉換
│   │   ├── entities/                   # 業務實體
│   │   ├── repositories/               # 抽象倉庫接口
│   │   └── use_cases/                  # 業務邏輯用例
│   └── presentation/               # 表現層
│       ├── pages/                      # UI 頁面
│       ├── widgets/                    # 模塊內複用組件
│       ├── view_models/                # 視圖模型
│       ├── router/                     # 模塊獨立路由
└──     └── state/                      # 狀態管理

1.2 MVVM + Repository 架構層職責説明

Model 層‌:

  • data/models:數據模型(DTO)
  • domain/entities:業務實體
  • data/services:數據源實現(SQLite/API)

ViewModel 層‌:調用 UseCase、處理業務邏輯、管理 UI 狀態。

  • presentation/viewmodels
    

View 層‌:純 UI 展示、通過 Consumer 監聽 ViewModel。

  • presentation/pages
    

Repository 層‌:

  • domain/repositories:抽象接口。
  • data/repositories:具體實現。

1.3 ViewModel 層解析

在 Flutter 功能優先結構中融入 ViewModel 層時,核心區別如下:

1)ViewModel 層的定位與實現

在現有結構中,presentation/state/ 目錄即 ViewModel 層的天然位置,用於管理 UI 狀態和業務邏輯協調。ViewModel 在 MVVM 架構中主要承擔以下角色:

  • 狀態管理‌:負責管理應用的狀態,包括 UI 狀態(如加載中、錯誤)和業務數據狀態(如用户信息)。

  • 業務邏輯處理‌:封裝業務邏輯,包括數據獲取、轉換和處理。

  • 數據層交互‌:通過 UseCase 或 Repository 與數據層交互,獲取或存儲數據。

典型實現方式:

class UserViewModel with ChangeNotifier {
  final GetUserByIdUseCase _getUserByIdUseCase;
  UserEntity? _userEntity;
  bool _isLoading = false;
  String? _error;

  UserEntity? get user => _userEntity;
  bool get isLoading => _isLoading;
  String? get error => _error;

  UserViewModel(this._getUserByIdUseCase);

  Future<void> fetchUser(String userId) async {
    _isLoading = true;
    notifyListeners();

    try {
      _userEntity = await _getUserByIdUseCase.execute(userId);
      _error = null;
    } catch(e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

此處 presentation/state/ 存放 ViewModel,通過 use_cases 調用領域邏輯。

2)添加 ViewModel 層的優勢

職責分離‌,解決UI與業務邏輯耦合問題。

  • View:純 UI 渲染 (pages/, widgets/)
  • ViewModel:狀態管理/邏輯協調 (state/)
  • Model:數據操作 (repositories/, services/)

可測試性提升‌,ViewModel 獨立於 Widget 樹,可直接進行單元測試。

test('UserViewModel should emit loading state', () {
  final vm = UserViewModel(mockUseCase);
  vm.fetchUser('123');
  expect(vm.state, ViewState.isLoading);
});

狀態生命週期管理‌,自動處理頁面銷燬時的資源釋放,避免內存泄漏。

跨組件狀態共享‌,通過 Provider/Riverpod 實現多個 Widget 訪問同一狀態源。

3)不加 ViewModel 層的缺陷

邏輯臃腫‌,業務代碼侵入 Widget,導致萬行 StatefulWidget 地獄。

// 反例:業務邏輯混入UI層
class LoginPage extends StatefulWidget {
  Future<void> _login() async {
    // API調用+狀態管理+導航跳轉
  }
}

測試困難‌,需啓動完整 Widget 樹測試基礎邏輯。

狀態分散‌,相同業務狀態可能被重複實現於不同Widge。

4)關鍵實踐建議

層級交互規範‌,遵循單向依賴:外層→內層

View[Widget] -->|監聽| ViewModel
ViewModel -->|調用| UseCase
UseCase -->|依賴抽象| Repository
Repository -->|組合| DataSource

狀態管理選型

  • 中小項目:ChangeNotifier + Provider
  • 大型項目:Riverpod/Bloc + Freezed

模塊化擴展‌,保持各功能模塊內聚性

2. 業務調用場景(獲取用户信息)

假設我們需要通過 API 獲取用户數據,並進行業務邏輯處理(如數據驗證、模型轉換)。

2.1 數據層(data/)

目的‌:聚焦數據獲取與存儲邏輯,實現具體的數據獲取邏輯(如網絡請求、數據庫操作)。

1)/data/models/user_model.dart
// data/models/user_model.dart
// 數據模型:對應 API 返回的 JSON 結構(含序列化註解)
@JsonSerializable()
class UserModel {
  @JsonKey(name: 'user_id') 
  final String id;
  final String username;
  final int age;

  UserModel({required this.id, required this.username, required this.age});
  
  factory UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
}
2)data/services/user_api_service.dart
// data/services/user_api_service.dart
// 數據服務:與 API 交互(具體實現)
class UserApiService {
  final Dio dio;

  UserApiService(this.dio);

  Future<UserModel> fetchUser(String userId) async {
    final response = await dio.get('/users/$userId');
    return UserModel.fromJson(response.data);
  }
}
3)data/repositories/user_repository_impl.dart
  1. 組合多個數據源。
  2. DTO 與 Entity 轉換。
// data/repositories/user_repository_impl.dart
// 倉庫實現:將數據轉換為業務實體(實現 domain 層的抽象接口)
class UserRepositoryImpl implements UserRepository {
  final UserApiService apiService;

  UserRepositoryImpl(this.apiService);

  @override
  Future<UserEntity> getUserById(String userId) async {
    final userModel = await apiService.fetchUser(userId);
    return UserEntity(
      id: userModel.id,
      name: userModel.username, // 字段名轉換(API username → 業務 name)
      age: userModel.age,
    );
  }
}

2.2 業務層(domain/)

目的‌:處理業務規則與核心邏輯流轉、抽象接口,如數據驗證、流程編排、領域模型轉換(與具體技術無關)。

1)domain/entities/user_entity.dart
// domain/entities/user_entity.dart
// 業務實體:純 Dart 對象,僅包含業務核心屬性(無 JSON 註解)
class UserEntity {
  final String id;
  final String name;
  final int age;

  UserEntity({required this.id, required this.name, required this.age});
  
  // 業務邏輯方法(如年齡驗證)
  bool isAdult() => age >= 18;
}
2)domain/repositories/user_repository.dart
// domain/repositories/user_repository.dart
// 倉庫抽象接口:定義業務需要的數據操作方法(不依賴具體實現)
abstract class UserRepository {
  Future<UserEntity> getUserById(String userId);
}
3)domain/use_cases/user_id_usecase.dart
  1. 遵循單一職責原則
  2. 調用 Repository 接口
// domain/use_cases/user_id_usecase.dart
// 業務用例:編排數據獲取和業務邏輯(如驗證)
class GetUserByIdUseCase {
  final UserRepository repository; // 依賴抽象接口
  
  GetUserByIdUseCase(this.repository);

  Future<UserEntity> execute(String userId) async {
    final user = await repository.getUserById(userId);
    if (!user.isAdult()) {
      throw Exception('User must be an adult'); // 業務規則驗證
    }
    return user;
  }
}

2.3 表現層(presentation/)

1)依賴注入:injection_container
final getIt = GetIt.instance;

void setupDependencies() {
  setupApiDependencies();
  setupRepositoryDependencies();
  setupCaseDependencies();
  setupViewModelDependencies();
}

void setupApiDependencies() {
  // 數據層
  getIt.registerSingleton<UserApiService>(UserApiService(Dio()));
}

void setupRepositoryDependencies() {
  // 倉庫層
  getIt.registerSingleton<UserRepository>(
      UserRepositoryImpl(getIt<UserApiService>())
  );
}

void setupCaseDependencies() {
  // 業務用例層
  getIt.registerSingleton<GetUserByIdUseCase>(
      GetUserByIdUseCase(getIt<UserRepository>())
  );
}

void setupViewModelDependencies() {
  // ViewModel(工廠模式,每次新建實例)
  getIt.registerFactory<UserViewModel>(
          () => UserViewModel(getIt<GetUserByIdUseCase>())
  );
}
2)view_models/user_view_model.dart

狀態管理採用 ChangeNotifier,統一處理成功/失敗。

class UserViewModel with ChangeNotifier {
  final GetUserByIdUseCase _getUserByIdUseCase;
  UserEntity? _userEntity;
  bool _isLoading = false;
  String? _error;

  // 狀態暴露給視圖層
  UserEntity? get user => _userEntity;
  bool get isLoading => _isLoading;
  String? get error => _error;

  UserViewModel(this._getUserByIdUseCase);

  Future<void> fetchUser(String userId) async {
    _isLoading = true;
    notifyListeners();

    try {
      _userEntity = await _getUserByIdUseCase.execute(userId);
      _error = null;
    } catch(e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}
3)pages/home_page.dart
class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {

  @override
  void initState() {
    super.initState();

    WidgetsBinding.instance.addPostFrameCallback((_) {
      final viewModel = Provider.of<UserViewModel>(context, listen: false);
      viewModel.fetchUser("1234");
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home Page')),
      body: Consumer<UserViewModel>(
        builder: (context, viewModel, child) {
          if (viewModel.isLoading) {
            return const Center(child: CircularProgressIndicator());
          }
          if (viewModel.error != null) {
            return Center(child: Text('Error: ${viewModel.error}'));
          }
          return ElevatedButton(
              onPressed: () => context.push('/detail', extra: {'id': '${viewModel.user?.id}'}),
              child: Text('Go to the Details page With id: ${viewModel.user?.name}')
          );
        },
      )
    );
  }
}
4)局部注入

⚠️ 在具體頁面局部註冊業務邏輯類(如 LoginViewModel)。

class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => LoginViewModel(
        loginUseCase: sl<LoginUseCase>(),
      ),
      child: _LoginView(),
    );
  }
}
5)入口類全局註冊

⚠️ 我們應該只在 main.dart 全局註冊基礎服務(如 NetworkService)。

void main() async {
  // 初始化環境配置
  await EnvLoader.load();
  setupDependencies();
  runApp(
      MultiProvider(
        providers: [
          ChangeNotifierProvider(create: (_) => getIt<UserViewModel>()),
        ],
        child: const MyApp(),
      )
  );
}

3. 架構設計特點

3.1 依賴關係圖‌

presentation 層 → domain/use_cases → domain/repositories(接口)
                                      ↑
data/services(API/Database) ← data/repositories(實現)

3.2 關鍵區別與必要性

層面 domain/ 業務層 data/ 數據層 是否冗餘?
模型 UserEntity(業務屬性+邏輯方法) UserModel(純數據映射) 否,面向不同場景
倉庫 接口(UserRepository 實現(UserRepositoryImpl 否,抽象與實現分離
關注點 業務規則(如年齡驗證) 技術細節(如 JSON 解析、網絡請求) 明確分工

📊 架構效能對比

維度 無 Repository 有 Repository
數據源切換 需修改 ViewModel 僅調整 Repository 實現
測試成本 需啓動完整網絡環境 Mock 單一接口即可
錯誤處理 分散在各 ViewModel 集中處理
代碼複用 相似邏輯需重複實現 跨模塊共享數據策略

3.3 架構總結‌

不重複設計‌:業務層定義 ‌“做什麼”‌(抽象接口、業務規則),數據層實現 ‌“怎麼做”‌(具體技術細節)。

優勢:業務層可獨立測試(無需依賴網絡/數據庫);數據源切換靈活(如從 API 切換為本地緩存只需修改 data/ 層);符合依賴倒置原則(高層模塊不依賴低層細節)。

當應用涉及多數據源協同(如實時API+本地緩存)時,Repository 的價值尤為突出。

4. Repository 解析

Repository 是 MVVM 架構中‌數據層的統一管理者‌,通過抽象數據訪問細節、標準化數據格式和集中化策略處理,顯著提升代碼的可維護性、擴展性和測試便利性。其設計本質符合“高內聚低耦合”的架構原則,是複雜 Flutter 項目推薦的實踐模式。

在 Flutter 的 MVVM + Repository 架構中,Repository 層扮演着核心協調(數據中驅)角色,本質上是數據層的統一抽象網關。其核心價值體現在以下方面。

4.1 數據抽象與統一入口

1)‌隔離數據源細節‌:Repository 作為數據訪問層,將網絡 API、本地數據庫(如SQLite)、緩存(如 Hive)等數據源的具體實現與業務邏輯解耦。ViewModel 僅通過 Repository 提供的統一接口獲取數據,無需關心數據來自 REST 請求還是本地存儲。

abstract class UserRepository {
  Future<User> fetchUser(); // 統一接口
}

2)‌數據轉換與標準化‌:將原始數據(如 JSON)轉換為領域模型(Domain Model),確保 ViewModel 接收的是可直接使用的業務實體(Entity),而非原始 API 響應。

User _mapToEntity(UserDto dto) {
  return User(id: dto.id, name: dto.username);
}

3)多數據源協調器‌:智能組合遠程與本地數據源,實現如「緩存優先」策略:

Future<User> fetchUser() async {
  if (localDataSource.hasData) {
    return localDataSource.getUser();
  } else {
    final remoteUser = await api.getUser();
    await localDataSource.cache(remoteUser);
    return remoteUser;
  }
}

4.2 架構優勢與設計價值

為何 MVVM 需要 Repository?

1)‌降低耦合性,打破 ViewModel 數據耦合‌:通過 Repository 模式,數據源變更(如切換 API 提供商)只需修改 Repository 內部實現,無需改動 ViewModel 或 UI 層代碼。不加 Repository 時,ViewModel 直接對接 API 導致:

  • 業務邏輯與數據獲取強耦合;
  • 切換數據源需修改 ViewModel;
// 反例:ViewModel 直接調用API
class ProfileVM {
  final ApiService _api; // 直接依賴具體實現
  Future<void> loadData() => _api.getProfile();
}

2)‌統一錯誤處理機制‌:Repository 可集中處理數據層異常(如網絡超時/解析錯誤),避免 ViewModel 重複實現錯誤處理。

3)增強可測試性,測試效率倍增‌:ViewModel 測試只需 Mock Repository 接口,無需構建真實網絡環境。可輕鬆替換為 Mock 實現,方便單元測試時模擬網絡請求或數據庫操作:

test('VM測試', () {
  when(mockRepo.getUser()).thenReturn(mockUser);
  expect(viewModel.user, mockUser);
});

4)集中管理數據策略:‌在 Repository 內部實現緩存邏輯(如“先本地後網絡”)、數據合併或錯誤重試等複雜策略,簡化 ViewModel 的職責。

4.3 與 ViewModel 的協作流程

典型數據流‌:ViewModel 調用 Repository 方法 → Repository 從數據源獲取數據 → 返回標準化模型 → ViewModel 更新狀態並觸發 UI 渲染。代碼示例:

class UserViewModel {
  final UserRepository repository;
  Future<void> loadUser() async {
    final user = await repository.fetchUser(); // 通過Repository獲取數據
    // 更新狀態...
  }
}

錯誤處理橋樑‌:Repository 統一捕獲數據源異常(如網絡超時),轉換為業務層可理解的錯誤類型,避免 ViewModel 直接處理底層異常。

4.4 實際應用場景

  • 多數據源協調‌:合併 API 響應與本地數據庫數據。
  • 離線優先策略‌:優先返回緩存數據,後台同步最新內容。
  • 權限管理‌:在 Repository 層處理認證令牌的刷新與注入。

‌5. 依賴注入(DI)與運行時綁定的實現原理

5.1 核心概念:依賴倒置原則(DIP)‌

  • 抽象接口(UserRepository‌:業務層僅依賴抽象,不關心具體實現(如 UserRepositoryImpl)。
  • 實現類(UserRepositoryImpl‌:數據層通過實現接口提供具體功能,但業務層無需直接引用它。

‌5.2 依賴注入的綁定過程‌

步驟1:定義抽象與實現
// 抽象接口(業務層)
abstract class UserRepository { ... }

// 實現類(數據層)
class UserRepositoryImpl implements UserRepository { ... }
步驟2:依賴注入容器(DI Container)的配置

在應用啓動時,通過依賴注入框架(如 get_itinjectable)註冊綁定關係:

// 示例:使用 get_it 註冊依賴
final getIt = GetIt.instance;

void setupDependencies() {
  // 註冊接口與實現的綁定關係
  getIt.registerSingleton<UserRepository>(UserRepositoryImpl(apiService));
  
  // 註冊 UseCase,自動注入 UserRepositoryImpl 實例
  getIt.registerFactory(() => GetUserByIdUseCase(getIt<UserRepository>()));
}
步驟3:運行時解析依賴

GetUserByIdUseCase 被實例化時:

  1. DI 容器檢測其構造函數需要 UserRepository 類型參數;
  2. 根據註冊的綁定關係,自動提供 UserRepositoryImpl 的實例。

‌5.3 關鍵點解析‌

  • 運行時動態綁定‌:實際傳入的 UserRepositoryImpl 實例是在程序運行時由 DI 容器動態解析的,而非編碼時硬依賴。
  • 解耦優勢‌:業務層(GetUserByIdUseCase)僅依賴接口,更換數據源(如從 API 改為本地數據庫)只需修改實現類,無需改動業務代碼。

‌5.4 代碼執行流程示例

void main() {
  // 初始化 DI 容器
  setupDependencies();
  
  // 獲取 UseCase 實例(自動注入 UserRepositoryImpl)
  final useCase = getIt<GetUserByIdUseCase>();
  useCase.execute("123"); // 實際調用 UserRepositoryImpl 的方法
}

5.5 總結‌

如何做到‌:通過 DI 容器在運行時將接口與實現綁定,業務層通過構造函數聲明依賴接口,容器自動注入具體實現。

核心價值‌:實現層間解耦,提升代碼可測試性和可維護性。

如需進一步瞭解具體 DI 框架的使用,可參考 get_itinjectable 的官方文檔。

6. ViewModel 集中註冊問題

在 Flutter 中使用 MVVM + repository 架構時,確實不需要將所有 ViewModel 都在 main 函數中通過 MultiProvider 集中註冊。以下是幾種優化方案,可以避免 main 函數臃腫並實現按需註冊,前面提到了一些實用規則,在具體頁面局部註冊業務邏輯類(如 LoginViewModel),應該只在 main.dart 全局註冊基礎服務(如 NetworkService)。

6.1 ‌GetIt 工廠模式 + Provider 動態註冊‌

結合 GetIt 的工廠模式註冊和 Provider 的按需使用,可以避免在 main 中預註冊所有 ViewModel:

// injection_container.dart
void setupViewModelDependencies() {
  getIt.registerFactory<UserViewModel>(
    () => UserViewModel(getIt<GetUserByIdUseCase>())
  );
  // 其他ViewModel同理
}

// 頁面中使用時動態獲取
final viewModel = Provider.of<UserViewModel>(
  context,
  listen: false,
  create: (_) => getIt<UserViewModel>(), // 從GetIt工廠創建
);

6.2 ‌懶加載 Provider‌

通過 ProxyProviderChangeNotifierProxyProvider 實現 ViewModel 的延遲初始化:

// main.dart中僅註冊基礎服務
void main() {
  runApp(
    MultiProvider(
      providers: [
        Provider(create: (_) => Dio()),
        Provider(create: (_) => UserApiService(getIt<Dio>())),
      ],
      child: MyApp(),
    ),
  );
}

// 頁面中按需組合 ViewModel
Provider(
  create: (context) => UserViewModel(
    GetUserByIdUseCase(
      UserRepositoryImpl(
        context.read<UserApiService>()
      )
    )
  ),
  child: Consumer<UserViewModel>(...),
)

6.3 ‌路由級 Provider 註冊

使用 onGenerateRoute 在路由跳轉時動態註冊:

MaterialApp(
  onGenerateRoute: (settings) {
    return MaterialPageRoute(
      builder: (context) {
        return Provider(
          create: (_) => getIt<UserViewModel>(), // 或直接構造
          child: const HomePage(),
        );
      },
    );
  },
)

6.4 方案對比

方案 優點 缺點 適用場景
GetIt+Provider 解耦註冊與使用,支持全局單例 需維護GetIt容器 中大型項目
懶加載ProxyProvider 依賴關係清晰 嵌套可能較深 依賴鏈複雜的場景
路由級註冊 精確控制生命週期 需手動管理路由 頁面獨立性強的應用

最佳實踐建議:

核心服務‌(如API Client、數據庫)仍在 main 中註冊。

頁面級 ViewModel‌ 通過 GetIt 工廠或路由動態創建。

使用 context.read() 替代 Provider.of 減少不必要的 rebuild。

通過以上方式,可以保持 main 函數簡潔,同時享受 MVVM 架構的清晰分層優勢。

十一. 後續

物不盡美,事無萬全。我很清楚,上面提到的很多細節方面存在一些不足,但作為一篇可參考技術文檔,還是直接借鑑和 star 的。我在後面項目開發過程中,會對架構(文章和架構代碼)進一步在實踐中做不斷的優化,代碼鏈接後續再放出來, Thanks 觀看。

user avatar xiangzhihong Avatar qngyun1029 Avatar mybj123 Avatar
Favorites 3 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.