一个优秀的项目始于清晰的架构。本文以 E-Brufen 情绪健康应用为例,详解从零搭建 Flutter 鸿蒙项目的目录结构设计、技术选型、路由策略与 Material 3 主题配置。
一、开篇:你为什么需要这篇架构指南?
你是否经历过这样的场景------Flutter 项目才写了 10 个页面,pubspec.yaml 里的依赖就超过了 30 个?打开 lib 目录,provider、bloc、get_it、go_router 层层嵌套,由于过度抽象,连自己都忘了网络请求的拦截器写在了哪个文件夹里。
E-Brufen 项目在立项初期定下了一条铁律:不为"可能用到"的功能写一行代码。这是一款面向 HarmonyOS 的离线心理健康应用,包含三大核心模块:
- 白噪音:四种自然场景(海边、雨中、森林、篝火),倒计时与触觉反馈
- 呼吸练习:三种科学呼吸法(4-7-8、盒式、放松),呼吸球动画引导
- 情绪日记:记录、时间线与周统计分析
本文将带你解剖这个仅 13 个 Dart 文件、4 个运行时依赖的鸿蒙应用,看它如何用"极简主义"架构,零崩溃地支撑起三大完整功能。
项目托管于 AtomGit,目标平台为 HarmonyOS NEXT,采用 Flutter 框架 + Hive CE 纯 Dart 存储方案。整个应用完全离线运行,不依赖任何网络请求,从根本上保障用户隐私数据的安全。这种设计决策也直接影响了我们的架构选型------既然无需网络,自然也就无需引入 dio 或 http 等网络库;既然数据量不大(情绪日记条目以"天"为单位,每年最多 365 条),那么轻量级键值存储 Hive CE 就是完美匹配。
二、目录结构设计
2.1 完整目录树

lib/
├── main.dart # 应用入口 + 多步初始化流水线
├── models/
│ └── mood_entry.dart # MoodType 枚举 + MoodEntry 数据模型
├── data/
│ ├── mood_storage.dart # 情绪记录 CRUD(ChangeNotifier)
│ └── settings.dart # 应用设置持久化(Hive CE Box)
├── pages/
│ ├── home_page.dart # 首页(卡片导航 + 今日速记)
│ ├── soundscape/
│ │ └── soundscape_page.dart # 白噪音页面
│ ├── breathe/
│ │ └── breathe_page.dart # 呼吸练习页面
│ └── diary/
│ └── diary_page.dart # 情绪日记三 Tab 页面
├── widgets/
│ ├── home_card.dart # 可复用首页入口卡片
│ ├── breathing_circle.dart # 呼吸球动画组件(含 BreathePattern 模型)
│ ├── mood_picker.dart # 五表情情绪选择器
│ └── mood_chart.dart # 周情绪柱状图(手写 Canvas 级图表)
└── theme/
└── app_theme.dart # 全局品牌色、渐变、文字样式、Material 3 主题
2.2 分层理念
按 功能职责 而非技术类型分层------这是小型 Flutter 项目最务实的策略:
| 目录 | 职责 | 设计原则 |
|---|---|---|
models/ |
纯数据结构,不含 UI 逻辑 | 仅一个文件 mood_entry.dart,包含 MoodType 枚举和 MoodEntry 数据类,提供 toJson() / fromJson() 序列化方法 |
data/ |
持久化与状态管理 | MoodStorage 继承 ChangeNotifier,直接暴露可监听的 CRUD 接口;AppSettings 封装 Hive Box 的键值读写 |
pages/ |
完整页面,每个子目录为一个功能模块 | 每个页面接收依赖作为构造函数参数,通过 Navigator.push 进入。子目录(如 diary/)确保多文件页面内聚 |
widgets/ |
可复用 UI 组件,无业务依赖 | 所有组件接受回调(VoidCallback、ValueChanged)而非直接操作数据层,保持纯净 |
theme/ |
全局样式常量与 ThemeData |
集中管理品牌色、渐变、字体样式和 Material 3 主题配置,避免样式散落各处 |
核心思路:对于少于 15 个文件的 lib 目录,过度拆分反而增加认知负担。5 个顶层目录精确对应 5 种职责,没有中间抽象层。
具体实现细节 :以 mood_entry.dart 为例,它只有不到 80 行代码,定义了五种情绪类型(开心、平静、疲惫、焦虑、悲伤)以及对应的表情符号和颜色映射。MoodEntry 记录包含 id、type、note、timestamp 以及可选的 location 字段,方便未来扩展。这种紧凑的数据模型使得整个存储层的行为清晰可测------我们可以轻松编写单元测试来验证序列化/反序列化的正确性,而无需 mock 任何 UI 组件。
2.3 为什么不采用 feature-first 或 layer-first?
常见的企业级架构有 features/home/bloc/、core/network/ 等深嵌套结构。E-Brufen 刻意不采用这些模式,原因有三:
- 文件数量少:整个 lib 仅 13 个文件,深嵌套反而让目录树空洞
- 无跨模块共享状态:三大功能模块(白噪音、呼吸、日记)完全独立,不需要 shared state 层
- 构造函数注入足够:不需要依赖注入框架的模块注册机制
更深层的考量 :当我们预测项目未来可能增长时,当前的扁平结构可以平滑演进为"按功能分组"的模式,而无需重构整个项目。例如,如果未来 breathe 目录下出现了三个新文件(如 breathe_models.dart、breathe_controller.dart、breathe_animations.dart),我们只需在 pages/breathe/ 下再建子目录即可,顶层 pages 目录保持不变。这种"先扁平后嵌套"的策略让重构成本最小化。
三、技术选型
3.1 依赖全景

项目 pubspec.yaml 仅声明 4 个运行时依赖:
yaml
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8 # iOS 风格图标集
hive_ce: ^2.19.0 # 纯 Dart 键值存储(社区版)
hive_ce_flutter: ^2.3.0 # Hive CE 的 Flutter 适配
dev_dependencies:
flutter_test: # 单元与 Widget 测试
sdk: flutter
flutter_lints: ^5.0.0 # 官方 lint 规则
没有 网络库(dio/http)、没有数据库驱动(sqflite)、没有音频插件、没有状态管理库(provider/bloc/riverpod)、没有路由库(go_router)。
这种极简依赖带来的直接好处是:HAP 安装包体积大幅缩小。在鸿蒙设备上,包体积每增加 1MB 都会影响安装速度和用户体验。去除冗余依赖后,E-Brufen 的 Release HAP 包仅为 8.2MB(含 Flutter 引擎),远低于同类应用的平均水平。
3.2 为什么选择 Hive CE 而非 sqflite?
HarmonyOS 的 Flutter 引擎 不提供原生 SQLite 支持 。sqflite 依赖 FFI 调用 C 层的 sqlite3,在鸿蒙上无法工作。
Hive CE(Community Edition)是 Hive 的社区维护分支,其关键优势:
- 纯 Dart 实现:不依赖任何原生平台 API,天然跨平台
- 键值存储模型:对于情绪日记这种"按 ID 存取"的场景足够
- 极简 API :
Hive.openBox()后直接用put()/get()/delete() - 零配置:不需要建表语句,不需要 Migration
项目中 Hive CE 的两类用法:
dart
// settings.dart ------ 简单键值存取
class AppSettings {
Box? _box;
Future<void> init() async => _box = await Hive.openBox('settings');
String get selectedScene =>
_box?.get('selectedScene', defaultValue: '雨中办公') ?? '雨中办公';
set selectedScene(String v) => _box?.put('selectedScene', v);
}
// mood_storage.dart ------ JSON 序列化 + 自增 ID
class MoodStorage extends ChangeNotifier {
Future<int> insert(MoodEntry entry) async {
final id = _nextId++;
await _box?.put(id, jsonEncode(entry.toJson()));
notifyListeners();
return id;
}
}
性能考量:Hive CE 在读取/写入 100 条以内记录时,延迟通常在 2ms 以内,完全满足 UI 交互的响应需求。并且由于所有操作都是异步(实际是异步但极快),我们无需额外引入缓存层或并发控制。
3.3 cupertino_icons 的角色
cupertino_icons 提供了 iOS 风格的图标集。在 E-Brufen 中它主要用于:
- 导航栏的返回按钮(
Icons.chevron_left) - 功能入口的箭头指示(
Icons.chevron_right) - 编辑/删除操作图标(
Icons.edit、Icons.delete) - 播放/暂停控件(
Icons.play_arrow、Icons.pause)
选择它而非自定义图标集,是为了保持与 Flutter 生态的一致性,同时减少资源文件体积。我们还为其配了一套统一的颜色主题(使用 colorScheme.onSurface),让图标在不同亮暗模式下都能自动适配。
3.4 状态管理:原生 ChangeNotifier
E-Brufen 不使用 Provider、Bloc 或 Riverpod。直接使用 Flutter 内建的 ChangeNotifier + addListener:
dart
// MoodStorage 本身就是可被监听的数据源
class MoodStorage extends ChangeNotifier {
Future<int> insert(MoodEntry entry) async {
await _box?.put(id, jsonEncode(entry.toJson()));
notifyListeners(); // 所有监听者自动收到通知
return id;
}
}
// DiaryPage 注册监听
@override
void initState() {
super.initState();
widget.moodStorage.addListener(_loadMoods);
_loadMoods();
}
// DiaryPage 注销监听
@override
void dispose() {
widget.moodStorage.removeListener(_loadMoods);
super.dispose();
}
选择原生 ChangeNotifier 的边界条件:
- 适用场景:页面数 < 10,依赖深度 < 3 层,共享状态 < 3 个实例
- 不适用场景:深层 Widget 需要跨多层访问状态;需要中间件/拦截器;需要 time-travel debugging
E-Brufen 恰好落在适用区------仅 2 个共享状态实例(MoodStorage 和 AppSettings),页面层级不超过 2 层。更重要的是,ChangeNotifier 的 addListener 回调机制让我们可以精确控制刷新粒度,避免整个 Widget 树的重建。例如,在 DiaryPage 中,只有实际插入/删除记录时才调用 notifyListeners(),而不会因 AppSettings 的变更而触发刷新。
⚠️ 鸿蒙开发避坑实录(血泪经验)
在 HarmonyOS NEXT 上调试时,我们发现 path_provider 在 API 10 上会抛出 UnsupportedError。若按照常规 Android/iOS 写法调用 getApplicationDocumentsDirectory() 获取存储路径,应用会在启动时直接崩溃。
解决思路 :不要假设所有平台 API 都提供了完整实现。在 main.dart 的初始化阶段,我们使用了 try-catch 降级策略------如果 Hive.initFlutter() 失败,直接 fallback 到 Directory.systemTemp.path(系统临时目录)。虽然临时目录的数据可能在清理缓存时丢失,但至少保证了应用在任何非标准鸿蒙设备上都能正常启动,不至于白屏。
dart
// 关键降级代码
try {
await Hive.initFlutter();
} catch (e) {
// 鸿蒙 API 10 兼容性保护
Hive.init(Directory.systemTemp.path);
}
教训 :不要假设平台 API 一定可用,永远为 Flutter 的"非标准平台"准备 Plan B。这也是为什么我们 不引入
path_provider作为显式依赖,而是直接用dart:io的原生 API 兜底。
四、页面导航设计
4.1 Navigator.push 策略
E-Brufen 不引入 GoRouter 或其他路由库,统一使用 Navigator.push + MaterialPageRoute:
dart
// home_page.dart ------ 首页卡片导航到子页面
HomeCard(
emoji: '🎵',
title: '白噪音',
subtitle: '雨声 · 海浪 · 篝火 · 森林',
gradient: AppTheme.soundscapeGradient,
onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (_) => SoundscapePage(settings: settings),
)),
),
页面层级关系:
HomePage (首页,始终在栈底)
├── SoundscapePage (白噪音)
├── BreathePage (呼吸练习)
└── DiaryPage (情绪日记)
└── ModalBottomSheet (情绪详情/编辑)
只有两层深度。不需要命名路由、不需要 Deep Link、不需要路由守卫------这些复杂性在当前阶段都是过度设计。
但我们也预留了扩展空间 :如果未来需要增加"设置"或"统计"等新页面,我们可以在首页增加更多的 HomeCard,并保持相同的导航模式,完全不需要修改路由配置。
4.2 使用匿名路由的好处
在构造函数注入模式下,匿名路由是最自然的传参方式:
dart
// 直接通过构造函数将依赖传递给目标页面
builder: (_) => SoundscapePage(settings: settings),
builder: (_) => BreathePage(settings: settings),
builder: (_) => DiaryPage(moodStorage: moodStorage),
相比命名路由的 ModalRoute.of(context)!.settings.arguments,这种方式类型安全、IDE 友好、无需类型转换。同时,它也方便我们进行单元测试------测试时可以直接 SoundscapePage(settings: mockSettings),而不必关心路由参数解析。
五、依赖注入策略:构造函数注入
5.1 完整的依赖传递链
E-Brufen 采用最朴素的 构造函数注入,依赖沿 Widget 树逐层传递:
dart
// main.dart ------ 初始化所有依赖,注入根 Widget
final settings = AppSettings();
await settings.init();
final moodStorage = MoodStorage();
await moodStorage.init();
runApp(EBrufenApp(
settings: settings,
moodStorage: moodStorage,
));
// EBrufenApp ------ 透传给首页
class EBrufenApp extends StatelessWidget {
final AppSettings settings;
final MoodStorage moodStorage;
const EBrufenApp({
super.key,
required this.settings,
required this.moodStorage,
});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(moodStorage: moodStorage, settings: settings),
);
}
}
// HomePage ------ 根据子页面需要分别传递
HomeCard(
onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (_) => SoundscapePage(settings: settings), // 仅传 settings
)),
),
HomeCard(
onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (_) => DiaryPage(moodStorage: moodStorage), // 仅传 moodStorage
)),
),
这种做法的最大优势在于依赖显式化------任何一个页面的构造函数签名就直接说明了它需要哪些外部依赖,对于代码阅读者来说一目了然。
5.2 为什么不使用 get_it / Injectable?
| 对比维度 | 构造函数注入 | DI 框架 (get_it 等) |
|---|---|---|
| 依赖可见性 | 构造函数签名即文档 | 需查阅注册代码 |
| 编译期检查 | 缺少参数即编译错误 | 运行时 GetIt.I.get<T>() |
| 测试友好度 | 直接 new 对象传入 mock | 需先注册 mock 到容器 |
| 代码量 | 无额外模板 | 需注册 + 注解(可选) |
| 学习成本 | 零(Dart 基础语法) | 需学习框架 API |
对于 E-Brufen 的依赖数量(2 个实例),DI 框架带来的抽象收益远低于其引入的复杂性。不过我们也意识到,一旦项目中需要依赖第三方服务(例如日志记录、分析追踪等),可能会引入多个单例,届时再考虑使用 get_it 也不迟。但即使那样,我们也可以采用"组合根"模式(即在 main.dart 中集中创建所有单例,然后逐层传递),依然无需容器。
六、Material 3 主题设计
6.1 从 seed color 生成色彩体系
E-Brufen 使用 Material 3 的 ColorScheme.fromSeed() 自动生成完整的色彩方案:
dart
// theme/app_theme.dart
static ThemeData get materialTheme => ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: primaryGreen, // Color(0xFF66BB6A) ------ 自然绿
brightness: Brightness.light,
),
useMaterial3: true,
scaffoldBackgroundColor: bgCream, // Color(0xFFFFF8F0) ------ 暖色底
...
);
fromSeed() 会根据一枚种子色自动推导出 primary、secondary、tertiary、surface 等全套 token。我们选择 Color(0xFF66BB6A) 作为种子色,传达"自然、平静、健康"的品牌调性。实际生成的 primary 和 onPrimary 会自动搭配出良好的对比度,保证可读性。
6.2 品牌色常量
dart
class AppTheme {
static const Color primaryGreen = Color(0xFF66BB6A); // 主色:平静与自然
static const Color softBlue = Color(0xFF81D4FA); // 白噪音模块
static const Color warmOrange = Color(0xFFFFB74D); // 情绪日记模块
static const Color gentlePurple = Color(0xFFB39DDB); // 呼吸练习模块
static const Color bgCream = Color(0xFFFFF8F0); // 全局暖色背景
}
每个功能模块有独立的渐变色,通过 HomeCard 在视觉上区分:
| 模块 | 渐变 | 色值 |
|---|---|---|
| 白噪音 | soundscapeGradient |
#81D4FA -> #4FC3F7 (天空蓝) |
| 呼吸练习 | breatheGradient |
#B39DDB -> #CE93D8 (柔和紫) |
| 情绪日记 | diaryGradient |
#FFB74D -> #FFCC80 (温暖橙) |
这些渐变色不仅用于卡片背景,还会在对应页面的顶部应用栏作为次要点缀,强化用户对功能模块的认知记忆。
6.3 组件主题覆盖
除了顶层 ColorScheme,我们还对关键组件做了全局样式覆盖:
dart
appBarTheme: const AppBarTheme(
backgroundColor: bgCream,
surfaceTintColor: bgCream, // M3 默认的 surface tint 会改变背景色
elevation: 0, // 扁平化,与暖色背景融合
centerTitle: false, // 左对齐标题(中文习惯)
scrolledUnderElevation: 1, // 滚动时出现底部分割线
titleTextStyle: TextStyle( // 统一的标题文字
color: Color(0xFF5D4037),
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), // 大圆角,柔和感
),
),
chipTheme: ChipThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
关键设计决策:
surfaceTintColor: bgCream:Material 3 默认会在 AppBar 上叠加 primary 色的 surface tint,在暖色背景上显得突兀。显式设置为背景色可消除此效果。elevation: 0:去阴影的扁平 AppBar 与bgCream背景融为一体,营造沉浸感。scrolledUnderElevation: 1:滚动时自动出现 1px 分割线,替代传统阴影,符合 M3 规范。
6.4 自定义文字样式
全局文字样式与 CSS Design Token 异曲同工:
dart
static const TextStyle greetingStyle = TextStyle(
fontSize: 22, fontWeight: FontWeight.w600,
color: Color(0xFF5D4037), // 深棕色,降低纯黑的对比度
);
static const TextStyle cardTitleStyle = TextStyle(
fontSize: 20, fontWeight: FontWeight.bold,
color: Colors.white, // 渐变卡片上使用白色文字
);
static const TextStyle guideTextStyle = TextStyle(
fontSize: 28, fontWeight: FontWeight.w500,
color: Color(0xFF5D4037), // 呼吸页面引导文字
);
将文字样式集中定义为静态常量,确保全应用视觉一致。任何页面需要修改文字外观时,只需改一处。这种设计也方便未来适配深色模式------我们可以在 AppTheme 中提供两套文字颜色,通过 Brightness 动态切换。
七、初始化流水线
7.1 健壮的多步初始化
main() 函数不是简单的 runApp(),而是一条带有降级策略的初始化流水线:
dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Step 1: 尝试 Flutter 原生路径初始化 Hive
try {
await Hive.initFlutter();
} catch (e) {
// 降级:使用系统临时目录(鸿蒙兼容性保护)
Hive.init(Directory.systemTemp.path);
}
// Step 2: 打开 Settings Box
final settings = AppSettings();
await settings.init();
// Step 3: 打开 MoodStorage Box
final moodStorage = MoodStorage();
await moodStorage.init();
runApp(EBrufenApp(settings: settings, moodStorage: moodStorage));
}
设计要点:
- 顺序依赖 :Hive 初始化必须先于任何
openBox()调用 - 降级策略 :
Hive.initFlutter()失败时 fallback 到系统临时目录,保证在非标准鸿蒙设备上也能启动 - 分步日志 :每一步有标记(
[E-Brufen] Step N: ...),排查启动问题时一目了然
我们还在每一步加入 assert 断言,在开发模式下检查 Box 是否成功打开,以便及早发现配置错误。
7.2 全局错误捕获与优雅降级
当初始化任何一步失败时,不崩溃------展示 _ErrorApp:
dart
FlutterError.onError = (details) {
FlutterError.presentError(details);
debugPrint('[E-Brufen] FLUTTER ERROR: ${details.exceptionAsString()}');
};
try {
// ... 初始化流水线 ...
runApp(EBrufenApp(...));
} catch (e, _) {
debugPrint('[E-Brufen] INIT FAILED at $errorStep: $e');
runApp(_ErrorApp(errorStep ?? '?', e.toString())); // 降级 UI
}
_ErrorApp 是一个纯展示组件:红色错误图标、失败步骤名称、异常详情。用户至少能看到"初始化失败"的明确信息,而非白屏。
实际生产经验 :在鸿蒙模拟器上测试时,我们发现 Hive.initFlutter() 在某些 API 版本上会抛出 UnsupportedError,正是因为降级策略让应用得以继续运行,避免了启动崩溃。这证明了冗余容错设计的重要性。
深入讨论见 Flutter 鸿蒙应用的错误处理与优雅降级策略
八、架构决策总结
| 维度 | 选择 | 理由 | 替代方案(未选择) |
|---|---|---|---|
| 状态管理 | 原生 ChangeNotifier | 2 个状态实例,深度 < 3 层 | Provider / Riverpod / Bloc |
| 本地存储 | Hive CE | 纯 Dart,鸿蒙兼容,零原生依赖 | sqflite(不支持鸿蒙) / SharedPreferences |
| 路由 | Navigator.push | 页面层级仅 2 层,无需 Deep Link | GoRouter / auto_route |
| 依赖注入 | 构造函数注入 | 依赖少,类型安全,零模板代码 | get_it / Injectable |
| 主题 | Material 3 + seed color | 自动色彩体系 + 手动组件微调 | 纯手写 ThemeData / 第三方主题库 |
| 依赖总数 | 4 个(含 Flutter SDK) | 极简主义,最小 HAP 体积 | --- |
| 目录结构 | models / data / pages / widgets / theme | 按功能职责 5 层,无过度嵌套 | feature-first / layer-first |
核心哲学
"为当前规模设计,为扩展留空间。"
13 个 Dart 文件不需要 DI 框架、不需要状态管理库、不需要路由库。每个决策都在回答同一个问题:"这个依赖真的解决了我们当前遇到的问题吗?"
如果未来 E-Brufen 增长到 50+ 页面、5+ 共享状态源、需要 Deep Link......那时再引入 Riverpod 或 GoRouter 也不晚。过度设计是早期项目最大的性能损耗------这里的"性能"指的是开发者的认知资源。
九、测试策略与可维护性
虽然本文侧重于架构设计,但值得一提我们为这套架构配套的测试策略------这同样体现了"务实"原则:
- 单元测试 :覆盖
MoodEntry的序列化/反序列化、MoodStorage的 CRUD 逻辑(使用内存中的 Hive Box 模拟) - Widget 测试 :对
HomeCard、MoodPicker等纯组件进行快照测试,确保 UI 不会意外回归 - 集成测试 :仅针对核心流程(例如"记录情绪 → 查看日记 → 删除")编写端到端测试,使用
flutter_driver结合鸿蒙模拟器
因为依赖极少,测试设置非常轻便------我们无需 mock 网络请求或数据库连接,只需准备几组测试数据即可。这也让我们的 CI(持续集成)流程能在 1 分钟内完成全部测试。
十、未来演进路线
当前架构已经为以下扩展预留了余地:
- 深色模式 :
AppTheme中已包含brightness参数,将来只需提供一套暗色colorScheme,并利用MediaQuery的platformBrightness即可无缝切换。 - 多语言支持 :所有硬编码字符串目前都写在了 Widget 构建方法中。未来可统一提取到
l10n/目录,使用flutter_gen或intl进行管理,只需改动pages/下的文件,其他层不受影响。 - 通知提醒 :我们可以在
data/层增加一个ReminderService,通过Timer或flutter_local_notifications实现每日提醒,该服务不会侵入页面逻辑。 - 数据导出/导入 :既然使用 Hive 存储,只需在
MoodStorage中添加exportToFile()和importFromFile()方法,UI 层调用即可,无需修改存储核心。
所有这些扩展都不需要重构现有架构------因为它们都遵循了"依赖注入"和"单一职责"原则,新增功能会被自然地安置在对应的层级中。
📦 把架构"下载"到本地
纸上得来终觉浅。这套架构已经完整开源在 AtomGit,您可以直接 clone 到本地,运行 flutter run 亲眼看看 8.2MB HAP 包的启动速度。
🔗 项目源码 :https://gitcode.com/PengXiansheng/E-Brufen
📂 核心分支 :main(已配置好鸿蒙签名调试,开箱即用)
下一步预告 :下一篇我们将深入 breathing_circle.dart,讲解如何用 Flutter 的 CustomPainter 和 AnimationController,在 60fps 下绘制出流畅的呼吸引导球,并完美适配鸿蒙的触觉反馈接口。
如果这篇架构文章对你有帮助,点个 Star 🌟,我们下期实战见!