☰
Flutter for OpenHarmony实战:从喂食记录到定时提醒的完整开发指南
2026/10/7 3:22:25 网站建设 项目流程

作为一个白天写业务代码、晚上回家还要伺候猫主子的开发者,我一直想给自家那只一到饭点就喵喵叫的橘猫做个喂食管理工具。最初的想法很简单:记录它什么时候吃了、吃了多少、下次该几点喂。但真正动手时发现,选技术栈这件事本身就够折腾一轮。后来我把目光放在了 Flutter for OpenHarmony 上,用一套 Flutter 代码把猫咪管家App跑在了 OpenHarmony 设备上,其中“添加喂食”这个核心功能从数据建模到界面实现再到定时提醒,完整走了一遍。这篇博文就把整个实战过程摊开来讲,包括环境搭建、状态管理、存储方案、通知触发逻辑,以及我在真机调试时踩过的几个印象深刻的坑,希望能给同样在 Flutter 和 OpenHarmony 之间徘徊的同学一些参考。

1. 几个关键决策:为什么是“Flutter + OpenHarmony + 猫咪管家”

1.1 跨平台框架那么多,为什么偏偏选 Flutter

做 OpenHarmony 应用,市面上最主流的路线其实是直接用 DevEco Studio 加 ArkTS 写原生应用。这条路的问题在于,如果你手头已经有一套 Flutter 代码,或者团队成员更熟悉 Dart,那迁移成本会非常高。我当时手里正好有一个之前在 Android 上写过的猫咪管家原型,UI 和逻辑都在 Flutter 里,与其在 ArkTS 里重新实现一遍,不如直接调研 Flutter 在 OpenHarmony 上的适配情况。

实测下来,Flutter for OpenHarmony 的适配层已经能把大部分原生 Flutter 能力映射到 OpenHarmony 的 API 上,基础的 Widget 渲染、手势系统、路由管理都能正常工作。对于猫咪管家这种以表单、列表、状态管理为主的工具类 App,Flutter 的跨端优势是实打实的:我不用维护两套 UI,ArkTS 那边只需要关心 OpenHarmony 特有的系统能力接入,比如后续要接摄像头识别猫咪进食行为时,再通过原生通道去调 OpenHarmony 的相机接口就好。

1.2 猫咪管家App的核心功能边界

猫咪管家这个项目我规划了四个功能模块:猫咪档案管理、喂食记录、定时提醒、健康数据统计。本文聚焦的是“添加喂食”这条完整链路,因为它是整个App里最能体现“数据—状态—UI—系统能力”协同的部分。

“添加喂食”看起来只是填个表单点保存,实际上拆开之后包含这些子问题:喂食记录的数据结构怎么设计、保存之后怎么通知列表刷新、喂食时间到了怎么触发提醒、App 被杀了之后提醒还能不能生效。这每一个子问题在 OpenHarmony 平台上都有一些跟 Android 不一样的细节,后面我会逐个拆解。

1.3 一次“添加喂食”操作背后的数据流全景

先给出一张流程视角的图景:用户在添加喂食页选择食物类型(猫粮、罐头、零食)、输入克数、设置喂食时间、填写备注,点击保存后,数据写入本地存储,同时更新全局状态,喂食记录列表立刻出现新条目。如果用户设置了“定时喂食”提醒,则向系统注册一个提醒代理。整个过程涉及 UI 层、状态管理、存储层、系统提醒服务四个环节,每一层我都做了单独的封装。

2. 环境准备:跑通 Flutter for OpenHarmony 的完整流程

2.1 开发工具链的版本组合

要在 OpenHarmony 上跑 Flutter,先要明确一个概念:你用的不是普通 Flutter SDK,而是 OpenHarmony 适配版的 Flutter SDK。社区里一般叫它 flutter_flutter 的 OpenHarmony 分支,或者直接叫 Flutter for OpenHarmony。我搭建环境时用的组合是:DevEco Studio 4.0 以上版本 + OpenHarmony SDK API 10 + Flutter OpenHarmony 适配版 SDK。这里有一个容易被忽略的点:DevEco Studio 自带的 SDK Manager 里需要额外安装“OpenHarmony”那一栏的系统库,而不是 HarmonyOS 的,因为两者的 API 差异会对 Flutter 适配层的编译产生影响。

我当时最先踩到的坑就在这里:OpenHarmony SDK 装得不全,导致 Flutter 工程在编译原生部分时找不到ohos相关的 Gradle 插件依赖。这个问题不是你 Flutter 代码写错了,而是底层 C++ 与原生桥接层的编译环境没准备好。

2.2 Flutter 工程里如何加入 OpenHarmony 平台支持

普通 Flutter 工程默认只有 android、ios 两个平台目录,OpenHarmony 支持通常是以额外的平台目录形式集成进去的。具体做法是:在项目根目录执行适配版 Flutter SDK 提供的命令,生成ohos平台目录,然后把这个目录作为一个原生工程导入 DevEco Studio。

flutter create --platforms=ohos .

如果你的适配版 SDK 不支持这个参数,就需要手动创建一个空的ohos目录,并配置build.gradle、ohos-package.json、module.json等文件。这一步我建议直接用官方模板,手写容易遗漏module.json里的abilities声明,一旦漏了,安装到真机上会直接报“找不到入口 Ability”,App 根本起不来。

2.3 验证 Hello World 一定要用真机

OpenHarmony 的模拟器体验一直一般,尤其是涉及相机、通知这类系统能力时,模拟器的行为跟真机差别很大。我建议从一开始就直接连真机调试。在 DevEco Studio 里配置好设备连接后,依次检查:设备是否开启 Developer Mode、hdc(HarmonyOS Device Connector)能否识别设备、Flutter 的ohos运行配置是否指向了正确的 entry module。

跑通 Hello World 之后,先别急着写业务代码。我习惯先验证两个基础能力:Flutter 页面能正常渲染,以及MethodChannel能成功调用 OpenHarmony 的原生接口。这两个通了,后面加功能才不慌。我用一个最简单的通道验证:Flutter 端调原生返回当前系统版本号,能弹出来就说明桥接层没问题。

3. 猫咪管家App的数据建模:喂食记录不是简单存个时间

3.1 FeedRecord 模型设计中的字段取舍

“添加喂食”首先要有地方放数据。我定义了一个FeedRecord模型,字段如下:

class FeedRecord { final String id; final String catId; final String foodType; // 猫粮 / 罐头 / 零食 final int amountGram; // 喂食克数 final DateTime feedTime; // 喂食时间 final String note; // 备注 final bool isScheduled; // 是否定时提醒 FeedRecord({ required this.id, required this.catId, required this.foodType, required this.amountGram, required this.feedTime, this.note = '', this.isScheduled = false, }); }

字段设计时有几个细节值得说一下。id我用的是时间戳加随机数拼接,保证离线状态下也能生成唯一主键;amountGram用int而不是double,因为日常喂食克数基本是整数,用整数还能减少后续统计时的浮点误差;isScheduled是给列表页用的,用来区分“这条记录当时设了提醒”,后续用户取消提醒时也要同步改这个字段。catId是为了将来多猫家庭扩展,虽然我这个项目目前只有一只橘猫,但多猫场景在真实用户里很常见。

3.2 存储层:OpenHarmony 上选 Preferences 还是数据库

喂食记录的数据量不会特别大,一天几十条顶天了,所以存储方案我没有一上来就上大型数据库。OpenHarmony 本身提供了一套轻量级偏好存储,类似 Android 的 SharedPreferences,适合存简单的键值对。但如果只存 JSON 字符串,每次添加喂食都要把整个记录列表读出来、改完再写回去,数据量到几百条之后性能会明显下降。

我的做法是分两层:第一层用偏好存储保存“当前选中猫咪”这类轻量配置,第二层用 OpenHarmony 的关系型数据库接口存喂食记录。关系型数据库虽然要多写不少建表和数据访问代码,但胜在支持条件查询,后续做“按日期统计喂食总量”这种需求时,一条 SQL 就能搞定,不用把数据全部读出来在内存里过滤。建表语句大致是这样:

CREATE TABLE IF NOT EXISTS feed_record ( id TEXT PRIMARY KEY, cat_id TEXT NOT NULL, food_type TEXT NOT NULL, amount_gram INTEGER NOT NULL, feed_time INTEGER NOT NULL, note TEXT, is_scheduled INTEGER DEFAULT 0 )

注意feed_time我存的是毫秒级时间戳,而不是日期字符串。原因很简单:时间戳在排序、范围查询、时区换算上都是最优解,显示层再转成YYYY-MM-DD HH:mm完全来得及。

3.3 用 Provider 搭起全局状态,让列表页自动感知新记录

存储层负责持久化,但页面之间要能实时联动,还缺一层内存状态管理。这里我用的是 Flutter 社区最常用的 Provider。选它而不是 Bloc 或者 Riverpod,主要考虑项目规模:猫咪管家这种中型 App 用 Provider 已经足够,团队成员上手成本也低,不至于为几十个页面引入一套过于复杂的状态机。

我的具体分工是这样的:FeedRepository负责跟数据库打交道,FeedListModel是一个继承ChangeNotifier的类,内部持有当前喂食记录列表和加载状态,然后通过ChangeNotifierProvider挂在 Widget 树顶层。添加喂食页保存成功后,只需要调用FeedListModel.addRecord(),列表页里用Consumer监听的组件就会自动刷新。

这个设计解决了“页面A添加数据,页面B要自动更新”的典型组件通信问题。后续如果你要加“喂食后自动弹出下一次建议时间”之类的功能,也只需要在FeedListModel里加一个计算属性,而不需要动 UI。

4. 添加喂食的完整实现:从表单到通知触发

4.1 添加喂食页面的交互拆解

页面交互我按“所见即所得”的原则来设计:顶部是一个大的食物类型选择区,用三个卡片横向排列(猫粮、罐头、零食),选中状态通过颜色和图标区分;中间是克数输入框,配置了数字键盘;下面是一个时间选择器和一个备注输入框;底部是大的保存按钮。

UI 上有一个容易忽略的细节:克数输入框的校验逻辑,我要求必须是 1 到 500 之间的整数,小于 1 或大于 500 都直接弹提示。因为猫咪一次进食量如果超过 500 克,大概率是用户输入错误,与其让脏数据进库,不如在表单层拦截。时间选择器我默认给当前时间,因为大部分用户是“喂完了记一笔”的场景,少部分才是提前设置定时提醒,默认当前时间能减少一步操作。

4.2 表单提交的逻辑链路和两条关键校验

保存按钮的onPressed绑定的是_submitForm方法。它的核心逻辑如下:

Future<void> _submitForm() async { if (_selectedFoodType == null) { _showToast('请选择食物类型'); return; } if (_amountController.text.isEmpty) { _showToast('请输入喂食克数'); return; } final amount = int.tryParse(_amountController.text); if (amount == null || amount <= 0 || amount > 500) { _showToast('喂食克数必须在1到500之间'); return; } final record = FeedRecord( id: '${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(9999)}', catId: _currentCatId, foodType: _selectedFoodType!, amountGram: amount, feedTime: _selectedTime, note: _noteController.text.trim(), isScheduled: _isScheduleEnabled, ); await _feedListModel.addRecord(record); if (record.isScheduled) { _reminderHelper.registerFeedReminder(record); } Navigator.of(context).pop(true); }

这段代码里有两条关键的校验:第一,克数必须先tryParse再判断范围,只判断非空是不够的,用户可能输入字母或者负数;第二,保存成功后才去注册提醒,如果数据落库失败,不应该产生一条“幽灵提醒”。这也是我在实际开发中常见的顺序问题:很多人先注册提醒再存数据库,结果数据库写入失败,提醒却已经挂在系统里了,到点就会弹一条不存在的喂食提醒,体验非常糟糕。

4.3 喂食记录列表与下拉刷新的联动

添加完记录返回列表页,列表要能立刻看到新数据。这里我用Consumer<FeedListModel>包裹列表组件,只要FeedListModel的notifyListeners()被触发,列表就会自动 rebuild。列表项上会显示食物类型图标、克数、时间,以及一个“已提醒”的小标签。

列表页另外加了RefreshIndicator做下拉刷新,这个不只是做做样子。因为数据存储层跟内存状态之间没有做自动同步,极端情况下(比如多设备登录或者数据被系统清理)内存里的记录可能和数据库不一致。下拉刷新会重新从数据库加载一次数据,保证看到的一定是最新的。

RefreshIndicator( onRefresh: () => _feedListModel.reloadFromDatabase(), child: ListView.builder(...), )

这里有个细节:RefreshIndicator在列表内容不满一屏时,下拉手势很难触发。解决方式是在ListView上设置AlwaysScrollableScrollPhysics,否则列表项只有五六个的时候下拉刷新会非常难用,你以为是功能坏了,其实是滚动物理特性默认值在作怪。

4.4 定时提醒:Timer方案与系统提醒代理方案怎么取舍

“添加喂食”里最有系统集成感的一步是定时提醒。我在项目里同时实现了两种方案:

方案一是纯 Flutter 侧 Timer 加本地通知。用户在 App 内时,用一个Timer延时到目标时间,弹一个 Flutter 内部的弹窗或通知。优点是纯 Dart 实现,完全跨端,不需要理解 OpenHarmony 的 API;缺点是 App 一旦被系统清理或者进程被杀,Timer 就失效了,提醒永远不会触发。

方案二是走 OpenHarmony 的提醒代理能力,也就是ReminderAgentManager。它把提醒任务注册到系统侧,由系统统一调度,App 进程不在也能到点触发。区别就像你自己定闹钟和请别人到点打电话叫你:前者手机没电就没戏,后者即使你关机,对方到点还是知道该提醒。

我的最终做法是两者结合:App 在前台时用方案一,体验更即时;同时把提醒通过MethodChannel注册到 OpenHarmony 系统侧,保证后台被杀之后依然能触发。注册的时机就是上面_submitForm里的_reminderHelper.registerFeedReminder(record)。

方案进程被杀后是否生效实现复杂度适用场景
Flutter Timer + 本地通知否低App 前台使用的兜底提醒
OpenHarmony 提醒代理是中用户关闭 App 后的准点提醒

注册系统提醒有一个新增弹窗权限的坑:OpenHarmony 对通知权限管理比较严格,第一次调用相关接口时需要动态申请ohos.permission.PUBLISH_AGENT_REMINDER权限,如果用户拒绝了,后续提醒注册会静默失败,不会抛异常。这一点一定要在 UI 上给用户明确的反馈,否则用户会以为设置了提醒但永远等不到通知。

5. 实战中绕不开的坑:我真机跑起来的四次报错

5.1 Gradle 插件配置的经典报错

真机调试第一个遇到的坑就是这个报错信息:you are applying flutter's main gradle plugin imperatively using the apply s...。原因很直接:Flutter 在 OpenHarmony 平台上的 Gradle 集成方式和 Android 不完全一样,适配版 Flutter 要求以插件方式声明式地应用 Gradle 插件,而旧模板里还是用apply命令式写法。修复方式是把外层build.gradle里的apply换成plugins块:

plugins { id "com.flutter.gradle" version "1.0.0" }

如果你的项目里还有第三方插件也用同样的命令式写法,需要统一改成声明式,否则会出现“插件被重复应用”之类的连锁报错。

5.2 Impeller 渲染引擎在 OpenHarmony 上的兼容性问题

Flutter 3.10 之后默认启用 Impeller 渲染引擎,但在 OpenHarmony 适配版上,Impeller 的支持并不完整,我在跑列表滚动时遇到了明显的渲染毛刺和偶发闪屏。排查下来发现是 Impeller 在 OpenHarmony 的 GPU 驱动上做某些着色器编译时会出错,官方推荐暂时关闭 Impeller,回到 Skia 渲染。

关闭方式是在 Flutter 启动参数里加:

--no-enable-impeller

或者直接在 AndroidManifest / module.json 里配置对应的 Flutter 引擎开关。这也是为什么很多 OpenHarmony 上的 Flutter 应用跑起来总感觉跟 Android 上“画风不太一样”,其实就是渲染引擎不同导致的。这个问题不算致命,但如果你发现列表动画或者页面转场特效异常,第一反应应该是检查渲染引擎而不是怀疑自己代码写错了。

5.3 Provider 在使用状态管理时容易“丢状态”

有段时间我遇到一个诡异问题:添加喂食完成后返回列表页,数据偶尔会消失,再次进入才恢复。排查后确认是路由跳转时的问题。我当时用了Navigator.push跳转到添加页,在添加页内部通过Provider.of<FeedListModel>(context)拿到的是同一个实例,理论上没问题。但添加页在键盘弹起、页面重建等场景下,如果 Provider 的create方法写得对,状态不会丢。真正让我丢状态的原因是:我在列表页的didChangeDependencies里做了多余的reloadFromDatabase,和Consumer的刷新逻辑互相竞争,导致界面先显示空数据,异步加载完成后再恢复正常。

解决办法是把数据加载统一收敛到FeedListModel的初始化方法里,页面层只负责“监听变化”和“触发用户操作”,不要每个页面各自为政去加载数据。这个教训的核心是:状态管理工具只是帮你共享数据,数据加载的职责边界还是要你自己定清楚。

5.4 Flutter AAR 集成方式与纯 Flutter 工程怎么选

OpenHarmony 上接入 Flutter 还有一种方式是 Flutter AAR,即把 Flutter 引擎打包成 AAR 库,由原生 ArkTS 工程引入。这个方案适合你已经有一个比较完整的 OpenHarmony 原生应用,只是想把某个 Flutter 页面嵌进去,而不是整个应用都用 Flutter 写。猫咪管家一开始我也考虑过这种方式,因为想着后面可能要用原生摄像头做猫咪识别,但实际评估后发现,纯 Flutter 工程配合 MethodChannel 就能解决原生能力调用问题,没必要维护一个更复杂的混合工程结构。

AAR 方式的另一个问题是调试体验差:每次改 Dart 代码都要重新打 AAR、重新编译原生工程,迭代速度明显比纯 Flutter 工程慢。如果你不是有强制的原生集成需求,我建议先跑通纯 Flutter 工程,原生能力通过方法通道逐个扩展。

5.5 新建项目跑不起来的网络与缓存问题

最后说一个新手几乎必踩的坑:Flutter 新建项目后第一次运行,卡在 Gradle 下载依赖一动不动,甚至直接报超时。OpenHarmony 的 Gradle 插件和依赖大多从公网仓库下载,部分地区网络状况不好的时候非常痛苦。我的经验是先配置镜像仓库,在build.gradle里把仓库地址替换为可用的镜像,同时把 Gradle Wrapper 的版本固定到适配版 SDK 验证过的版本,不要用最新的 Gradle,因为最新版和旧版插件经常不兼容。

另外一个额外提示:DevEco Studio 的构建缓存有时候会把旧的编译产物混进来,导致你改了 DART 代码但真机上还是旧版本。遇到这种情况时,不用慌,清理掉ohos目录下的build和.gradle缓存目录,重新构建一般就能解决。

6. 写在最后:猫咪管家后续可以这样扩展

做完“添加喂食”这条链路之后,整个 App 的骨架已经立住了。我给自己的扩展计划是两个方向:一是接入 OpenHarmony 的 HDI 能力,通过硬件接口读取智能猫碗的重量传感器数据,让“吃了多少”从手动输入变成自动感知,这才是喂食记录该有的样子;二是接入摄像头识别猫咪的进食行为,记录每天的进餐时长和频次,结合喂食数据做简单的健康趋势图。这两个方向都会涉及更多原生的东西,但好消息是基础架构已经有了:数据层、状态层、UI 层已经解耦,到时候最多只是再加一个方法通道和几个原生插件的事情。

最后分享一个我个人的小体会:跨平台开发最怕的不是写功能,而是环境问题。Flutter for OpenHarmony 的坑,很大一部分集中在环境搭建和渲染引擎上,真正写业务逻辑的时候反而很顺畅。如果你也打算在 OpenHarmony 上做 Flutter 应用,建议把版本组合、构建缓存、渲染引擎这些基础项先固定好,再开始写业务代码。反正我建这个猫咪管家项目踩完一轮坑之后,现在再让我在 OpenHarmony 上从零搭一个 Flutter 工程,半小时之内能搞定,希望你看完这篇也能少走些弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询