接到这个需求的时候,我第一时间想到的是:终于有人要在开源鸿蒙生态里做正经应用了。项目标题看起来很简单——flutter_for_openharmony家庭药箱管理app实战+体重记录实现——但拆开来看,里面涉及的东西相当多:Flutter 跨端适配 OpenHarmony、本地数据持久化、列表与图表交互、平台通道桥接、打包签名调试。这篇文章我就按自己实际走过的流程,把这套东西从头到尾捋一遍,包括那些文档里不会写、但你真的会遇到并且卡很久的坑。
先说结论:Flutter 跑在 OpenHarmony 上,这事儿已经可行了,但还没到“开箱即用”的程度。你需要在环境、依赖、渠道三方面额外花功夫。本文的目标读者是两类人:一是想把现有 Flutter 应用迁移到鸿蒙生态的团队,二是从零开始想做一个纯 OHOS 本地应用、但不想碰 ArkTS 的开发者。家庭药箱和体重记录这两个模块,恰好覆盖了“轻量 CRUD + 本地图表 + 系统能力调用”三类典型场景,做完这一套,市面上大多数工具类 App 的核心路径你都能复刻了。
1. 为什么用 Flutter 做 OpenHarmony 药箱应用
1.1 项目缘起与技术选型
家庭药箱管理,本质上是一个“知道家里有什么药、放哪了、过期没”的轻量工具。常见功能无外乎药品列表、过期日期提醒、用药记录。这些功能单独看都不复杂,但组合起来就有一个共性需求:本地数据存储优先、离线可用、轻量交互。而体重记录则更简单一些,核心是输入体重数值、保存历史、画一条变化曲线。
这两个模块为什么要放在 Flutter 里做,而不是直接写 ArkTS?我当时的判断有几点:
第一,Flutter 的 UI 开发效率确实快。鸿蒙生态目前的原生开发以 ArkTS + ArkUI 为主,如果你熟悉的是 Flutter 的 Widget 体系和状态管理,转过去写 ArkUI 会有一段学习成本。而 Flutter 在 OpenHarmony 上的适配层flutter_ohos已经能跑通底层渲染和事件分发,这就意味着你写一套 Dart 代码,可以同时覆盖 Android / iOS / OpenHarmony 三个平台。对于一个家庭工具类应用,这是非常划算的。
第二,这个场景对系统底层能力的依赖较少。药箱管理不需要复杂的推送服务,不需要频繁唤醒系统 API,核心工作都在应用层完成。这类应用恰恰是跨平台框架最擅长的领域。如果你要做的是需要深度系统能力的高性能工具(比如录屏、底层网络抓包),那我不建议用 Flutter 上鸿蒙,除非你愿意花大量时间写平台通道去桥接。
第三,生态位。OpenHarmony 的应用生态目前属于早期,竞争相对小,但工具类应用的需求是刚性的。药箱管理和体重记录都是高频、稳定、长期使用的场景,做出来之后不会变成“一次性 Demo”,而是真的有人会持续用。所以就算跨端适配有些麻烦,也值得投入。
1.2 功能拆解:两个模块的边界划分
我建议在项目一开始就把功能边界划清楚,不要混着写。家庭药箱模块和体重记录模块虽然都在同一个 App 里,但它们的存储模型、页面结构和交互模式完全不同。
家庭药箱模块的核心数据是“药品”,围绕药品有几个子功能:
- 药品添加与编辑(名称、规格、数量、有效期、存放位置)
- 药品列表展示,支持按有效期排序和过期高亮
- 过期提醒逻辑,需要计算当前日期与有效期的差值
- 服药记录(可选,比如记录今天是否吃了某种药)
体重记录模块的核心数据是“体重快照”,它的逻辑非常简单:
- 记录一条体重(数值、单位、当时的 BMI 指数)
- 历史列表展示,支持删除误记录
- 一段时间内的体重变化曲线
这两个模块的 UI 风格也会有差异。药箱管理偏数据型界面,适合列表 + 卡片组合;体重记录偏趋势型界面,适合图表 + 数字展示。后续我会详细说怎么在两个模块之间共享统一的主题和导航,但保持页面逻辑分离。
2. 环境搭建与工程创建,踩过的坑都在这里
2.1 OpenHarmony 侧到底需要准备什么
网上关于“如何用 Flutter 开发 OpenHarmony 应用”的资料很杂,很多是拿 HarmonyOS NEXT 的开发流程来混着说,容易把人绕晕。我理一下真实需要的环境组件,按顺序列出来:
- OpenHarmony SDK:也就是
ohos-sdk,包含 API 和工具链。开发阶段建议选 API 10 及以上的版本,API 12 目前适配度更好。 - DevEco Studio:这是基于 IntelliJ 的 IDE,用来配置 OpenHarmony SDK 路径和打包签名。你也可以用命令行工具,但用 IDE 配置签名省心很多。
- Flutter SDK:必须是你本机的 Flutter SDK,还有一个专门为 OpenHarmony 适配的 Flutter SDK 分支或补丁。这里有个关键点——并不是官方 Flutter SDK 直接就能编鸿蒙,需要给 Flutter 增加一个
ohos平台target。
实际操作时,我在配置 OpenHarmony SDK 时遇到过版本对不上的问题。DevEco Studio 会自带或引导下载 SDK,但如果你本机 Android SDK 和 OpenHarmony SDK 同时存在,路径别搞混了。Flutter 项目里需要额外创建一个ohos/目录来承载 OpenHarmony 工程,这个目录结构类似于 Android 的android/目录。
这里我想重点强调一个容易被忽略的细节:OpenHarmony 和 HarmonyOS NEXT 并不完全等价。OpenHarmony 是开源项目,HarmonyOS NEXT 是商业发行版,虽然底层同源,但开发工具和支持范围有差异。如果你是给普通用户分发应用,需要根据目标平台选择合适的 SDK 和签名证书;如果只是想在开源生态里跑通应用,直接用 OpenHarmony SDK 即可。
2.2 Flutter SDK 版本与 ohos 适配层的坑
如果你使用 Flutter 官方最新版 SDK 去直接跑 OpenHarmony,大概率会看到一条类似警告:The current configured Flutter SDK is not known to be fully supported。这不是编不过,而是 SDK 版本与适配层的支持范围不一致。我在实际开发中用的是 Flutter 3.22 左右的版本配合flutter_ohos适配分支,整体比较稳定;如果你用到 3.24 以上,某些底层渲染可能有回归。
还有一个高频报错,报错信息长这样:
you are applying flutter's main gradle plugin imperatively using the apply script这通常是你在构建ohos目录时,Gradle 脚本里给某个模块手动 apply 了 Flutter 插件,但 Flutter 的 Gradle 插件本身已经通过plugins方式加载,重复应用导致冲突。解决办法是回到ohos/settings.gradle和根目录build.gradle,检查是否有多余的apply语句,改成标准的plugins { id "com.flutter.gradle.ohos" version "..." }配置。
另一个我实际踩过的坑是 Gradle 依赖解析:
Could not determine the dependencies of task ':app:compileDebugJavaWithJavac'. > Could not resolve all task dependencies for configuration ':app:debugCompileClasspath'.这个问题的根源通常是依赖仓库顺序或者缺少特定的鸿蒙构件仓库。肖 fix 方法是在ohos/build.gradle里把mavenCentral()和鸿蒙的仓库地址都加上,并且确保没有把 Android 的仓库地址混进来。OpenHarmony 的构建系统虽然基于 Gradle,但它识别的是ohos插件,依赖解析与 Android 不完全一致。
经验小结:环境搭建阶段最重要的不是写代码,而是把 SDK 版本锁定。建议在项目的README里明确写清楚 Flutter 版本、ohos SDK 版本、DevEco Studio 版本,以及适配分支的 commit 号。不然过一个星期你自己都可能忘了当时用的哪个版本组合,换台电脑大概率会摔跤。
3. 家庭药箱管理核心功能实现
3.1 数据模型设计与存储选型
家庭药箱的数据模型,我最终设计成下面这样:
class Medicine { final int id; final String name; // 药品名称 final String spec; // 规格,例如 0.5g*24片 final int quantity; // 剩余数量 final DateTime? expiryDate; // 有效期 final String location; // 存放位置,例如 客厅药柜第二层 final String? notes; // 备注,例如 饭后服用 }为什么字段要这么设计?核心是抓住“药品管理”场景下的三个关键动作:找得到(location)、看得懂(spec + notes)、记得住(expiryDate + quantity)。
存储方案上,我在 sqlite 和 Hive 之间对比后,最终选择了 sqlite。原因有两点:一是药品数据天然是结构化关系型数据,后续如果要增加“用药记录表”做关联查询,sqlite 直接支持 SQL 语法,扩展起来方便;二是sqflite插件有 OpenHarmony 适配版本,不需要自己写太多原生代码。Hive 更适合键值对密集的场景,但做不了复杂的范围查询,比如“查出所有三个月内过期的药品”,用 SQL 一句WHERE expiry_date BETWEEN ? AND ?就搞定了。
这是我在实际写表结构时的建表语句:
CREATE TABLE medicines ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, spec TEXT, quantity INTEGER DEFAULT 0, expiry_date TEXT, location TEXT, notes TEXT );有一点要提醒:日期字段我建议用 TEXT 存储 ISO8601 字符串,而不是直接用 INTEGER 存时间戳。原因是在 SQL 查询时,TEXT 格式可以直接按字典序比较大小,例如expiry_date > '2025-03-01'是合法且可读的;而时间戳虽然也可以比较,但可读性差,调试时你一眼看不出那条数据是什么日期。
3.2 药品列表、到期提醒与高亮逻辑
药品列表是药箱模块的主界面,我的设计是上下两层结构:顶部是一个横向滚动的“状态统计卡片”区域,下面是按有效期排序的药品列表。
状态卡片展示三个指标:总药品数、临期药品数(30天内到期)、已过期药品数。这三个数字都是通过 SQL 聚合查询得出的,不需要在 Dart 侧写循环统计逻辑。
列表的排序规则是:已过期的最靠前,其次按有效期从近到远正序排列,没设置有效期的排最后。在 Dart 里我写了一个排序函数:
List<Medicine> sortMedicines(List<Medicine> items) { items.sort((a, b) { bool aExpired = a.expiryDate != null && a.expiryDate!.isBefore(DateTime.now()); bool bExpired = b.expiryDate != null && b.expiryDate!.isBefore(DateTime.now()); if (aExpired != bExpired) return aExpired ? -1 : 1; if (a.expiryDate == null) return 1; if (b.expiryDate == null) return -1; return a.expiryDate!.compareTo(b.expiryDate!); }); return items; }每条药品卡片上,我会用背景色来提醒状态:过期药品卡片背景带浅红色、30 天内临期带浅黄色、正常药品维持白色。这里的颜色判断我用了一个独立函数,保证逻辑单一、可测试。
这里有一个交互细节值得讲讲:临期提醒卡片。我一开始做的是只在列表顶部放一行文字“有 X 种药即将过期”,后来发现用户根本不会去看。后来改成在 App 启动时弹一个非阻塞的底部弹窗,列出临期药品名称和剩余天数,体验好了很多。弹窗要设计成可一键跳转到对应药品详情。
3.3 页面导航与状态保持的正确姿势
药箱模块涉及多个页面:列表页、药品详情页、添加/编辑页、过期提醒弹窗页。我在导航上用了Navigator 1.0的经典 push/pop,没有引入go_router。为什么?因为药箱模块的导航路径很浅,基本是两层结构,不需要深链,也不需要路由守卫,手动管理栈反而最可控。
这里有一个群里经常讨论的问题:Navigator 切换页面后,原页面状态会不会丢失?答案是:默认会,但可以控制。
在我的场景中,药品列表页每次从详情页返回时,需要刷新列表数据,因为用户可能在详情页删除了药品或修改了数量。这个刷新逻辑我放在Navigator.push(...).then(...)回调里,返回后重新查询数据库。但对于另一个状态——列表的滚动位置——我希望它保留。实现方式是给ListView加PageStorageKey:
ListView.separated( key: PageStorageKey('medicine_list'), ... )这样在 push 到详情页再返回时,列表滚动位置不会跳到顶部。
如果你的某个子页面内部有 TabBar,并且希望 Tab 之间切换时页面状态不丢失,那就要在 TabBarView 的孩子上包一层AutomaticKeepAliveClientMixin,这个在体重记录模块的“周/月切换”场景里我会用到,等下细说。
4. 体重记录功能的落地
4.1 记录模型与轻量级持久化
体重记录的数据模型非常简单:
class WeightRecord { final int id; final double weightKg; final double bmi; final DateTime recordedAt; }BMI 计算并不复杂,公式是体重(kg) / 身高(m)的平方,但身高是一个用户设置常量,存在SharedPreferences里。这样每次记录体重时,BMI 可以自动算出来,不用用户再手动填。
存储选型上,体重记录我依然选择了 sqlite 的同一个数据库实例,只是单独建一张weight_records表。不建议用 SharedPreferences 存体重历史,虽然数据量不大,但后续你要做“按周筛选”“按月统计”,用 SQL 的WHERE recorded_at BETWEEN ? AND ?效率远高于遍历键值对。
体重的输入界面,我用的是一个全屏的无边框大号输入框,配合自定义数字键盘。为什么不用系统键盘?因为体重输入只需要小数点加数字,系统键盘占屏幕空间大,且容易误触。自定义键盘虽然开发量多一些,但体验提升非常明显。业界很多健康类 App 都是这么做的。
4.2 轨迹走势图:不引重量级图表库的方案
关于体重趋势图,很多人的第一反应是引入fl_chart这种图表库。但我要说:这个功能真的不需要。体重记录一周或者一个月才增加几条数据,用一个自绘的简单折线图完全够用。
我在项目里用CustomPainter画了一个轻量级折线图,展示最近 30 条记录的趋势。核心思路是:将记录列表按时间排序,映射为 Canvas 上的坐标点,然后连接成折线图。画图时需要注意几个细节:
- Y 轴范围不要用“最小体重~最大体重”,那样曲线波动会显得非常夸张,用户看了容易焦虑。合理做法是把 Y 轴范围设置为“最小体重减 2kg”到“最大体重加 2kg”。
- X 轴按时间均匀分布,还是不按时间均匀分布?这里我踩过坑。如果按时间均匀分布,用户某几天连续记录了多条,那几天的点会挤在一起;如果按“实际天数间隔”分布,则更符合直觉,但实现复杂一些。对于 30 条以内的轻量数据,我建议按时间均匀分布,简单且足够清晰。
- 曲线要画平滑线,不要画直连线段。直连线段看起来像心电图,平滑线更像专业健康应用的风格。平滑线可以用贝塞尔曲线实现,不用引第三方库。
4.3 体重记录的列表交互与 Tab 切换细节
体重记录页我设计成分段视图:上半部分是趋势趋势图,下半部分是历史记录列表。历史记录列表支持左滑删除误记录,删除后图表要同步更新。
在一个页面里同时展示图表和列表,这里有一个性能问题:列表滚动时,图表会不会跟着重建?我的处理方式是把图表和数据列表拆成两个独立 Widget,图表用RepaintBoundary隔离,这样列表滚动时 Chart 不会反复重绘。
这里顺便把前面说的 TabBar 场景讲清楚。我最初加了一个“周/月”切换的 TabBar,用来切换图表展示的时间范围。如果 TabBarView 里的每个子页都带一个图表,那默认情况下切换 Tab,图表会重建一次,这没问题;但如果子页面里有列表,并且你希望列表的滚动位置在 Tab 切换后保持不变,就要用AutomaticKeepAliveClientMixin:
class MonthTabView extends StatefulWidget { ... } class MonthTabViewState extends State<MonthTabView> with AutomaticKeepAliveClientMixin<MonthTabView> { @override bool get wantKeepAlive => true; }加了这层之后,Tab 切换时进度保存没问题,但如果你同时想要每次切换到该 Tab 时刷新数据,就得配合VisibilityDetector或者在 TabController 的监听里去手动调用刷新方法。这里是个取舍,实际项目里看你的场景更看重哪一边。
5. 平台交互:EventChannel、MethodChannel 与鸿蒙能力对接
5.1 三种通信渠道,用哪个、什么时候用
Flutter 与鸿蒙原生侧的通信,核心渠道还是那三个:EventChannel、MethodChannel、BasicMessageChannel。这个在移动开发圈是老生常谈,但在 OpenHarmony 上有些差异。
我做了个对比表格,方便你一眼选型:
| 渠道 | 适用场景 | 数据方向 | Flutter 侧体验 |
|---|---|---|---|
| MethodChannel | 一次性请求,例如“获取系统电量” | Flutter 调用原生,原生返回结果 | 返回 Future,适合类似函数调用 |
| EventChannel | 持续的事件流,原生主动推送 | 原生 -> Flutter | 返回 Stream,适合监听类场景 |
| BasicMessageChannel | 双向消息,格式可自定义 | 双向持续通信 | 类似 WebSocket 的体验,适合复杂交互 |
在家庭药箱应用里,我实际用到的其实是 EventChannel:我需要监听系统的“低电量提醒”事件,在低电量时提示用户“记得按时吃药”。这个提醒逻辑不做就会丢一个很有温度的场景。如果只是在 App 内部做提醒,用本地的Timer就够了,完全不需要碰平台通道。
5.2 通过 EventChannel 实时接收系统事件的完整实现
在 OpenHarmony 侧,EventChannel 的实现跟 Android 侧类似,但 API 名称有区别。我在 ohos 目录的MainAbility里注册了一个事件通道:
// 鸿蒙侧 import { EventChannel } from '@ohos/flutter_ohos'; let eventChannel = EventChannel('com.example.medicine/event/low_battery'); eventChannel.setStreamHandler({ onListen: (arguments, eventSink) => { // 监听系统电量事件,通过 eventSink.success(data) 推给 Flutter }, onCancel: (arguments) => {} });Flutter 侧,我封装了一个服务类来订阅这个通道:
class BatteryEventService { static const _eventChannel = EventChannel('com.example.medicine/event/low_battery'); Stream<String> get lowBatteryStream { return _eventChannel.receiveBroadcastStream(); } }需要特别注意的是,EventChannel 的通道名称必须在原生侧和 Flutter 侧完全一致,否则你会遇到事件完全接收不到的情况,且不会有明确报错。调试这个问题的技巧是:先在原生侧用日志确认事件确实被触发了,再在 Flutter 侧打印流事件,逐步排查是“没发出去”还是“没收到”。
5.3 通道命名规范与调试技巧,踩过高频问题都在这
通道命名这件事,看起来是一小步,做不好是打脸的一大步。官方建议是用域名反写的命名方式,例如com.yourcompany.app/channel_name。这种命名在跨端开发时特别重要,因为一个应用里可能有十几个通道,如果不规范,写到后面你自己都分不清哪个是哪个。
我实际遇到过一个问题:Flutter 侧MethodChannel调用鸿蒙原生方法时,返回值偶尔为 null。排查了很久,最后发现是原生侧返回了一个Map,但Map里的 key 是String类型而 Flutter 侧期望的是int类型,类型不匹配导致反序列化失败。经验是:跨端传递数据时,尽量只传 JSON 兼容的纯数据对象(String、num、bool、null、List、Map),不要传自定义对象或特殊类型。这个规则在 OpenHarmony 侧同样适用。
如果你的项目后续要扩展大量平台能力,建议把通道相关的类统一放在一个platform/目录下面,并给每个通道写一个独立服务类。比如BatteryEventService、NotificationService、SensorService。这样业务代码里不会到处散落魔法字符串,后续排查也好定位。
6. 打包、性能与常见问题排查实录
6.1 OpenHarmony 打包流程与权限配置注意事项
OpenHarmony 应用打包和 Android 大致相似,但有几个点不一样,值得单独写出来。
第一个是签名。在 DevEco Studio 里配置好签名证书后,你打包出来的 HAP 或者 APP 文件才能安装到真机上。如果是开发自用,可以用自动签名;如果要分发,需要申请发布证书。这里的细节是:签名配置是跟着 DevEco Studio 项目的,如果你的 Flutter 工程因为ohos目录是后补的,还需要确认 Flutter 工程里的ohos目录能被 IDE 正常识别和关联到 Dart 代码。
第二个是权限配置。药箱应用需要什么权限?坦白说,本地存储和 UI 展示都不需要特殊权限。但如果你加入了“通知栏提醒”功能,就需要在module.json5里声明通知权限。有一个容易踩坑的地方:某些设备上通知权限默认是关闭的,你需要主动引导用户开启。鸿蒙的权限列表和 Android 不完全一致,不要直接把 Android 的权限声明搬过来。
第三个是打包命令。Flutter 构建产物到鸿蒙包,跟在 Android 上略有不同。我在实际项目里是先构建 Flutter 的 AOT 产物,然后由 DevEco Studio 构建 HAP。如果你全程命令行操作,需要注意hvigor的配置,否则容易出现 “无法找到 Flutter 动态库” 这类问题。
6.2 性能优化:Impeller 渲染带来的改变
性能这个话题,几乎每个 Flutter 开发者都会关注。在 OpenHarmony 平台上,Flutter 默认使用的渲染引擎是 Impeller。我在实际体验中发现,打开 Impeller 之后,列表滚动时的帧率稳定度明显更好,滑动过程中的掉帧明显减少,尤其是在药箱列表这种带卡片阴影效果的页面上,视觉效果更平滑。
有一点要提醒的是:Impeller 和 Skia 渲染在某些图形特效上表现不同。我发现在 OpenHarmony 上,如果你的自定义CustomPainter用了复杂的BlendMode(混合模式),Impeller 的表现可能跟 Skia 不同,导致画面出现意外效果。如果你遇到了“自己画的东西跟预期不一致”,先检查是不是 Impeller 导致的。这个排查方向在官方文档里写得不是特别清楚,我自己是被坑过两次才发现的。
另一件值得做的事,是关闭或自定义 TabBar 的点击动画。我看网上好多人问“flutter tabbar点击取消动画效果”,实际做法是在TabBar的indicator配置里去掉动画,或者通过TabController手动控制切换。如果你做的体重记录页有“周/月”切换,动画反而可以保留,因为那是 Tab 切换,不是 TabBar 到 TabBar 的跳转,保留动画会让过渡更自然。但如果你的药箱列表里有“筛选”Tab,那动画就可以去掉,减少不必要的视觉噪声。
怎么自定义呢?最简单的方案是:
TabBar( controller: _tabController, indicator: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(8), ), ... )indicator的默认实现是带平滑动画的,如果你给indicator传了自定义BoxDecoration,动画行为会有所不同,但整体的点击切换动效依然存在。如果你想完全取消动画,需要把TabBar换成自定义按钮组,自己维护选中状态。这个方案最可控。
性能优化的最后一个建议是:列表页的 item 尽量用const构造,并在 item 内避免复用复杂的 GDI 对象。在 OpenHarmony 上这一点跟 Android 一样,但更敏感,因为有些真机的 GPU 性能不如主流手机。我发现药品列表的卡片阴影如果太重,真机上滚动会有一点点噪点感。阴影改为浅色和浅范围后,观感明显干净了。
6.3 常见报错与排查速查表(都是实战中验证过的)
把我在这个项目里遇到过的、以及群里高频出现的报错整理成一张表,方便你对照参考:
| 报错关键字 | 核心原因 | 排查建议 |
|---|---|---|
| Main Gradle Plugin applied imperatively | Flutter Gradle 插件重复应用 | 检查ohos/build.gradle是否有重复apply |
| Could not resolve task dependencies | 依赖仓库配置缺失或顺序错误 | 在ohos/build.gradle中补全仓库地址 |
| Flutter SDK is not known to be fully supported | Flutter 版本与 ohos 适配分支不一致 | 锁 pin 一个已验证的 Flutter 版本组合 |
| Could not close input stream | 构建缓存损坏或者文件占用 | 清理.gradle缓存,重点检查 Windows 环境 |
| 原生回传数据解析失败 | 跨端传递了不兼容类型 | 统一用 JSON 兼容的纯数据类型 |
在这些报错里,最不值得浪费时间的就是 “Flutter SDK is not known to be fully supported” 这一类警告。它只是一个提示,告诉你当前组合未经官方完整验证,但不代表不能用。我一开始看到这个警告就疯狂换版本,后来发现完全没必要,只要功能正常、测试通过,该用还是用。但是,如果你打包时遇到报错,而且报错信息里出现了 SDK 版本字样,那就要认真对待版本一致性问题了,这是两码事。
结尾的几点实在话
这个项目做下来,最大的体会是:Flutter 上 OpenHarmony 并不是“把 Android 的配置改名复制一份”就行,它需要你对 Flutter 的跨端抽象有更深的理解,尤其是平台通道和构建链路的差异。家庭药箱和体重记录这两个模块,难度不算高,但胜在覆盖面广,能帮你把 Flutter + OpenHarmony 的主流开发路径整个走通一遍。如果后面你有更多想法,比如把药箱数据通过云同步到家人手机上,或者加入体脂、血压等更多健康指标的记录,这个项目的地基是能撑得住的。留在手里的这套代码和这份踩坑清单,起码能让你下次起新项目的时候,少走一整晚的弯路。