Flutter 工程构架设计(MVVM + Repository)

认真对待每时、每刻每一件事,把握当下、立即去做。

移动应用开发领域的技术演进正持续推动着跨平台解决方案的创新。在 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 集成的特殊路由;

建议根据团队技术栈和项目需求(如是否跨平台、是否需要强类型支持)做出选择。

十. 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 观看。

发表评论

评论已关闭。

相关文章