ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flutter 跨端适配 OpenHarmony 实战:家庭药箱与体重记录实现

Flutter 跨端适配 OpenHarmony 实战:家庭药箱与体重记录实现 接到这个需求的时候我第一时间想到的是终于有人要在开源鸿蒙生态里做正经应用了。项目标题看起来很简单——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 flutters 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 里我写了一个排序函数ListMedicine sortMedicines(ListMedicine 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加PageStorageKeyListView.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 切换后保持不变就要用AutomaticKeepAliveClientMixinclass MonthTabView extends StatefulWidget { ... } class MonthTabViewState extends StateMonthTabView with AutomaticKeepAliveClientMixinMonthTabView { 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); StreamString 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 imperativelyFlutter Gradle 插件重复应用检查ohos/build.gradle是否有重复applyCould not resolve task dependencies依赖仓库配置缺失或顺序错误在ohos/build.gradle中补全仓库地址Flutter SDK is not known to be fully supportedFlutter 版本与 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 的主流开发路径整个走通一遍。如果后面你有更多想法比如把药箱数据通过云同步到家人手机上或者加入体脂、血压等更多健康指标的记录这个项目的地基是能撑得住的。留在手里的这套代码和这份踩坑清单起码能让你下次起新项目的时候少走一整晚的弯路。
返回列表