认真对待每时、每刻每一件事,把握当下、立即去做。
移动应用开发领域的技术演进正持续推动着跨平台解决方案的创新。在 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. 使用示例
二. 静态资源配置
1. 资源目录结构设计
- ├── assets/ # 静态资源
- │ ├── images/ # 图片资源
- │ ├── fonts/ # 字体文件
- │ └── json/ # 本地JSON文件
复制代码 2. pubspec.yaml 配置
- flutter:
- assets:
- - assets/images/
- - assets/json/
- fonts:
- - family: Rbt
- fonts:
- - asset: assets/fonts/Rbt-Framework.ttf
复制代码 3. 资源图片引用类生成
这里是自定义工具实现示例,其实我们可以直接使用通过资源代码生成工具实现自动生成的 generated/assets.dart 工具类实现文件。该机制本质上是通过元编程手段,将文件系统的资源组织结构转化为类型安全的编程接口,属于 Flutter 现代化开发工具链的典型实践,后面会具体介绍。- // lib/src/core/constants/assets_constants.dart
- class AppAssets {
- static const String framework = 'assets/images/framework/home_head_image.jpg';
- }
- // 使用示例
- Image.asset(AppAssets.framework)
复制代码 4. 字体资源使用
全局应用:- MaterialApp(
- theme: ThemeData(
- fontFamily: 'Rbt', // 使用声明的字体家族名
- ),
- );
复制代码 局部应用:- Text(
- '自定义字体',
- style: TextStyle(
- fontFamily: 'Rbt',
- fontWeight: FontWeight.bold, // 匹配配置的字重
- ),
- );
复制代码 5. json 文件使用
推荐使用 json_serializable、json_annotation、build_runner 库,进行一个通用的封装,这部分会在后续框架项目中进行开源,欢迎 star。
三. 资源管理生成器
在 Flutter 项目中,generated/assets.dart 是一个自动生成的文件,主要用于资源管理的代码化和开发效率优化。以下是其核心作用与生成逻辑:
1. 核心作用
1)资源路径的静态化访问
将 assets 目录下的资源(如图片、字体)转换为 Dart 常量,避免手动输入路径字符串,减少拼写错误。- // 示例:通过生成的常量访问图片
- Image.asset(Assets.images.logo);
- // 替代
- Image.asset('assets/images/logo.png')
复制代码 2)类型安全与智能提示
资源名称通过代码生成器映射为强类型属性,IDE 可提供自动补全,提升开发体验。
3)多分辨率资源适配
自动处理不同分辨率的资源文件(如 logo@2x.png),生成统一的访问接口。
2. 自动生成的触发机制
1)依赖插件
通常由 flutter_gen 或 flutter_generate_assets 等插件实现,这些插件基于 Dart 的 build_runner 工具链。
2)配置文件驱动
在 pubspec.yaml 中声明资源后,插件会监听文件变化并自动生成代码:- flutter:
- assets:
- - assets/images/
复制代码 3)编译时生成
执行 flutter pub run build_runner build 命令触发生成,结果保存在 lib/generated/ 目录下。
3. 优势对比手动管理
特性手动管理自动生成 (generated/assets.dart)路径准确性易出错100% 准确重构友好性需全局搜索替换自动同步修改多语言支持需额外工具可整合国际化资源4. 高级应用场景
1)与国际化结合
通过注解生成多语言资源的访问代码,例如 Assets.translations.homeTitle。
2)自定义资源类型
扩展支持 JSON、音频等非图片资源,生成对应的解析方法。
四. 常量配置集
常用常量配置集合结构参考如下,当然我们在开发过程中应该根据具体实际情况进行增加和修改。- core/ # 核心层
- │ │ │ ├── constants/ # 常量
- │ │ │ │ ├── app_constants.dart # 应用常量
- │ │ │ │ ├── app_strings.dart # 字符串常量
- │ │ │ │ ├── app_layouts.dart # 布局尺寸常量
- │ │ │ │ └── app_colors.dart # 颜色常量
复制代码- class AppConstants {
- // 应用基础信息
- static const String appName = 'pubassistant';
- static const String appVersion = '1.0.0.0';
- static const int appBuildNumber = 1000;
- }
复制代码 五. Theme 主题配置
Theme 主题系统的核心文件,用于集中管理应用的视觉样式和文本风格。
1. 全局主题配置
功能:定义应用的整体视觉风格,包括颜色、组件样式、亮度模式等,通过 ThemeData 类实现统一管理。典型内容:- import 'package:flutter/material.dart';
- import 'text_styles.dart'; // 关联文本样式
- class AppTheme {
- // 明亮主题
- static ThemeData lightTheme = ThemeData(
- colorScheme: ColorScheme.light(
- primary: Colors.blueAccent,
- secondary: Colors.green,
- ),
- appBarTheme: AppBarTheme(
- backgroundColor: Colors.blueAccent,
- titleTextStyle: TextStyles.headlineMedium,
- ),
- buttonTheme: ButtonThemeData(
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
- ),
- textTheme: TextTheme(
- displayLarge: TextStyles.displayLarge, // 引用文本样式
- bodyMedium: TextStyles.bodyMedium,
- ),
- );
- // 黑暗主题
- static ThemeData darkTheme = ThemeData.dark().copyWith(
- colorScheme: ColorScheme.dark(
- primary: Colors.indigo,
- secondary: Colors.tealAccent,
- ),
- );
- }
复制代码 关键点:
- 使用 ColorScheme 定义主色、辅色等配色方案;
- 通过 appBarTheme、buttonTheme 等定制组件样式;
- 引用 text_styles.dart 中的文本样式保持一致性;
2. 文本样式规范
功能:集中管理所有文本样式(如标题、正文、按钮文字等),避免散落在各处重复定义。典型内容:- class TextStyles {
- // 标题样式
- static const TextStyle displayLarge = TextStyle(
- fontSize: 24,
- fontWeight: FontWeight.bold,
- color: Colors.black87,
- );
- // 正文字体
- static const TextStyle bodyMedium = TextStyle(
- fontSize: 16,
- height: 1.5,
- color: Color(0xFF424242),
- );
- // 按钮文字
- static const TextStyle buttonLabel = TextStyle(
- fontSize: 14,
- fontWeight: FontWeight.w600,
- letterSpacing: 0.5
- );
- }
复制代码 关键点:
- 使用 const 定义静态样式提升性能;
- 包含字体大小、颜色、字重、行高等属性;
- 支持自定义字体(需在 pubspec.yaml 配置)。
3. 使用方式
在 main.dart 中应用主题:- MaterialApp(
- theme: AppTheme.lightTheme, // 使用预定义主题
- darkTheme: AppTheme.darkTheme,
- home: MyApp(),
- );
复制代码 在组件中调用文本样式:- Text('Hello', style: TextStyles.displayLarge);
复制代码 4. 设计建议
- 分层管理:将颜色、间距等基础变量单独提取(如 colors.dart),这一点就是常量配置集中提到的;
- 扩展性:通过 copyWith 方法局部覆盖主题;
- 一致性:避免直接在组件内硬编码样式;
六. 网络请求方案
dio 是一个强大的 HTTP 网络请求库,支持全局配置、Restful API、FormData、拦截器、 请求取消、Cookie 管理、文件上传/下载、超时、自定义适配器、转换器等。
项目里通过封装设计 http_exception、http_interceptor、http_options、http_request 类,适应于大型项目的开发应用。
七. 数据存储方案
1. 偏好设置
推荐 shared_preferences 方案,项目里进行了一层应用封装。
2. 数据库方案设计
2.1 核心设计原理
数据库封装采用了分层架构设计,主要由三个部分组成:基础提供者类(DbBaseProvider)、数据库助手类(DbHelper)和具体业务提供者(UserDbProvider)。
- 单一职责原则:每个类都有明确的职责划分;
- DbBaseProvider:提供基础表操作能力;
- DbHelper:管理数据库连接和初始化;
- UserDbProvider:实现具体业务表操作;
- 模板方法模式:DbBaseProvider 中定义了抽象方法(getTableName, createTableString),要求子类必须实现;
- 单例模式:DbHelper 采用单例确保全局只有一个数据库连接;
- 懒加载:数据库连接在首次使用时才初始化;
2.2 封装优点
- 结构清晰:分层明确,职责分离;
- 复用性强:基础功能封装在父类,子类只需关注业务表结构;
- 性能优化:
- 单例模式避免重复创建连接;
- 表存在检查避免重复建表;
- 扩展性好:新增表只需继承 DbBaseProvider;
- 线程安全:所有操作都是异步的;
2.3 常见问题和改进注意点
注意事项:
- 数据库版本管理前期设计不足:DbHelper 中虽然有 version 字段但没有用于升级逻辑,缺少数据库升级迁移机制。增强的版本管理:添加了 onUpgrade 和 onDowngrade 回调、每个 Provider 可定义升级 SQL;
- 事务支持不足:提供事务操作方法封装;
- 错误处理缺失:没有统一的对数据库操作异常的捕获和处理机制;
- SQL 注入风险:UserDbProvider 中直接拼接 SQL 字符串,部分 SQL 语句直接拼接字符串参数,使用参数化查询防止 SQL 注入;
- 性能优化空间:数据库连接没有关闭机制;
最佳实践建议:
- 增加模型层:建议添加User模型类,替代直接使用Map;
- 使用ORM框架:考虑使用floor或moor等Dart ORM框架;
- 日志记录:添加数据库操作日志;
- 备份机制:实现定期备份功能;
- 性能监控:添加查询性能统计;
总结:封装遵循了基本的软件设计原则,提供了清晰的扩展接口。主要改进空间在于错误处理、类型安全和版本管理方面。通过引入模型层和 ORM 框架可以进一步提升代码质量和开发效率。
八. 状态管理
InheritedWidget 提供了在 Widget 树中从上往下共享数据的能力;
全局事件总线(Event Bus)实现跨页面、跨组件的通信,进行数据传递与交互。具体的实现封装结合项目;
ChangeNotifier(provider) + ValueNotifier;
BLoC(推荐 bloc + flutter_bloc + Cubit);
九. 路由管理
在 Flutter 项目中,go_router 和 auto_route 都是优秀的第三方路由库,但它们的定位和特性有所不同。以下是两者的对比分析及选型建议:
1. 核心特性对比
go_router:
- 基于 URL 的路由管理,支持深度链接和 Web 兼容性。
- 提供路由守卫(如登录验证、权限控制)和重定向功能。
- 支持嵌套路由和动态参数解析,语法简洁。
- 与 Navigator API 兼容,适合需要 Web 支持或复杂路由逻辑的项目。
auto_route:
- 基于代码生成的路由方案,通过注解自动生成路由代码。
- 强类型路由参数,编译时检查减少运行时错误。
- 支持嵌套导航和自定义过渡动画。
- 适合追求类型安全和减少样板代码的团队。
2. 性能与复杂度
- go_router:运行时配置路由,灵活性高但可能增加运行时开销。
- auto_route:编译时生成代码,性能更优但需依赖代码生成步骤。
3. 选型建议
选择 go_router 的场景:
- 需要深度链接或 Web 支持。
- 项目中有复杂路由拦截需求(如动态权限控制)。
- 团队偏好声明式配置而非代码生成。
选择 auto_route 的场景:
- 追求类型安全和编译时检查。
- 需要减少手动编写路由样板代码。
- 项目已使用其他代码生成工具(如freezed)。
4. 混合使用方案
对于大型项目,可结合两者优势:
- 使用 auto_route 管理基础页面路由;
- 通过 go_router 处理需要动态拦截或 Web 集成的特殊路由;
建议根据团队技术栈和项目需求(如是否跨平台、是否需要强类型支持)做出选择。
5. go_router 示例
- final _rootNavigatorKey = GlobalKey<NavigatorState>();
- final _homeNavigatorKey = GlobalKey<NavigatorState>();
- final _residentNavigatorKey = GlobalKey<NavigatorState>();
- final _mineNavigatorKey = GlobalKey<NavigatorState>();
- class AppRouter {
- final GoRouter router = GoRouter(
- initialLocation: RoutePaths.login,
- navigatorKey: _rootNavigatorKey,
- redirect: (context, state) async {
- final authViewModel = Provider.of(context, listen: false);
- final isLoggedIn = await authViewModel.isLoggedIn;
- // 非登录保护逻辑
- if (!isLoggedIn && state.matchedLocation != RoutePaths.login) {
- return RoutePaths.login;
- }
- // 已登录状态下的路径修正
- if (isLoggedIn && state.matchedLocation == RoutePaths.login) {
- return RoutePaths.home;
- }
- return null;
- },
- routes: <RouteBase>[
- GoRoute(
- path: RoutePaths.login,
- builder: (context, state) => const LoginPage()
- ),
- StatefulShellRoute.indexedStack(
- builder: (context, state, navigationShell) =>
- MainPage(navigationShell: navigationShell),
- branches: [
- StatefulShellBranch(
- navigatorKey: _homeNavigatorKey,
- routes: [
- GoRoute(
- path: RoutePaths.home,
- builder: (context, state) => const HomePage(),
- routes: [
- // 首页子路由
- ],
- ),
- ],
- ),
- StatefulShellBranch(
- navigatorKey: _residentNavigatorKey,
- routes: [
- GoRoute(
- path: RoutePaths.resident,
- builder: (context, state) => const ResidentPage(),
- ),
- ],
- ),
- StatefulShellBranch(
- navigatorKey: _mineNavigatorKey,
- routes: [
- GoRoute(
- path: RoutePaths.mine,
- builder: (context, state) => const MinePage(),
- ),
- ],
- ),
- ],
- ),
- ],
- );
- }
复制代码 十. Flutter MVVM + Repository 架构
以下是 Flutter MVVM + Repository 架构的业务示例解析。
1. 架构结构和各层职责
1.1 目录架构结构
- ├── features/ # 业务功能模块划分层
- │ ├── data/ # 数据层:聚焦数据获取与存储逻辑
- │ │ ├── models/ # 数据模型
- │ │ ├── repositories/ # 数据仓库
- │ │ └── services/ # 数据服务(API接口)
- │ ├── domain/ # 业务层:处理业务规则与逻辑流转,如数据验证、流程编排、领域模型转换
- │ │ ├── entities/ # 业务实体
- │ │ ├── repositories/ # 抽象仓库接口
- │ │ └── use_cases/ # 业务逻辑用例
- │ └── presentation/ # 表现层
- │ ├── pages/ # UI 页面
- │ ├── widgets/ # 模块内复用组件
- │ ├── view_models/ # 视图模型
- │ ├── router/ # 模块独立路由
- └── └── state/ # 状态管理
复制代码 1.2 MVVM + Repository 架构层职责说明
Model 层:
- data/models:数据模型(DTO)
- domain/entities:业务实体
- data/services:数据源实现(SQLite/API)
ViewModel 层:调用 UseCase、处理业务逻辑、管理 UI 状态。
View 层:纯 UI 展示、通过 Consumer 监听 ViewModel。
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 解析、网络请求)明确分工
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |