1. 为什么我把“日迹”做成了 OpenHarmony 上的 Flutter 应用
1.1 不是所有的“跨端”都叫跨端
先说个背景。我平时主要做 Flutter 开发,Android 和 iOS 双端用同一套 Dart 代码已经成了习惯。第一次拿到 OpenHarmony 设备的时候,我本能地以为“这又是一套新生态,跟 Flutter 没关系”,直到我翻到社区里已经跑起来的 Flutter for OpenHarmony 分支,才意识到这事儿值得认真试一把。
“日迹”这个项目就是这么来的。它是一款极简习惯打卡日历:你创建几个习惯,每天打开应用,点一下,今天的格子就亮了。核心功能只有三个:习惯管理、日历视图、打卡记录。没有账户体系、没有云端同步、没有社交关系链。我选择把它作为 OpenHarmony 上的 Flutter 验证项目,是因为它的边界足够清晰——一个完全本地单机的应用,最能暴露框架本身在目标平台上的真实状态。
这里的“跨端”不是指写完一套代码自动跑所有平台,而是指 Flutter 的渲染引擎、Dart 运行时、插件机制,能在 OpenHarmony 的 OHOS 系统上跑通,并且行为与 Android 表现一致。OpenHarmony 不是一个 Linux 发行版,它有自己的一套窗口管理、事件分发、Ability 生命周期,这些对于应用框架层都是全新的适配面。日迹虽然小,却涵盖了 UI 渲染、存储、日期处理、系统主题适配这些常见需求,恰好能当作一块试金石。
1.2 Flutter、ArkTS、React Native:三条路线的取舍
在 OpenHarmony 上做应用,主流选择是 ArkTS 原生,也就是用 ArkUI 声明式语法写页面。这条路最稳,文档和官方支持最全。但从一个 Flutter 开发者的角度来看,问题也很明显:如果我手里已经有一套习惯打卡的 Flutter 代码,换成 ArkTS 重写一遍,意味着 UI 层、状态管理、日期工具类全部重来,维护成本直接翻倍。
React Native for OpenHarmony 社区也在推进,但目前三方库的适配程度、原生模块的桥接成本,都还不够让普通项目放心依赖。相比之下,Flutter for OpenHarmony 分支的成熟度已经到“能跑通实际项目”的程度,再加上 Flutter 的 UI 一致性和 Dart 的 AOT 编译,对一个小型工具类应用来说足够用了。
我当时列过一张简单的对比表,分享出来供参考:
| 对比项 | ArkTS 原生(ArkUI) | Flutter for OpenHarmony | React Native for OpenHarmony |
|---|---|---|---|
| 官方支持 | OpenHarmony 官方主推 | 社区分支,OpenHarmony SIG 在跟进 | 社区推进中 |
| 代码复用 | 仅 OpenHarmony 生态 | Dart 代码可复用 Android/iOS/Web | TS 代码可复用 RN 生态 |
| 第三方组件 | 官方组件丰富 | 依赖 pub 插件适配状态 | 依赖 npm 包,桥接成本高 |
| 渲染性能 | 原生自绘 | Skia/自绘引擎,一致性好 | 依赖 JS 桥,复杂场景有损耗 |
| 适合项目 | OpenHarmony 独占应用 | 多端一致优先的小型/中型应用 | 已有 RN 技术栈的团队 |
对日迹这种“小而全”的 App,Flutter 分支完全能扛住。而且它的价值不只是跑起来,而是让你在 OpenHarmony 上保留了一整套熟悉的开发模式:Hot Reload、Widget 树、provider 状态管理、Dart 的 DateTime 处理,这些都可以直接平移。
1.3 “日迹”的产品定位:极简不是功能少,而是决策少
回到产品本身。习惯打卡类应用市面上很多,但大多数都做得太重:统计报表、社区打卡、徽章体系、连续天数提醒,恨不得把用户每一天都安排得明明白白。日迹的定位反过来,我不需要用户在我的应用里“停留”,只需要用户在每天早上或晚上愿意花十秒钟点几个圆点,然后关闭应用。
这个定位决定了两个设计方向。第一,所有功能必须一屏内完成,日历和今日打卡列表同时可见,不要二级导航。第二,交互反馈要即时,点击即打勾、再点即取消,不需要弹窗确认,不做撤销条。换句话说,极简的本质是帮用户省下做决定的成本,而不是简单地把功能删掉。
基于这个产品哲学,日迹的架构可以保持得很干净:本地 JSON 文件存储、ChangeNotifier 做状态管理、自绘日历网格。这也正好为后面拆解 Flutter on OpenHarmony 的细节留出了清晰的观察窗口。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搭建 Flutter for OpenHarmony 开发环境,我踩过的那些坑
2.1 选对 Flutter SDK 分支是第一步
这是第一个大坑,也是很多人卡住的地方:OpenHarmony 上的 Flutter 不是官方 Flutter SDK 直接支持的,你必须切换到社区维护的 fork 分支。我先说明一下我使用的路径,不一定是最新,但思路是对的:从 OpenHarmony SIG 的 Flutter 仓库拉取特定的 release 分支,把它作为 Flutter SDK 根目录,然后配置到 PATH 里。
这里有一个容易搞混的点:你电脑上可能已经装过标准的 Flutter SDK,比如 3.16.9,用来开发 Android。现在你要把 OpenHarmony 分支的 SDK 放到另一个目录,并且在切换项目时改 PATH。我就是因为偷懒,直接在同一个 Flutter SDK 里切分支,结果 flutter doctor 一会儿报 Android 没问题,一会儿又识别不了 OpenHarmony 工程,来回折腾了大半天。
我的建议是单独开一个目录,比如 flutter-ohos-sdk,并且在 shell 配置里用一个环境切换函数,别直接改全局 PATH。具体分支选择可以看 OpenHarmony SIG 的 release 说明,一般会标注支持的 API 版本对应关系。日迹当时用的是适配 OpenHarmony 4.x 的分支,Dart 版本和标准 Flutter 有一定的差异,这会影响依赖版本的选择。
2.2 DevEco Studio、hdc 与设备连接的协作关系
OpenHarmony 应用的构建产物是 HAP 包,不是 APK。你需要安装 DevEco Studio 来提供编译工具链和签名配置。但日迹的主要开发量在 Flutter 侧,你不可能每次都打开 DevEco Studio 去改代码,所以正确的工作流是:用 VS Code 写 Dart 代码,用命令行构建 HAP,再用 DevEco Studio 或者 hdc 工具安装到设备。
hdc 是 OpenHarmony 的调试工具,类似 adb。连接一台 RK3568 开发板或者真机后,最常用的命令有这么几个:
bash复制# 查看设备是否识别
hdc list targets
# 查看系统版本信息
hdc shell param get const.product.name
hdc shell param get const.ohos.version
# 安装 HAP 包
hdc install entry-default-signed.hap
# 查看应用日志
hdc shell hilog | grep -i flutter
这里我想强调一下 param get 命令。很多人拿到一台 OpenHarmony 设备,第一反应是问“系统是哪个版本”,下意识会敲 getprop 或者去设置里翻。实际上 OpenHarmony 的参数系统用的是一个叫 param 的服务,内核参数和系统属性都挂在 const.*、ohos.* 等命名空间下。不同厂商的定制系统还会有自己的字段,但 const.product.name 和 const.ohos.version 是通用的,可以快速确认设备固件是否满足 Flutter 分支的最低要求。
在 RK3568 / RK3588 这类开发板上部署时,还要注意设备的存储空间和 CPU 架构。RK 系列一般是 arm64-v8a,构建 HAP 时架构配置要对应,否则装上去会出现无法解析的安装错误。
2.3 从 Android 工程切到 OpenHarmony 工程的关键差异
一个典型的 Flutter 项目目录里,有 android/、ios/、web/ 这些平台目录。切换到 OpenHarmony 后,你需要额外引入 ohos/ 目录。这个目录不是 Flutter create 自动生成的,而是通过 Flutter for OpenHarmony 提供的插件机制或者命令生成的。
直接说结论:项目根目录的 pubspec.yaml 不变,lib/ 下的 Dart 代码不变,但平台相关配置全部要换成 OHOS 的。最让我困惑的是 Gradle 的报错提示:
code复制You are applying Flutter's main Gradle plugin imperatively using the apply script
这其实是标准 Flutter 的 Android Gradle 配置方式,在 OpenHarmony 分支中是无效的。原因很简单,OpenHarmony 构建 HAP 走的是自己的编译链路,Flutter 插件需要通过 ohos 的插件工程方式注册,而不是通过 Android 的 Gradle apply 机制。所以看到这类报错时,不用纠结 Android 侧怎么修,正确的处理方法是检查项目是否生成了 ohos 目录,以及 ohos 目录下是否有 build-profile.json5。如果没有,就去翻分支仓库里的 sample 工程,把平台壳工程拷过来,再把 Flutter module 的路径配好。
另外,网络热词里频繁出现的 flutter error resolving plugin [id: 'dev.flutter.flutter-plugin-loader'],也属于同类型问题。这个 id 是标准 Flutter 的 Gradle plugin id,OpenHarmony 分支不认识。出现这个报错基本可以断定:你还在用 Android 的构建入口在跑 OpenHarmony 项目,或者项目根目录的 settings.gradle 没有正确切到 OHOS 模式。
2.4 环境变量与版本对齐建议清单
这一节我直接把经验浓缩成一张清单,照着检查可以少走很多弯路:
| 检查项 | 目标值/建议 | 注意事项 |
|---|---|---|
| Flutter SDK 路径 | flutter-ohos-sdk 独立目录 |
别和标准 Flutter 混用 PATH |
| DevEco Studio | 使用与分支要求匹配的版本 | 版本过低会导致 HAP 编译失败 |
| Java 环境 | DevEco Studio 自带的 JBR 或 JDK 17 | 配置 JAVA_HOME 后要多确认 java -version |
| hdc 工具 | 与 SDK 匹配,hdc -v 可运行 |
注意别和 Android 的 adb 冲突 |
| 设备架构 | 构建产物选 arm64-v8a | 在 ohos 工程里确认 abiFilters |
| 日志工具 | 优先 hilog 而非 logcat | Flutter 输出可以用 hilog 过滤 |
| 路由/插件 | 使用支持 OHOS 的 Flutter 插件版本 | 检查每个 plugin 是否有 ohos 实现 |
环境配置这件事,本质上不是看你装了多少工具,而是看你的开发工作流是否闭环:能编译 HAP、能装到设备、能拿到日志、能热重载。四个环节缺一个,后面写代码都会很痛苦。
3. 打卡日历的骨架:数据模型、存储与状态管理
3.1 把“习惯”和“打卡记录”拆开,为什么
日迹的数据模型不复杂,但我坚持拆成两个部分:习惯(habit)和打卡记录(checkin)。直观上,你可能会觉得“在 habits 表里存一个 checkinDays: List<String> 字段就完事了”,但实际跑起来会发现查某一天有哪些习惯没打卡、统计连续天数时非常别扭。
习惯的元数据是低频信息:名称、图标颜色、创建时间、是否归档。打卡记录是高频信息:日期、习惯ID、打卡时间。如果塞在同一张表里,每次点一下都要变更整个习惯对象,还要处理数组的增删排序,序列化和反序列化的开销完全不值得。更关键的是,日历视图需要按日期索引查“今天打了哪些卡”,拆表之后一句 where date = '2025-06-18' 就能拿到,性能和维护性都更好。
我用的 JSON 结构大概是这样的:
json复制{
"habits": [
{ "id": "h1", "name": "喝水", "color": "#4CAF50", "archived": false }
],
"checkins": [
{ "id": "c1", "habitId": "h1", "date": "2025-06-18", "doneAt": "2025-06-18T08:30:00Z" }
]
}
文件存储的格式不需要太纠结,重点是读写时的原子性。我每次写入时是先生成一个临时文件,再 rename 覆盖正式文件,这样可以避免应用在写入过程中被杀死而导致 JSON 文件损坏。这个经验是从 Android 上学来的,在 OpenHarmony 上一样适用。
3.2 日历网格的计算逻辑:别用第三方日历库
日历视图是日迹最核心的 UI。我一开始想过去 pub.dev 找一个现成的 Flutter 日历组件,但调研后发现大多绑定国际化的日期选择器逻辑,而且很多三方库的 OpenHarmony 适配没跟上。对于日迹这种只需要“月视图 + 打卡圆点”的诉求,自绘反而是更优解,代码量不大,逻辑完全可控。
月份网格的计算核心就三类数据:某月第一天是星期几、某月有多少天、今天是几号。Dart 的 DateTime 可以直接处理:
dart复制int daysInMonth(int year, int month) {
return DateTime(year, month + 1, 0).day;
}
拿到第一天后,用 DateTime(year, month, 1).weekday 得到它是第几列。这里的 weekday 返回 1 到 7,代表周一到周日,我习惯把周一当作一周的第一天。然后通过 GridView.builder 渲染一个 7 列网格,在对应 index 位置填充日期号。
打卡状态用 Map<String, Set<String>> 保存:外层 key 是日期字符串,value 集合存当天已打卡的习惯 ID。
dart复制final Map<String, Set<String>> _checkinSet = {};
bool isChecked(String date, String habitId) {
return _checkinSet[date]?.contains(habitId) ?? false;
}
这套模型在内存中的查询复杂度是 O(1),渲染整个月视图只需要遍历当天数据,完全不卡。有人可能会问:为什么不直接在 widget 里反复遍历 checkins 列表?那种写法也能跑,但日历格子数量多,每个格子都要判断“我有没有打卡”,遍历列表的复杂度会让 build 过程变得很拖沓。在设备性能不如旗舰手机的 OpenHarmony 开发板上,这种区别放大得特别明显。
3.3 状态管理选型:ChangeNotifier 够用就不上重量级框架
日迹的状态管理用的是 provider + ChangeNotifier,没有上 Riverpod,也没有上 Bloc。理由很简单:状态量本身就两个,一个是习惯列表,一个是打卡集合。没有复杂的异步依赖,没有表单校验,没有跨页面共享的复杂状态。
我创建了一个 HabitStore 类,里面暴露所有状态变更方法,比如 toggleCheckin(String habitId, DateTime day)。这个方法的内部逻辑是:先判断今天是否已经打卡,如果打了就移除,没打就添加,然后写文件,最后 notifyListeners()。
dart复制class HabitStore extends ChangeNotifier {
List<Habit> _habits = [];
Map<String, Set<String>> _checkinSet = {};
void toggleCheckin(String habitId, DateTime day) {
final dateKey = _formatDate(day);
final set = _checkinSet.putIfAbsent(dateKey, () => {});
if (!set.add(habitId)) {
set.remove(habitId);
}
_save();
notifyListeners();
}
}
为什么不用更重的方案?因为日迹应用启动后的第一件事是读取本地 JSON,这个操作在 OpenHarmony 真机上通常只要几十毫秒。如果你用 Bloc 或者 Riverpod,等于额外引入了一整套抽象层,对一个小工具应用来说是纯负担。
但我必须补一句:状态管理选型也要看团队习惯。如果你日常就在用 Riverpod,用它也没问题。核心不是框架,而是状态变更路径清晰、易测试、性能可控。选择 provider 只是因为它的心智负担最低,贴近日迹的极简风格。
3.4 点击打卡与改签的交互状态机
打卡不是每次都能一次点对的。我设计了一个简单的状态机,保证操作反馈的确定性和可逆性:
- 状态 A:习惯未打卡,日历圆点呈空心。
- 状态 B:习惯已打卡,日历圆点呈实心,并给习惯列表打上对勾。
- 操作一:在日历格子点击某一天,再点击某个习惯,切换 A/B。
- 操作二:在习惯列表点击今日的“打卡圆环”,切换 A/B。
这里有个细节:切换时不允许跨天补卡。日迹的产品定位是“强调当下”,如果你昨晚忘了打卡,今天早上补一个,那记录就失真了。这个限制是刻意的,不做成“历史记录可编辑”,而是让用户翻回之前日期查看时,只有浏览态,没有编辑态。这样交互状态机就不需要处理“补卡是否影响连续天数”之类的边缘问题,逻辑上干净很多。
4. 极简日历 UI 的实现与设计哲学
4.1 日历格子:不画线、只用留白和圆角
日历 UI 是最容易做丑的地方,因为大多数人的第一反应是画 6 行 7 列的表格,然后用边框线把每个格子框起来。日迹的做法反过来了:完全不画网格线,每个日期就是一个带圆角的容器,通过颜色深浅来表达状态。
每个格子的布局是这样的:顶部放一个“日期数字”,底部放该习惯对应的彩色圆点(用 Row 排布,最多显示 4 个,超出显示为省略号)。今天这个日期有一个淡淡的描边,已打卡的日期背景是浅绿色,未打卡但有记录过往的日期背景是灰色低透明度。
这种设计的好处是,视觉焦点自然落在“今天”和“打卡状态”上,而不是先被横七竖八的线框吸引。留白不是浪费空间,而是在告诉用户:这个应用只需要你关注那几个圆点,其他都是陪衬。
在 Flutter 里,这些格子其实就是一个自定义的 StatelessWidget,通过 BoxDecoration 控制圆角和背景色:
dart复制Container(
decoration: BoxDecoration(
color: isToday ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: isToday ? Border.all(color: theme.primaryColor) : null,
),
child: Column(
children: [
Text('${day.day}'),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: dotWidgets,
),
],
),
)
一个容易忽略的问题是 GridView.builder 的 childAspectRatio。日历格子高度如果写死,遇到系统字体缩放或大字体模式时会溢出。我建议用 MainAxisExtent 来指定固定高度,比如 64,而不是用比例值,这样省去大量边界判断。
4.2 动效的克制:什么时候不加动画
日迹的动效非常少,只在三个地方做了手感优化:点击打卡圆环时的缩放反馈、打卡完成后的对勾绘制、局部切换动画(AnimatedSwitcher)。
其他场景一律不加动画,尤其是日历切换月份、打开详情页这类操作,用瞬时切换。为什么?因为习惯打卡的产品场景里,用户是在固定时间点执行一个固定动作,动效过长会干扰操作节奏。日迹的目标是“用户从打开应用到完成打卡不超过 3 秒”,任何超过 300ms 的过渡动画都在拖这个目标的后腿。
缩放反馈我用 GestureDetector 里的 onTapDown 和 onTapUp 配合 AnimatedScale 实现,点击瞬间圆环缩小到 0.9,松手后回弹。这个反馈只有 80ms,感知上是老练的“物理按键手感”,而不是炫技。
在 OpenHarmony 设备上做动画还要注意一点:Skia 引擎的渲染循环在没有系统级优化的情况下,动画帧率和 Android 顶配手机是有差距的。动画越多、越复杂,掉帧概率越大。所以克制动画不只是产品设计理念,也是性能策略。
4.3 主题适配与多设备下的响应式布局
OpenHarmony 的设备形态比 Android 还杂。你既可能在手机上跑日迹,也可能在 RK3568 开发板的触屏显示器上跑,甚至可能在平板形态上跑。所以我必须处理响应式布局。
方案是先用 LayoutBuilder 获取父级宽度,超过 600 逻辑像素时走双栏布局:左侧日历,右侧习惯列表;小于 600 时走单栏布局:日历在上,习惯列表在下。
双栏布局下还有一个联动细节:点击左侧日历的某一天,右侧列表的“打卡状态区”会自动切换到那一天的视图,并高亮显示“查看模式”。到了今天再切回可编辑模式。这个逻辑不算复杂,但需要把当前选中日期放在 HabitStore 里,让日历和列表同时监听。
主题适配我只做了浅色和深色两套,通过 ThemeMode.system 跟随系统。颜色上没做太多自定义,因为日迹的界面元素就那几种:背景、卡片、圆环、文字、强调色。保持颜色数量在 6 个以内,界面基本不会乱。
5. 真机实测与性能调优:一些常见的坑
5.1 热重载在 OpenHarmony 上的表现差异
这是我整个开发过程中最想吐槽的一点。Flutter 在 Android 上热重载几乎是瞬时的,但在 OpenHarmony 分支上,修改 Dart 代码后按 r 键,很多时候 UI 不会刷新,或者刷新后状态错乱。我一度以为是设备问题,后来才发现是这个分支对热重载的支持还没完全跟上。
所以我的工作流变成了:写代码前先想清楚逻辑,尽量一次成型,然后通过完整热重启(按 R)来验证。如果只是微调颜色或间距,热重载偶尔能用;一旦改了状态管理相关的代码,就不要抱有幻想,直接重启。
这里分享一个排查日志的小技巧:如果热重载后页面白屏,先别急着重启应用,在命令行输入 flutter logs 或者通过 hdc shell hilog 看 Flutter 引擎是否有异常输出。很多时候是因为 VM service 连接断了,重新 attach 就好。
5.2 列表卡顿排查:RepaintBoundary 与无意义的 rebuild
日迹的习惯列表在大量习惯(比如 20 个)和 30 天日历同时渲染时,出现过明显卡顿。我用 Flutter DevTools 的 Performance 页面看帧率,发现日历网格几乎每帧都在 rebuild。
根源是我的粗心:在 CalendarView 的 build 方法里,直接读取了 HabitStore 的全部状态,导致任意一次打卡都会触发整棵日历 widget 树重建。虽然有 provider 的 Selector 机制可用,但在 GridView.builder 的 itemBuilder 里不容易精确控制。
解决办法是给每个日历条目加 RepaintBoundary,并且在 HabitStore 里拆分出两个 ChangeNotifier:一个管习惯列表,一个管打卡集合。习惯列表很少变,只有新增/编辑时才触发重建;打卡集合变化时,只让依赖它的小组件重建。这个拆分的收益极其明显,同样 20 个习惯的场景下,帧率从明显掉帧恢复到稳定的 60 帧。
这个坑也提醒了我:不要在 store 里放一个“大杂烩”模型,然后到处 context.watch。状态拆分不是越大越好,而是每个独立变化源对应一个通知器,控件按需订阅。
5.3 中文字体渲染与日期格式化的细节
在 OpenHarmony 上,Flutter 的默认字体回退链和 Android 不同。我在真机上第一次跑起来时发现,部分中文标点和生僻字显示成了方框或者用了错误的 fallback 字体。排查后发现是 TextStyle 里没有明确指定 fontFamily,系统字体匹配回退到了 Noto Sans CJK 的一个较旧版本。
我的处理办法是:在全局主题里显式配置 fontFamily:
dart复制ThemeData(
fontFamily: 'sans-serif',
)
如果应用内允许用户切换字体,那需要把一个自定义字体打包到 assets 里。对于日迹,默认系统字体就够,关键是显式声明,别让引擎瞎猜。
日期格式化方面,日期字符串格式统一使用 yyyy-MM-dd,不要用 yyyy年MM月dd日 作为存储格式。显示时才需要格式化,否则你在排序和字符串比较时会被中文年月日坑死。这里也建议引入 intl 包,但还是那句话:存储用标准 ISO 格式,显示用 intl,两者不要混。
5.4 内存占用和长时间运行的稳定性
日迹的功能简单,但我还是给了它一个“7 天不杀进程”的测试。做法是把应用挂在后台,每天定时打开一次点击打卡,观察内存曲线。
结果发现前 3 天内存很稳定,到第 5 天开始出现缓慢上升。查下来是两个原因:一个是 Observatory / DevTools 的 VM service 在 debug 模式下有内存泄漏,release 构建不存在这个问题;另一个是 ChangeNotifier 的监听器没有在页面销毁时调用 dispose(),导致组件泄漏。
这个教训很实际:release 包才是真正要交付的东西,性能问题要以 release 构建为准。如果只在 debug 模式下分析内存,很容易被引擎日志、服务扩展和慢速断言误导。
另外,在 OpenHarmony 上做后台恢复还有一点和 Android 不一样:应用被系统杀掉后,Flutter 的 onStart / onStop 生命周期映射,需要你在 ohos 壳工程里正确转发 Ability 的生命周期事件。如果没接好,用户切后台再回来,应用可能会白屏,Dart 侧却没有任何异常。排查这个问题时,我最终是给 MainAbility 加了生命周期回调,把 onBackground 和 onForeground 转发到 Flutter 侧的一个事件通道,才恢复正常。
6. 从“日迹”往后走:一些经验和后续规划
6.1 如果把存储从 JSON 升级到本地数据库
日迹现在用的是 JSON 文件存储,习惯数量和打卡记录都少时完全没问题。但如果用户坚持用两年,打卡记录会积累到几千条,JSON 文件会膨胀到几 MB,每次全量读写就会显得笨重。
后续升级路径我有两个选择:一是使用支持 OpenHarmony 的 sqflite 分支,把数据迁到 SQLite;二是用 objectbox,但它在 OpenHarmony 上的适配还不明确。考虑到 OpenHarmony 生态插件的现状,我会优先把数据访问层抽象成一个 Repository 接口,当前实现是 JsonHabitRepository,以后想切 SQLite 时再写一个 DatabaseHabitRepository,业务层不感知变化。
接口抽象这件事要提早做。我在日迹里一开始就是直接操作文件,没做接口,后来想加一个“导出备份”功能时才发现到处都在直接读 JSON,改动面大得多。如果当时多花半小时定义一个接口,后面会省事很多。
6.2 桌面卡片、通知提醒与 Flutter 侧的配合
OpenHarmony 的桌面卡片官方推荐用 ArkTS 写,没法直接用 Flutter 渲染。但日迹可以在卡片上显示“今日打卡进度”,所以需要解决 Flutter 和 ArkTS 卡片之间的数据互通。
最简单的方案是数据落到本地文件,卡片侧通过 @ohos.data.preferences 读取同一个 JSON。Flutter 写入后,再通过广播或定时刷新让卡片更新。这个方案不优雅,但胜在简单可靠。另一种是用 OpenHarmony 的公共事件机制,Flutter 侧发事件,卡片侧接收后刷新,这需要写一个原生插件,工程量上了一个台阶。
日迹目前没做桌面卡片,只做了通知提醒的接口预留。在 OpenHarmony 上,本地通知同样需要原生侧实现,Flutter 侧调用插件。我建议这类系统能力不要试图从 pub.dev 上找现成库,因为适配 OpenHarmony 的往往还没有,自己写一个 20 行的 plugin 壳,比改别人的代码舒服得多。
6.3 我坚持的几条设计原则
做完日迹后,我总结了几条原则,后面做其他应用也会沿用。
第一,离线优先。日迹所有功能在飞行模式下都能完整使用,联网只是未来的可选项。数据主权在用户手里,这个原则对工具类应用尤其重要。
第二,不给用户制造焦虑。连续打卡天数不会在主页上显示一个巨大的数字,也不会有“你已坚持 X 天,不要让链条断掉”的推送。日迹只是记录,不评判。
第三,框架迁移的核心不是“能跑”,而是心智模型是否一致。Flutter for OpenHarmony 能让我把 Dart 的思维模型迁移过来,这才是它最大的价值。如果你只是追求在 OpenHarmony 上跑一个 Demo,那用 ArkTS 的模板工程更快;如果你想保持多端一致的代码资产和开发体验,Flutter 分支对日迹这类中小型应用是真实可用的选择。
第四,把“慢”当作一种设计。很多应用追求让用户多停留,日迹反着来:它希望用户每次打卡后立刻离开。所以界面没有信息流,没有广告位,没有红点提醒。做的时候克制,用户用的时候才会舒服。
最后再说一个个人体会。很多人问 OpenHarmony 值不值得投入,我的看法是:不要在“要不要学”上花太多时间,直接找一个足够小的应用场景上手。日迹这个项目让我把环境搭建、插件适配、生命周期、性能调优、日志排查这些环节都过了一遍,这些经验在任何新平台上都值钱。哪怕你最后不把 OpenHarmony 当主力平台,通过这样一个最小项目,你对 Flutter 的生命周期、渲染边界和存储设计也会理解得更深。
