ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

OpenHarmony上跑Flutter:猫咪喂食计算器从0到1

OpenHarmony上跑Flutter:猫咪喂食计算器从0到1 做 Flutter 开发这几年我一直在关注它在非传统平台上的落地情况。去年手上接了一个宠物类 App 的案子目标平台是搭载 OpenHarmony 的国产设备客户点名要 Flutter 技术栈需求里最核心也最吸引我的一个模块就是猫咪管家里的喂食计算器。这篇文章就把这个功能的完整实现过程拆开讲透从技术选型到状态管理从公式逻辑到真机踩坑全程记录我实际操作中的选择和取舍。聊这个项目之前先说结论Flutter for OpenHarmony 已经不是概念验证阶段了只要 SDK 分支用对、构建配置补齐日常业务的开发体验和 Android 端几乎一致。喂食计算器又是一个典型的逻辑可测试、UI 可复用的模块非常适合作为跨端迁移的突破口。无论你是刚听说 OpenHarmony 适配还是已经在摸索 Provider 做状态管理这篇实战记录应该都能给你一些能直接抄作业的东西。1. 项目整体设计与技术选型1.1 为什么在 OpenHarmony 上选 Flutter 而不是 ArkTSOpenHarmony 官方的主推语言是 ArkTS基于 TypeScript 的方舟语言生态也在快速补全。但客户这边的情况很现实团队里没有一个专职的 ArkTS 开发App 主体代码是现成的 Flutter 工程需要同时覆盖 Android 和欧拉系设备。重新用 ArkTS 写一套人力成本至少要翻两倍。这种场景下Flutter for OpenHarmony 几乎就是唯一解。后来跟几个同行交流过大家对这个方案的顾虑集中在两点一是渲染引擎适配二是插件生态。渲染这块Flutter 在 OpenHarmony 上的适配由社区 SIG 在推进核心的 engine 已经移植到了 ohos 平台Impaeller 渲染后端也能跑通。插件生态确实比 Android 少但底层的平台通道是完整的原生能力缺失时可以通过 MethodChannel 自己封装后面我会讲具体怎么做的。选 Flutter 还有一个隐性好处UI 一致性。OpenHarmony 设备的屏幕尺寸和交互逻辑跟手机不完全一样但 Flutter 的布局系统天然跨端统一不用针对某个平台单独写布局适配代码。对猫咪管家这种偏工具属性的 App 来说一套代码跑三种设备收益非常明显。1.2 喂食计算器的功能边界与需求拆解跟客户聊需求的时候对方一开始给的描述很模糊就是输入猫的信息告诉主人一天喂多少。这种需求如果直接开做后期一定会被细节磨死。我拉了一版更细的需求清单把计算器分成了三个层次第一层是基础输入项体重kg、年龄阶段幼猫/成猫/老年、绝育状态已绝育/未绝育、活动量偏低/正常/活跃。第二层是输出项每日推荐喂食量g/天、单次喂食量按一日三餐拆分、每日建议饮水量。考虑到用户不一定只喂一种粮我把猫粮的代谢能kcal/100g也做成了可输入的参数默认给一个常见区间值。第三层是扩展信息如果猫的体重明显偏离正常范围计算器要给提示比如偏胖建议逐步减量而不是干巴巴地给个数字。这个边界梳理清楚之后整个模块的开发量就非常可控了核心是一个纯 Dart 的计算类外加一个基于 Provider 的状态容器外加一个结果展示页面。UI 和逻辑完全分离这也是我后面敢直接单元测试喂食算法的主要原因。1.3 技术方案选型Provider、纯 Dart 计算层与平台通道状态管理我选了 provider而不是 riverpod 或者 bloc主要是因为它学习曲线平缓、模板代码少适合这种中小型模块。riverpod 的编译期安全确实更强但项目里其他模块还在用 provider保持统一更利于维护。计算逻辑单独抽成一个FeedingCalculator类不依赖任何 Flutter 框架代码。这样做的好处是单测跑得快、逻辑可复用以后如果出手表端或小程序端直接把这个类拿过去用就行、也方便在 UI 之外做边界测试比如体重为 0、极端体重等。平台通道Platform Channel主要用来做两件事读取设备上安装的猫粮品牌数据库通过原生代码访问本地 SQLite以及后续可能用到的摄像头扫描猫粮袋上的二维码。第一版我先用 MethodChannel 实现了前者后者用 camera 插件评估过在 OpenHarmony 上的稳定性决定留到二期再做。2. 核心算法猫咪喂食量计算逻辑2.1 从营养学公式到代码模型猫咪每日喂食量不是拍脑袋定的目前兽医营养学里最通用的计算路径是两步法先算静息能量需求RER再乘以生活阶段系数得到每日能量需求DER。RER 的公式是70 乘以体重的 0.75 次方单位是 kcal/天。这个公式是从哺乳动物代谢规律里来的基础代谢跟体表面积的关系比跟体重更接近线性而体表面积近似于体重的 0.75 次方。代码实现很直接double _rer(double weightKg) { return 70 * math.pow(weightKg, 0.75).toDouble(); }DER 的计算则是拿 RER 乘系数。下面这组系数是我从多个兽医营养资料来源里汇总出来的也是这个计算器最核心的知识资产猫咪状态每日能量需求系数幼猫4 个月以下2.5幼猫4~9 个月2.0成年未绝育1.4成年已绝育1.2老年猫7 岁1.1减重目标0.8代码里我用了一个枚举加一个 switch 结构来映射enum CatLifeStage { kittenUnder4Months, kitten4To9Months, adultNeutered, adultIntact, senior, weightLoss } double _lifeStageFactor(CatLifeStage stage) { switch (stage) { case CatLifeStage.kittenUnder4Months: return 2.5; case CatLifeStage.kitten4To9Months: return 2.0; case CatLifeStage.adultNeutered: return 1.2; case CatLifeStage.adultIntact: return 1.4; case CatLifeStage.senior: return 1.1; case CatLifeStage.weightLoss: return 0.8; } }待办事项里有一个细节减重目标的系数不是一个固定值更严谨的做法是基于目标体重来算而不是当前体重。我在第一版先用了 0.8 的保守系数界面上提示逐步减量、勿骤减后续迭代再接入目标体重逻辑。2.2 猫粮代谢能换算与分餐建议算出 DERkcal/天之后还没法直接告诉用户每天喂多少克因为不同猫粮的能量密度不一样。市面上常见的干粮代谢能在 350~450 kcal/100g 之间浮动换算成常见包装营养表中的单位就是 3.5~4.5 kcal/g。double dailyGrams der / (foodMetabolizableEnergy / 100);这里有一个新手很容易踩的坑不要把包装袋上的粗蛋白或粗脂肪直接当成能量密度。正规猫粮包装上标的代谢能或热量值才能用来计算没有的话可以按 4.0 kcal/g 这个中间值先估。分餐建议方面我按三个年龄段做了差异化推荐幼猫建议一天 4 餐尤其是 4 个月以下的小猫胃容量小、需要更频繁进食成年猫建议一天 2~3 餐老年猫如果活动量低建议维持 3 餐但每餐减量防止饥饿感过强导致讨食行为。这个部分在 UI 上用一个时间轴样式展示几点喂、每餐多少克一眼就能看明白。2.3 饮水量计算与异常体重识别猫咪饮水量的估算公式相对简单正常状态下每公斤体重每天需要 40~60ml 的水。如果是湿粮喂养湿粮本身含水量在 70%~80%可以相应扣减。我在计算器里给出的是建议总饮水量和需额外补充水量两个值。异常体重识别用的是一个简化版体况评分BCS通过体重和年龄综合判断。比如一只成年绝育猫体重如果超过 6kg通常就偏胖了但如果是缅因猫这种大体型品种这个标准又不适用。所以第一版我只做了规则提示不做硬性判定界面文案是该体重偏高于常见室内猫水平建议咨询兽医做体况评估。3. Flutter for OpenHarmony 的工程配置与实操3.1 环境准备Flutter SDK 分支与 DevEco StudioOpenHarmony 的 Flutter 支持并不是官方主干直接带的需要使用社区维护的 flutter_flutter 仓库分支。我用的是 httpse 上 OpenHarmony SIG 发布的 tpc 版本对应 Flutter 3.7 系列。环境配置的几个关键点Flutter SDK 下载分支后要把flutter doctor跑通确认 ohos 平台被识别。如果执行flutter doctor时看不到 ohos一般就是分支下错了或者没拉子模块。OpenHarmony SDK 需要用 DevEco Studio 单独安装版本跟目标的 API 级别要对齐。我用的 API 9 CanvasKit 分支构建工具链是 hvigor。创建项目时用flutter create --platforms ohos如果命令不识别手动在既有工程里补ohos目录也行但容易漏生成必要的原生配置文件不推荐。提示不要用 Android Studio 来打开 ohos 目录它识别不了 hvigor 构建脚本。用 DevEco Studio 打开工程根目录或 ohos 子目录才是正路。3.2 项目创建与目录结构项目初始化完成后目录结构大概是这样的cat_manager_app/ ├── lib/ │ ├── main.dart │ ├── models/ │ │ └── cat_profile.dart │ ├── services/ │ │ └── feeding_calculator.dart │ ├── providers/ │ │ └── feeding_provider.dart │ └── pages/ │ └── feeding_calculator_page.dart ├── ohos/ │ ├── entry/ │ │ └── src/main/ │ ├── build-profile.json5 │ └── hvigorfile.ts └── pubspec.yaml注意pubspec.yaml里的依赖我只加了 provider、intl 和 mathmath 是 Dart 自带。后面因为要读取原生 SQLite 数据库额外加了sqflite_ohos社区插件这个插件走的就是 MethodChannel在 OpenHarmony 上表现比预想的稳。3.3 状态管理实现Provider 通信实践喂食计算器的交互流程是用户调整输入项页面实时刷新结果。用 Provider 的思路就是让一个ChangeNotifier持有用户输入的CatProfile和计算结果UI 通过context.read或context.watch来读写。CatProfile模型我设计成了不可变对象所有字段 final每次修改返回新实例这样调试时状态变化更清晰也不会出现共享引用导致的脏数据class CatProfile { final double weightKg; final CatLifeStage stage; final bool isNeutered; final ActivityLevel activityLevel; final double foodKcalPer100g; const CatProfile({ required this.weightKg, required this.stage, required this.isNeutered, required this.activityLevel, required this.foodKcalPer100g, }); CatProfile copyWith({...}) { ... } }Provider 里定义了三个方法updateWeight、updateStage、updateNeutered、updateActivity和updateFoodKcal。每次调用都会重新计算喂食量然后notifyListeners()。这里要注意的一点是不要在 UI 里手动调用calculate方法应该让它成为 Provider 内部的副作用这样可以避免手势滑动和计算结果不同步的问题。class FeedingProvider extends ChangeNotifier { CatProfile _profile; FeedingResult? _result; FeedingResult? get result _result; void updateWeight(double w) { _profile _profile.copyWith(weightKg: w); _recalculate(); } void _recalculate() { _result FeedingCalculator.calculate(_profile); notifyListeners(); } }3.4 UI 层实现与结果卡片展示页面布局我用的是上下分栏上半部分是输入区下半部分是结果区。Slider 做体重输入1~15kg分段按钮做年龄阶段选择Switch 做绝育状态切换底部有一个 TextField 允许用户手动输入猫粮的代谢能。结果区用了一个自绘的能量环组件用CustomPainter画出环形进度条中间显示每日建议克数。这个视觉元素在 Android 和 OpenHarmony 上表现一致因为 CustomPainter 走的是 Flutter 自己的渲染引擎不依赖原生控件。关键代码结构如下Widget build(BuildContext context) { final result context.watchFeedingProvider().result; return Column( children: [ _buildInputSection(context), if (result ! null) _buildResultCard(context, result), ], ); }context.watch是 provider 包里的核心手法它建立了依赖关系当 Provider 通知变化时只有 watch 了对应数据的 widget 会重建其他部分可以继续复用 Element性能开销可控。我做了一版用 InheritedWidget 手写状态管理来做对照组代码量多了一倍不止而且容易在 dispose 时机上踩坑。Provider 之所以在中小项目里这么流行本质上就是因为它把依赖订阅这件事封装得足够稳。3.5 在 OpenHarmony 真机上构建与安装构建命令和 Android 不太一样OpenHarmony 的安装包是有签名要求的。hvigorw assembleHap这条命令会在entry/build/default/outputs/下生成 .hap 包。安装到设备有两种方式一种是 DevEco Studio 一键运行自动签名很方便另一种是命令行方式hdc install entry/build/default/outputs/default/entry-default-signed.haphdc 等价于 adb是 OpenHarmony 的设备调试工具在 DevEco Studio 的 SDK 目录里能找到。注意裸的 hap 包没有签名信息的话hdc install大概率会报error: install failed due to invalid signature。开发阶段用 DevEco 的自动签名即可发布前再申请正式签名。4. 常见问题与排查技巧实录4.1 构建阶段Gradle 插件与 hvigor 的纠缠热词里有一条很典型you are applying flutters main gradle plugin imperatively using the apply。这是在 Android 工程里集成 Flutter 模块时的报错意思是 Flutter 的 Gradle 插件必须用plugins {}声明式应用不能直接用apply命令式加载。OpenHarmony 上虽然没有 Gradle但思路是相通的hvigor 的插件配置也必须声明式写在hvigorfile.ts里。当时我遇到的一个报错是flutter ohos plugin not found检查后发现是 hvigorfile.ts 里漏配了 flutter 插件的依赖。解决方式是重新执行flutter create --platforms ohos .来补全配置而不是手动硬改构建文件。如果你是从旧版升级过来的工程flutter pub get之后一定要检查ohos/.flutter-plugins文件是否生成了这个文件缺失会导致所有插件和平台通道静默失效表现就是运行正常但 MethodChannel 调用一直超时。4.2 运行阶段Dart VM 初始化崩溃热词里还有一条让我印象很深Unhandled Exception: [error:flutter/runtime/dart_vm_initializer.cc(41)]。这个错误我在 OpenHarmony 真机上遇到过第一反应是代码问题但排查了一圈发现跟业务代码无关。这个报错的典型原因是应用的 AOT 模式与实际加载的 Dart VM 不匹配。在 OpenHarmony 上如果你用 debug 模式构建的 hap 包带的是 JIT 运行库但在设备上以 release 方式启动Dart VM 初始化就会在dart_vm_initializer.cc卡住。解决方案是统一的debug/release 构建配置。在 DevEco Studio 里运行调试版就全程用 debug 模式命令行签发布包之前先把--release参数显式传给 flutter 构建命令。经验是少混用构建模式就不会出现这个问题。flutter build hap --release hvigorw assembleHap4.3 渲染引擎Impeller 与 OpenHarmony 的兼容性Flutter 3.10 之后 Impeller 在 iOS 上成了默认渲染引擎这个热词 assoc 到 OpenHarmony 上就带出了兼容性问题。在部分搭载国产 GPU 的设备上Impeller 的 Vulkan 后端可能出现花屏或掉帧。我遇到的情况是在 OpenHarmony 模拟器上一切正常换到真机后动画掉帧严重尤其是 CustomPainter 的能量环部分。排查到最后直接禁用了 Impeller改用 Skia 渲染// main.dart void main() { if (Platform.isLinux) { // OpenHarmony 上的禁用方式某个版本后统一走环境变量或命令行参数 } runApp(const CatManagerApp()); }具体禁用方式跟 Flutter SDK 版本相关。3.7 分支可以直接在ohos/entry/src/main/ets/entryability/EntryAbility.ets里添加配置let flutterInstance new flutter.FlutterEngine(context, { enableImpeller: false });Skia 渲染虽然比不上 Impeller 的现代图形管线但在 OpenHarmony 设备上稳定性是第一位的动画性能损失在小型工具类 App 上几乎感知不到。顺带说一句slint这个 UI 框架我也研究过它是纯 Rust 实现的渲染性能很猛但生态和 Flutter 完全不在一个体量。除非你的团队已经有 Rust 背景否则我不建议在 OpenHarmony 跨端项目里冒险选它。4.4 XTS 认证与设备适配的注意点OpenHarmony 设备如果要上架官方应用市场需要通过 XTS 认证其中有一项是兼容性测试。如果你的 Flutter 应用要在这些设备上稳定运行有几个点提前注意会省很多事权限声明OpenHarmony 的权限模型跟 Android 不一样。比如读取存储权限要在module.json5里声明命令行和 UI 弹出的允许是两个层级。屏幕适配OpenHarmony 设备里有一些是带外设的比如带键盘的平板Flutter 默认的MediaQuery能拿到正确的安全区域但键盘弹起时的 resize 行为需要测试覆盖。生命周期Flutter 的 AppLifecycleState 在 OpenHarmony 上目前能正常派发但部分设备的分屏恢复事件可能触发状态重建Provider 的状态在 onRebuild 后要能兼容恢复。5. 单元测试与计算精度控制模拟用户手滑把体重滑到 0.5kg 以下或者 15kg 以上计算器给出的结果要符合直觉。这一节我把边界测试和精度控制的经验一起讲了。FeedingCalculator设计成纯 Dart 类之后测试写起来非常痛快。下面这个用例覆盖了最基础的绝育成年猫在正常体重下的喂食量test(已绝育成年猫 4kg常规猫粮 400kcal/100g, () { final profile CatProfile( weightKg: 4.0, stage: CatLifeStage.adultNeutered, activityLevel: ActivityLevel.normal, foodKcalPer100g: 400, ); final result FeedingCalculator.calculate(profile); // RER 70 * 4^0.75 ≈ 198 // DER 198 * 1.2 ≈ 238 // dailyGrams 238 / 4.0 59.5g expect(result.dailyGrams, closeTo(59.5, 0.1)); });计算过程的精度控制我用了一个小工具函数统一处理小数点位数避免 UI 展示59.499999999这种尴尬double roundTo(double value, int places) { final mod math.pow(10.0, places); return (value * mod).roundToDouble() / mod; }半公斤以上的体重我用 step0.1 的 Slider配合显示一位小数交互上是够用的。有不少用户反馈说为什么体重只能调0.1不能输入0.05这就是溺爱式需求了MVP 阶段不值得为它改变量级。6. 扩展思路与经验总结项目到 v1.0 验收的时候客户很满意但我知道这个计算器还有很大的优化空间。后续迭代可以加的目标事项按优先级排序接入猫粮扫码OpenHarmony camera 插件、体况评分BCS 视觉识别、多猫数据管理、以及把单位从每天细化到每周分装餐盒的批量计算。如果哪天真要做我会优先把FeedingCalculator重构成一个基于状态机的模块但第一版最重要的任务是把计算准确、体验顺手这块地基打牢。最后说一个我个人的实操体会在 OpenHarmony 上用 Flutter 做业务模块真正的门槛其实不是代码本身而是心态。你会在构建配置文件上花掉比写业务多一倍的时间会在设备兼容性排查上消耗耐心但只要把环境理顺你会发现 Flutter 的跨端优势在 OpenHarmony 上发挥得淋漓尽致。猫咪管家 App 的喂食计算器只是一个小小的切入点它证明了一件很关键的事情Flutter 的这套统一 UI 和逻辑开发模式放在 OpenHarmony 上也是能住人的。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进