认真对待每时、每刻每一件事,把握当下、立即去做。
移动应用开发领域的技术演进正持续推动着跨平台解决方案的创新。在 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 集成的特殊路由;
建议根据团队技术栈和项目需求(如是否跨平台、是否需要强类型支持)做出选择。
十. 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 观看。