认真对待每时、每刻每一件事,把握当下、立即去做。
移动应用开发领域的技术演进正持续推动着跨平台解决方案的创新。在 Android 与 iOS 等多平台并存的现状下,传统原生开发面临代码复用率低和开发效率瓶颈等核心挑战。Flutter 作为 Google 推出的现代化 UI 工具包,通过自绘引擎和响应式框架实现了真正的跨平台一致性,其"一次编写,处处运行"的理念已在全球范围内得到验证------根据往年 Dart 开发者调研,采用 Flutter 的企业项目平均缩短了40%左右的开发周期。本文基于 MVVM+Repository 架构模式,系统阐述 Flutter 在工程化实践中的解决方案。
这次公司在新项目技术再次选型的前景下,让我对 Flutter 做一次技术构架分享。为了把 Flutter 说清楚,如何去做架构企业级项目,项目架构中应该包含哪些技术点,我做了下面结构性的技术总结,前面部分我会针对技术、工具链生态做一个系统解析,最后一部分详细根据业务点来阐述 MVVM+Repository 架构。
特别地,本文方案融合了笔者在2022年主导公司的**企业级移动应用重构经验(Native + KMM + React 架构)**,其中对状态管理、模块化解耦等关键问题的解决路径,均在本架构中得到延续与升级。通过完整的代码示例与架构图解进行解析。
当然,在互相学习过程中欢迎指出其中的不足和改进意见,后续有时间会对基础架构一些延续的东西我也会陆续补充进来。我们先看看基础项目结构的定义,有个大概了解再往下看。
yaml
# 项目目录结构定义
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/目录)
这里一般配置一个开发环境和一个生产环境就行了,目前我们公司涉及到大量客户演示,这里增加一个演示环境,总的来说按需配置。
markdown
├── 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 依赖
yaml
dependencies:
flutter_dotenv: ^5.2.1
4. 创建配置加载器
配置文件路径:lib/src/config/env_loader.dart
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 中初始化环境
dart
void main() async {
// 初始化环境配置
await EnvLoader.load();
runApp(const MyApp());
}
6. 启动和打包时指定环境
6.1 调试开发环境
dart
# 1. 命令启动开发环境
flutter run --dart-define=ENV=dev
# 2. 配置IDE运行参数
# 在IDE的 "Run"->"Edit Configurations" 中:
- 找到 Flutter 运行配置
- 在"Additional arguments"添加:--dart-define=ENV=dev
6.2 正式环境打包
Android APK:
dart
# 生产环境
flutter build apk --dart-define=ENV=prod
# 演示环境
flutter build apk --dart-define=ENV=staging
iOS IPA:
-
命令行打包:
dart# 生产环境 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. 使用示例
dart
Text(EnvLoader.envName)
二. 静态资源配置
1. 资源目录结构设计
├── assets/ # 静态资源
│ ├── images/ # 图片资源
│ ├── fonts/ # 字体文件
│ └── json/ # 本地JSON文件
2. pubspec.yaml 配置
yaml
flutter:
assets:
- assets/images/
- assets/json/
fonts:
- family: Rbt
fonts:
- asset: assets/fonts/Rbt-Framework.ttf
3. 资源图片引用类生成
这里是自定义工具实现示例,其实我们可以直接使用通过资源代码生成工具实现自动生成的 generated/assets.dart
工具类实现文件。该机制本质上是通过元编程手段,将文件系统的资源组织结构转化为类型安全的编程接口,属于 Flutter 现代化开发工具链的典型实践,后面会具体介绍。
dart
// 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. 字体资源使用
全局应用:
dart
MaterialApp(
theme: ThemeData(
fontFamily: 'Rbt', // 使用声明的字体家族名
),
);
局部应用:
dart
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 常量,避免手动输入路径字符串,减少拼写错误。
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
中声明资源后,插件会监听文件变化并自动生成代码:
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、音频等非图片资源,生成对应的解析方法。
四. 常量配置集
常用常量配置集合结构参考如下,当然我们在开发过程中应该根据具体实际情况进行增加和修改。
md
core/ # 核心层
│ │ │ ├── constants/ # 常量
│ │ │ │ ├── app_constants.dart # 应用常量
│ │ │ │ ├── app_strings.dart # 字符串常量
│ │ │ │ ├── app_layouts.dart # 布局尺寸常量
│ │ │ │ └── app_colors.dart # 颜色常量
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
类实现统一管理。典型内容:
dart
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. 文本样式规范
功能:集中管理所有文本样式(如标题、正文、按钮文字等),避免散落在各处重复定义。典型内容:
dart
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
中应用主题:
dart
MaterialApp(
theme: AppTheme.lightTheme, // 使用预定义主题
darkTheme: AppTheme.darkTheme,
home: MyApp(),
);
在组件中调用文本样式:
dart
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 目录架构结构
markdown
├── 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 与数据层交互,获取或存储数据。
典型实现方式:
dart
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 树,可直接进行单元测试。
dart
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
地狱。
dart
// 反例:业务逻辑混入UI层
class LoginPage extends StatefulWidget {
Future<void> _login() async {
// API调用+状态管理+导航跳转
}
}
测试困难,需启动完整 Widget 树测试基础逻辑。
状态分散,相同业务状态可能被重复实现于不同Widge。
4)关键实践建议
层级交互规范,遵循单向依赖:外层→内层
dart
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
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
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 转换。
dart
// 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
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
dart
// domain/repositories/user_repository.dart
// 仓库抽象接口:定义业务需要的数据操作方法(不依赖具体实现)
abstract class UserRepository {
Future<UserEntity> getUserById(String userId);
}
3)domain/use_cases/user_id_usecase.dart
- 遵循单一职责原则
- 调用 Repository 接口
dart
// 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
dart
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,统一处理成功/失败。
dart
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
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
)。
dart
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => LoginViewModel(
loginUseCase: sl<LoginUseCase>(),
),
child: _LoginView(),
);
}
}
5)入口类全局注册
⚠️ 我们应该只在 main.dart
全局注册基础服务(如 NetworkService
)。
dart
void main() async {
// 初始化环境配置
await EnvLoader.load();
setupDependencies();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => getIt<UserViewModel>()),
],
child: const MyApp(),
)
);
}
3. 架构设计特点
3.1 依赖关系图
dart
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 请求还是本地存储。
dart
abstract class UserRepository {
Future<User> fetchUser(); // 统一接口
}
2)数据转换与标准化:将原始数据(如 JSON)转换为领域模型(Domain Model),确保 ViewModel 接收的是可直接使用的业务实体(Entity),而非原始 API 响应。
dart
User _mapToEntity(UserDto dto) {
return User(id: dto.id, name: dto.username);
}
3)多数据源协调器:智能组合远程与本地数据源,实现如「缓存优先」策略:
dart
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;
dart
// 反例:ViewModel 直接调用API
class ProfileVM {
final ApiService _api; // 直接依赖具体实现
Future<void> loadData() => _api.getProfile();
}
2)统一错误处理机制:Repository 可集中处理数据层异常(如网络超时/解析错误),避免 ViewModel 重复实现错误处理。
3)增强可测试性,测试效率倍增:ViewModel 测试只需 Mock Repository 接口,无需构建真实网络环境。可轻松替换为 Mock 实现,方便单元测试时模拟网络请求或数据库操作:
dart
test('VM测试', () {
when(mockRepo.getUser()).thenReturn(mockUser);
expect(viewModel.user, mockUser);
});
4)**集中管理数据策略:**在 Repository 内部实现缓存逻辑(如"先本地后网络")、数据合并或错误重试等复杂策略,简化 ViewModel 的职责。
4.3 与 ViewModel 的协作流程
典型数据流:ViewModel 调用 Repository 方法 → Repository 从数据源获取数据 → 返回标准化模型 → ViewModel 更新状态并触发 UI 渲染。代码示例:
dart
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:定义抽象与实现
dart
// 抽象接口(业务层)
abstract class UserRepository { ... }
// 实现类(数据层)
class UserRepositoryImpl implements UserRepository { ... }
步骤2:依赖注入容器(DI Container)的配置
在应用启动时,通过依赖注入框架(如 get_it
、injectable
)注册绑定关系:
dart
// 示例:使用 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 代码执行流程示例
dart
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:
dart
// 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 的延迟初始化:
dart
// 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
在路由跳转时动态注册:
dart
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 观看。