做这个项目之前,我其实犹豫了很久。情绪日记这种应用,市面上已经有不少成熟产品,但绝大多数都跑在 iOS 或 Android 生态里。作为长期关注鸿蒙生态的开发者,我一直在琢磨一个问题:能不能用 Flutter 把一套代码跑到 OpenHarmony 上,同时把一个心理健康类产品的细节体验做到位?这中间有太多让人拿不准的地方——状态管理怎么跟平台生命周期兼容、Chip 选择器在不同主题下的表现、情绪记录类 UI 对视觉细节的苛刻要求。这篇文章就是把我从零到一构建这个 Flutter 情绪日记应用的全过程记录下来,包括架构设计、状态管理选型、Chip 组件的交互打磨,以及心理健康界面的设计准则。如果你正准备在 OpenHarmony 上做 Flutter 开发,或者在做任何情绪记录、打卡类的应用,这篇应该能帮你少走不少弯路。
1. 为什么选 OpenHarmony 作为情绪日记的落点
1.1 心理健康类应用在国产生态的独特机遇
先说一个背景。情绪日记这类应用天然对隐私高度敏感,用户记录的是自己最真实的心理状态,这些数据一旦泄露,后果比通讯录泄露更严重。所以我在设计之初就把"数据本地化、最小化收集"作为铁律。而 OpenHarmony 生态恰好在这方面有天然优势——它作为国产开源操作系统,设备端能力可控,数据可以完全保留在本地,不依赖任何云端服务。对于心理健康类产品来说,"数据不出设备"本身就是最强的卖点之一。
另一方面,OpenHarmony 的硬件生态在快速扩张,从开发板到智能终端,存量设备量已经不小。但应用生态还在爬坡期,心理健康类应用几乎是一片空白。这其实是个机会——第一批吃螃蟹的人,往往能占据用户心智。我选择在这个时间点进入,不是因为它成熟,而是因为它有增量空间,竞争少,试错成本低。
1.2 Flutter 跨端策略在 OpenHarmony 的落地现状
那为什么要用 Flutter 而不是直接上 ArkUI 呢?这个问题我纠结了很久。ArkUI 是 OpenHarmony 的原生声明式框架,性能和平台能力调用当然是最优的。但对我来说有个现实问题:我手头已经有不少 Flutter 代码沉淀,团队对 Flutter 的熟练度也更高,如果从头学 ArkUI,学习成本会吃掉整个项目的排期。
目前 OpenHarmony 社区对 Flutter 的支持主要靠 OpenHarmony SIG 维护的 flutter_flutter 项目,它把 Flutter 引擎和框架层移植到了 OpenHarmony 上,支持通过 flutter create --platforms ohos 创建工程,也能调用底层的基础能力。实际体验下来,大部分 UI 渲染、动画、手势都能正常工作,对于情绪日记这种中轻量应用来说完全够用。当然也要承认,部分涉及原生平台能力的插件(比如地图、支付)还不兼容,需要找替代方案。但我这个项目里几乎没有这种依赖,所以 Flutter 是当下性价比最高的选择。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搭建 Flutter-for-OpenHarmony 开发环境:踩过的坑逐个说
2.1 工具链清单与版本匹配
环境搭建是第一个大坑。Flutter 官方版本并不直接支持 OpenHarmony,你需要使用 OpenHarmony SIG 维护的 ohos 分支。以下是我最终确定并跑通的工具链组合:
| 工具 | 版本/分支 | 说明 |
|---|---|---|
| DevEco Studio | 5.0 及以上 | OpenHarmony 官方 IDE,用于 SDK 管理和设备调试 |
| OpenHarmony SDK | 4.0/5.0 均可 | 在 DevEco Studio 的 SDK Manager 中下载 |
| Flutter SDK | ohos 分支 | 从 gitee 克隆 openharmony-sig/flutter_flutter 的 ohos 分支 |
| 开发板 | RK3568 / RK3588 | 我用的 RK3568,性能足够跑 Flutter 应用 |
| 调试工具 | hdc | OpenHarmony 的设备连接工具,类似 adb |
版本匹配是最容易出问题的地方。我在一开始图省事,直接用 Flutter 官方稳定版试着加 ohos 平台,结果 flutter create 直接报错,根本不识别 ohos 这个平台标识。后来才意识到,必须在环境变量里指向 ohos 分支的 Flutter SDK,DevEco Studio 里的 OpenHarmony SDK 也要保持版本对应关系。如果你用的是 DevEco Studio 5.0,对应的 OpenHarmony SDK 版本建议选 4.0 以上,太低版本的部分 API 会被 Flutter 框架层调用失败。
2.2 从零到真机运行的完整步骤
整个流程比标准 Flutter 开发多几步,但按顺序做下来是能跑通的。以我自己的环境为例:
bash复制# 1. 克隆 ohos 分支的 Flutter SDK
git clone -b ohos https://gitee.com/openharmony-sig/flutter_flutter.git
export PATH="$PWD/flutter_flutter/bin:$PATH"
# 2. 配置 OpenHarmony SDK 路径
export OHOS_SDK_HOME=/path/to/your/ohos-sdk
# 3. 检查环境
flutter doctor
flutter doctor 如果能识别出 OpenHarmony 工具链(类似 OpenHarmony toolchain · yes),说明环境基本就绪。接着创建项目:
bash复制# 4. 创建项目,显式指定 ohos 平台
flutter create --platforms ohos mood_diary
# 5. 进入项目目录,查看可用设备
flutter devices
这里要注意,flutter devices 只有在开发板连接并开启 hdc 调试后才会显示设备。我的开发板是 RK3568,通过 USB 连接后,需要在 DevEco Studio 里确认设备已被识别,然后运行:
bash复制# 6. 编译并安装到开发板
flutter run -d <device-id>
第一次编译会非常慢,因为要同时构建 Flutter 引擎相关产物和 OpenHarmony 的 hap 包,我在 RK3568 上大概等了十分钟左右。
2.3 排查真机调试中的常见报错
这一环节我花了整整一个下午,遇到的报错大概能凑一页文档。挑几个典型的给你参考:
报错一:Unable to locate adb 或 hdc not found
这是 hdc 工具没找到。hdc 是 OpenHarmony 的设备调试工具,类似 adb,它在 DevEco Studio 的 SDK 目录下,需要手动把路径加到 PATH 里。我这里的路径是 /opt/DevEco-Studio/sdk/default/openharmony/toolchains/hdc,不同版本会有些差异,找到 hdc 所在目录后 export 出来就行。
报错二:sign the hap package 签名失败
OpenHarmony 的设备上安装 hap 包需要签名。刚接触的人很容易在这里卡住,因为 flutter run 默认不会帮你做签名。解决方案是在 DevEco Studio 中配置自动签名,或者在项目里的 build-profile.json5 中手动指定签名证书。我用的是 DevEco Studio 的自动签名功能,登录账号后它会自动生成调试证书,这个最省事。
报错三:libflutter.so not found
这个多半是 Flutter SDK 的 ohos 分支编译不完整导致的,不是项目代码的问题。我当时是克隆分支后直接用的远程产物,某次同步代码后没有重新编译缓存产物,导致 so 库缺失。解决办法是把 $PWD/flutter_flutter/bin/cache 删掉重新跑 flutter doctor 触发重新下载,或者直接重新克隆分支。
这些坑看似碎,但任何一环掉链子都会让项目卡在原地。我的建议是环境准备阶段按官方 README 一步步走,每一步的报错都搜一下有没有现成 issue,不要硬着头皮往下走。
3. 情绪日记的核心:数据模型与状态管理设计
3.1 情绪条目的数据结构取舍
情绪日记的核心功能很聚焦:用户选择一种情绪、记录一段文字、打几个标签,然后按日期归档。这些数据看似简单,但在结构设计上要考虑到后续的统计需求和跨页面状态同步。
我最终定义的数据结构是这样的:
dart复制class MoodEntry {
final DateTime date; // 记录的日期,精确到天
final String moodKey; // 情绪类型:calm / happy / anxious / tired / angry
final int intensity; // 情绪强度 1-5
final String content; // 日记正文
final List<String> tags; // 自定义标签
final DateTime createdAt; // 创建时间
}
为什么用 moodKey 而不是直接存颜色值或表情符号?因为情绪类型是要参与统计和后续渲染的,存语义化的 key,展示层才能灵活映射到不同的图标、颜色、文案。如果直接存了颜色值,后面想改主题色就要迁移数据,很痛苦。
intensity 这个字段是我后来加上的。单一的情绪类型太粗糙,同样写"焦虑",强度 2 和强度 5 的心理状态完全不同。有了强度维度,日历上的情绪色块就可以变深浅,后续也方便做趋势分析。
3.2 用 Provider 做状态管理的核心写法
状态管理选型上,我最终选了 Provider,而不是 Bloc 或 Riverpod。原因很简单:这个应用的状态流是单方向的——用户操作 -> 更新状态 -> 刷新 UI,没有复杂的异步事件流,也没有需要跨模块共享的大状态。Provider 的 ChangeNotifier 模式足够用,而且学习成本低,后续接手的人也好维护。
核心的 MoodController 长这样:
dart复制class MoodController extends ChangeNotifier {
final List<MoodOption> _moods = [
MoodOption(key: 'calm', label: '平静', icon: Icons.spa,
baseColor: const Color(0xFF7FB5B5)),
MoodOption(key: 'happy', label: '开心', icon: Icons.wb_sunny,
baseColor: const Color(0xFFF4B860)),
MoodOption(key: 'anxious',label: '焦虑', icon: Icons.waves,
baseColor: const Color(0xFFD89A9E)),
MoodOption(key: 'tired', label: '疲惫', icon: Icons.nightlight,
baseColor: const Color(0xFF9B8EC4)),
MoodOption(key: 'angry', label: '生气', icon: Icons.local_fire_department,
baseColor: const Color(0xFFE07A5F)),
];
MoodOption? _selectedMood;
DateTime _selectedDate = DateTime.now();
final Map<String, MoodEntry> _entries = {};
MoodOption? get selectedMood => _selectedMood;
DateTime get selectedDate => _selectedDate;
List<MoodOption> get moodOptions => _moods;
MoodEntry? entryFor(DateTime date) {
final key = _dateKey(date);
return _entries[key];
}
void selectMood(MoodOption mood) {
_selectedMood = mood;
notifyListeners();
}
void selectDate(DateTime date) {
_selectedDate = date;
notifyListeners();
}
void saveEntry(String content, {int intensity = 3, List<String> tags = const []}) {
final key = _dateKey(_selectedDate);
_entries[key] = MoodEntry(
date: _selectedDate,
moodKey: _selectedMood!.key,
intensity: intensity,
content: content,
tags: tags,
createdAt: DateTime.now(),
);
notifyListeners();
}
String _dateKey(DateTime date) =>
'${date.year}-${date.month}-${date.day}';
}
这里的核心思想是:所有的状态变更都通过 notifyListeners() 通知依赖它的组件刷新,但组件之间不直接通信,而是通过 Provider.of 或 Consumer 读取状态。这样页面之间的状态同步问题就变成了"谁依赖这个 Controller,谁就自动刷新"。
在页面侧的使用方式:
dart复制Consumer<MoodController>(
builder: (context, controller, child) {
final selectedDate = controller.selectedDate;
final entry = controller.entryFor(selectedDate);
// 根据 entry 渲染当天的情绪记录
},
)
这个设计的好处是:日历页和编辑页不必互相引用,只要它们都 Provider.of<MoodController> 同一个实例,切换到某一天、保存一条记录,所有相关页面自动保持一致。
3.3 状态持久化:不丢一个小心情
状态管理解决了运行期间的同步问题,但应用一关,内存里的状态就没了,情绪数据必须落盘。我在选持久化方案时对比了两个主流的:
| 方案 | 优点 | 缺点 |
|---|---|---|
| shared_preferences | 简单、轻量、适合小数据 | 只能存简单类型,大数据量性能差 |
| hive | 高性能、支持复杂对象、纯 Dart | 需要引入依赖,初始化稍复杂 |
对情绪日记来说,一天的记录就是一个对象,数据量不大,但其实用 shared_preferences 也完全扛得住。不过考虑到后续可能增加图片附件、历史记录快速检索,我选了 hive。
dart复制import 'package:hive_flutter/hive_flutter.dart';
// 初始化
await Hive.initFlutter();
// 打开一个盒子(类似数据库表)
final box = await Hive.openBox<MoodEntry>('mood_entries');
// 保存
await box.put(_dateKey(date), entry);
// 读取
final entry = box.get(_dateKey(date));
// 批量读取某段日期范围
final map = box.toMap();
hive 的序列化性能比 shared_preferences 高一截,而且支持原生对象存储,不需要手动 jsonEncode。这里有个细节:MoodEntry 需要加 @HiveType() 注解并注册 adapter,不然 hive 不知道怎么序列化这个类。我一开始忘了注册 adapter,运行时报错 TypeError: type 'MoodEntry' is not a subtype of type 'Object',排查了好一会儿才反应过来。
4. Chip 选择器:让情绪记录体验更顺滑
4.1 为什么用 Chip 而不是按钮或下拉框
这是一个交互设计上的重要决策。情绪选择这个动作,用户每天至少做一次,而且要在一两秒内完成。如果用传统 RadioListTile,视觉上太重,占据大片屏幕空间;如果用下拉框,情绪这类视觉化信息会被隐藏起来,用户需要点开才能看到选项,多了一次不必要的心智负担。
Chip 组件正好在轻量感和可达性之间取得了平衡:它小巧、可以横向成组排列,也可以通过 Wrap 自动换行,还能承载图标、颜色、文本等多种信息。在情绪记录的场景里,用户一眼就能看到全部情绪选项,点一下即选中,这个交互成本是最低的。
Flutter 提供了五种 Chip:
| 组件 | 用途 | 适合场景 |
|---|---|---|
| Chip | 展示静态信息标签 | 展示性标签、状态标识 |
| ActionChip | 点击后触发操作 | 快捷操作入口 |
| FilterChip | 多选过滤 | 给日记打多个标签 |
| ChoiceChip | 单选选择 | 情绪类型单选 |
| InputChip | 输入相关内容 | 搜索结果展示 |
4.2 情绪单选与多选混用的实现思路
我在主流程里用 ChoiceChip 做情绪类型单选,因为它和 RadioButton 语义一致,但视觉更轻盈。而在记录详情页,用 FilterChip 让用户补充多个情绪标签,两者分工明确。
情绪单选的核心代码:
dart复制Consumer<MoodController>(
builder: (context, controller, child) {
final selected = controller.selectedMood;
return Wrap(
spacing: 8,
runSpacing: 8,
children: controller.moodOptions.map((mood) {
final isSelected = selected?.key == mood.key;
return ChoiceChip(
label: Text(mood.label),
avatar: Icon(mood.icon,
color: isSelected ? mood.baseColor : Colors.grey),
selected: isSelected,
onSelected: (_) => controller.selectMood(mood),
selectedColor: mood.baseColor.withOpacity(0.12),
backgroundColor: Colors.transparent,
showCheckmark: false,
side: BorderSide(
color: isSelected ? mood.baseColor : Colors.grey.shade300,
width: 1.2,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
labelStyle: TextStyle(
color: isSelected ? mood.baseColor : Colors.grey.shade600,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
),
);
}).toList(),
);
},
)
这里有几个细节值得讲。
第一,showCheckmark 我关掉了。默认的 ChoiceChip 在选中时会显示一个对勾图标,但情绪选择这种场景,对勾的语义是"确认"而不是"情绪内容",反而干扰视觉。我改用颜色和边框粗细来区分选中态,干净很多。
第二,selectedColor 用的是情绪色 12% 透明度的浅色底,side 边框用情绪色实线,这样选中状态一眼可辨,又不显得刺眼。
第三,avatar 里的图标也跟随选中状态变色,这个细节让整体反馈更完整。
多选情绪的 FilterChip 逻辑类似,只是选了多个标签存到 List<String> 里。
4.3 定制 Chip 的视觉细节
Flutter 默认的 Chip 在 Material 3 主题下的样式比较"标准",但用在心理健康类应用里,我觉得默认样式不够柔和,需要几处定制。
首先是圆角。默认 Chip 的圆角是 8dp,我调到 20dp,接近胶囊形,视觉上更亲和。硬边角会给人一种生硬感,在情绪记录这种偏私密、偏感性的场景里不合适。
然后是选中动画。Chip 选中时默认会有 150ms 左右的动画,我通过 shape 和 side 的组合调整,加上 AnimatedContainer 包裹外围状态,整体切换大约 200ms,更快会更生硬,更慢会显得拖沓。实测下来 200ms 是情绪记录场景下最舒服的节奏。
再有就是标签文本的字体权重。选中态我用 FontWeight.w600,非选中态用 FontWeight.w400。看似不起眼,但用户能明显感受到"这个选项现在被选中了"的反馈。不要用颜色深浅作为唯一区别,视觉障碍用户可能看不出来。
5. 心理健康类 UI 的设计准则与视觉落地
5.1 低饱和度的情绪色板设计
情绪日记不是普通的工具应用,它的界面会直接影响用户的情绪状态。心理学上,高饱和度的颜色(比如纯红、亮黄)会刺激交感神经,让人兴奋或焦虑;而低饱和度的颜色则能带来平静感。所以在色板设计上,我刻意把所有情绪色都往"灰调"方向压了一档。
最终采用的色板:
| 情绪 | 色值 | 设计理由 |
|---|---|---|
| 平静 | #7FB5B5 |
蓝绿调,模拟水面和植物的平静感 |
| 开心 | #F4B860 |
暖黄,像阳光,但不刺眼 |
| 焦虑 | #D89A9E |
玫瑰灰,表达敏感但不过度预警 |
| 疲惫 | #9B8EC4 |
淡紫,暗示夜色和休息 |
| 生气 | #E07A5F |
克制的砖红,不是警告红 |
这些颜色有一个共同点:饱和度基本都在 40%~60% 之间,明度控制在中间偏浅。大面积作为卡片背景时,不会强迫用户情绪,同时又保留了对每种情绪的差异化认知。我在一个简单的心理学原理里看到过,色彩本身的"温度感"会影响人对界面氛围的判断——砖红比正红"冷"了太多,所以即使是生气这种高唤醒情绪,视觉上也更容易被接受。
5.2 可读性与温和节奏:字体、间距、卡片布局
心理健康的界面,阅读舒适度比信息密度更重要。日记应用不像数据报表,用户需要在一个安全、放松的界面里写下真实的感受,所以我在排版上做了几个刻意的选择。
字体方面,正文我用的是系统默认字体,但行高调到了 1.6 以上。中文小字号在默认行高下会显得拥挤,情绪日记又是长文本输入场景,更宽松的行高能让文字呼吸起来。标题用了 FontWeight.w600,不用太粗的 w700/w800,太粗的字重会显得攻击性强。
间距上,核心操作区域用了 Padding 24dp 的外边距,卡片之间的间距 16dp。情绪选择区域放在屏幕中上部,是视觉焦点;日记输入区在下方,留出充裕的高度,让用户有"这张纸很大,可以随便写"的感觉。
卡片布局我用的圆角是 20dp,阴影非常轻,BlurStyle.normal, color: Colors.black.withOpacity(0.04),扩散半径很大但透明度很低,模拟一种柔软的投影。重阴影会让卡片产生"悬浮感",在情绪记录场景里反而有压迫感,轻阴影会让人觉得内容更融入背景。
5.3 情感化动效与暗黑模式
心理健康类应用的动效,核心原则是"平稳、不炫耀"。我观察过很多应用在动效上最容易犯的错误是为了炫技而用力过猛,弹性动画、大幅缩放、旋转,都会让用户的心率跟着起伏。情绪记录场景下的动效应该是安静地引导用户。
我实现的两处动效:
第一,页面切换用了 FadeTransition,时长 300ms,曲线 easeInOut。没有使用滑动切换,因为滑动在视觉上会把用户"推"向另一个页面,而淡入淡出让用户觉得是自己进入了新的空间,情绪上更自主。
第二,情绪选择的反馈用了 AnimatedContainer,时长 200ms,只变化颜色和边框,不做过度的缩放。这样能明确传达"你选了它",但又不会像按钮按下那样的剧烈反馈。
暗黑模式是心理健康类应用的刚需。很多用户的情绪记录发生在夜间,白底的刺眼界面会打断他们记录的心情。我在暗黑模式下没有简单地把背景换成纯黑(#000000),而是用了 #1C1C1E 这种接近黑但带一点点灰的颜色,降低对比度刺激。情绪色在暗黑模式下整体增加透明度 10% 左右,避免高亮色在暗背景上过于扎眼。
另外,暗黑模式下亮色浅底的 selectedColor(12% 透明度)在黑色背景下几乎不可见,我单独用 MediaQuery.platformBrightness 或 Theme.of(context).brightness 判断,暗色主题下选中态的底色调到 20% 透明度,配套边框用 2dp 宽度,确保切换主题后选中状态依然清楚可辨。
在开发过程中我还发现一个容易被忽略的细节:暗黑模式下系统的 surfaceTintColor 会影响 Card 的背景色,如果你用的是 CardTheme 配置,建议把 surfaceTintColor 显式设为 Colors.transparent,否则卡片颜色在不同版本上会不一致,和整个设计的统一性产生冲突。
写到这里,情绪日记这个应用从环境搭建、状态管理到 Chip 组件和 UI 设计的完整链路基本都过了一遍。最后再分享一个我在项目收尾阶段的小技巧:在真机调试时,flutter run 的热重载在 OpenHarmony 上偶尔会失效,尤其是修改了原生相关配置后,这时候不要反复按热重载,直接 flutter run 重新全量编译,反而更快。另外,情绪数据和用户隐私密切相关,就算做了本地存储,也建议在设置页加一个"清除全部记录"的按钮,这个功能虽然不起眼,但对用户来说是一种安全感的承诺。
