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

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

移动应用开发领域的技术演进正持续推动着跨平台解决方案的创新。在 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_genflutter_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 定义主色、辅色等配色方案;
  • 通过 appBarThemebuttonTheme 等定制组件样式;
  • 引用 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)。

  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 目录架构结构

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
  1. 组合多个数据源。
  2. 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
  1. 遵循单一职责原则
  2. 调用 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_itinjectable)注册绑定关系:

dart 复制代码
// 示例:使用 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 代码执行流程示例

dart 复制代码
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:

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‌

通过 ProxyProviderChangeNotifierProxyProvider 实现 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 观看。