認真對待每時、每刻每一件事,把握當下、立即去做。
移動應用開發領域的技術演進正持續推動着跨平台解決方案的創新。在 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_gen 或 flutter_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定義主色、輔色等配色方案; - 通過
appBarTheme、buttonTheme等定製組件樣式; - 引用
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)。
- 單一職責原則:每個類都有明確的職責劃分;
- DbBaseProvider:提供基礎表操作能力;
- DbHelper:管理數據庫連接和初始化;
- UserDbProvider:實現具體業務表操作;
- 模板方法模式:DbBaseProvider 中定義了抽象方法(getTableName, createTableString),要求子類必須實現;
- 單例模式:DbHelper 採用單例確保全局只有一個數據庫連接;
- 懶加載:數據庫連接在首次使用時才初始化;
2.2 封裝優點
-
結構清晰:分層明確,職責分離;
-
複用性強:基礎功能封裝在父類,子類只需關注業務表結構;
-
性能優化:
- 單例模式避免重複創建連接;
- 表存在檢查避免重複建表;
-
擴展性好:新增表只需繼承 DbBaseProvider;
-
線程安全:所有操作都是異步的;
2.3 常見問題和改進注意點
注意事項:
- 數據庫版本管理前期設計不足:DbHelper 中雖然有 version 字段但沒有用於升級邏輯,缺少數據庫升級遷移機制。增強的版本管理:添加了 onUpgrade 和 onDowngrade 回調、每個 Provider 可定義升級 SQL;
- 事務支持不足:提供事務操作方法封裝;
- 錯誤處理缺失:沒有統一的對數據庫操作異常的捕獲和處理機制;
- SQL 注入風險:UserDbProvider 中直接拼接 SQL 字符串,部分 SQL 語句直接拼接字符串參數,使用參數化查詢防止 SQL 注入;
- 性能優化空間:數據庫連接沒有關閉機制;
最佳實踐建議:
- 增加模型層:建議添加User模型類,替代直接使用Map;
- 使用ORM框架:考慮使用floor或moor等Dart ORM框架;
- 日誌記錄:添加數據庫操作日誌;
- 備份機制:實現定期備份功能;
- 性能監控:添加查詢性能統計;
總結:封裝遵循了基本的軟件設計原則,提供了清晰的擴展接口。主要改進空間在於錯誤處理、類型安全和版本管理方面。通過引入模型層和 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
- 組合多個數據源。
- 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
- 遵循單一職責原則
- 調用 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_it、injectable)註冊綁定關係:
// 示例:使用 get_it 註冊依賴
final getIt = GetIt.instance;
void setupDependencies() {
// 註冊接口與實現的綁定關係
getIt.registerSingleton<UserRepository>(UserRepositoryImpl(apiService));
// 註冊 UseCase,自動注入 UserRepositoryImpl 實例
getIt.registerFactory(() => GetUserByIdUseCase(getIt<UserRepository>()));
}
步驟3:運行時解析依賴
當 GetUserByIdUseCase 被實例化時:
- DI 容器檢測其構造函數需要
UserRepository類型參數; - 根據註冊的綁定關係,自動提供
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_it 或 injectable 的官方文檔。
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
通過 ProxyProvider 或 ChangeNotifierProxyProvider 實現 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 觀看。