
去年把团队里跑在 Android 上的 Flutter 智慧养老 App 迁移到了 OpenHarmony 上,重点做了护理服务模块。项目本身不是“Hello World”级别的 Demo,而是要真正应对养老机构里护理员排班、上门服务、健康数据采集、异常上报、家属通知这一整条业务闭环。这篇文章不打算谈大而全的架构,而是把我在 OpenHarmony 上从零跑通 Flutter、用 Platform Channel 调系统能力、通过 EventChannel 推任务状态、以及踩过的那些真机调试坑,完整复盘一遍。内容比较适合正在做鸿蒙生态迁移、又不想把现有 Flutter 工程推倒重来的团队参考。1. 项目整体设计与技术选型1.1 为什么是 Flutter 而不是纯 ArkUI智慧养老场景有个天然痛点:用户设备极其分散。护理员手里可能是 Android 手机,老人床边是 OpenHarmony 的智慧屏,管理端又有可能是 PC 浏览器。如果每个终端都用原生的 ArkUI 写一套,UI 和交互逻辑至少要维护三份,后期需求变更的成本非常高。我选择把 Flutter 作为核心跨端方案,不是因为 Flutter 比 ArkUI 强,而是因为项目里已经有大量沉淀好的 Flutter 组件和业务代码。在 OpenHarmony 上跑 Flutter,本质上不是把 Flutter 当成替代品,而是把它看作“可复用的 Dart 业务层”。实际上我在工程里做了很明确的隔离:所有页面、状态管理、数据模型、接口抽象都写在纯 Dart 层,不依赖任何平台独有能力;只有涉及定位、蓝牙、通知、传感器这类系统能力时,才通过 Channel 去调用 OpenHarmony 原生侧。这样设计的好处是,如果某一天要适配到其他平台,业务层可以原封不动地搬过去。坏处也很明显,OpenHarmony 上的 Flutter 生态不如 Android 成熟,很多原生插件需要自己封装,前期会有一些工作量。1.2 服务模块的分层思路护理服务模块我没有按页面去拆,而是按能力去拆。整体上分成三层:应用层:任务列表、工单详情、健康测量、异常上报、语音提醒等页面。业务层:护理工单状态机、待办计算、离线数据补传、通知规则等纯 Dart 逻辑。能力层:通过 MethodChannel 调用 OpenHarmony 的定位、蓝牙、通知、剪切板、传感器等能力,通过 EventChannel 接收来自原生侧的任务状态推送。每一层之间依赖是单向的。应用层只管 UI 渲染和用户交互,业务层不 import 任何package:flutter/material.dart,能力层只暴露抽象接口。实际编码时,我用了 Riverpod 做状态管理,没有选择 Bloc,原因很简单:护理任务的状态流转不是单纯的事件流,还掺杂着本地数据库读写和离线同步,用StateNotifier Provider组合更直观。这个分层帮我解决了一个很实际问题:OpenHarmony 和 Android 的插件实现并不一样,但应用层代码完全不用改。后来给 Android 加一个拍照上传图片的功能时,只需要在能力层多写一个实现类,UI 和业务逻辑完全复用。2. 核心功能拆解与服务链路设计2.1 护理工单状态机与触发链路护理服务的第一条主线是“工单”。护理员看到的不是一堆零散任务,而是经过排班系统生成的一条条护理单。最开始需求方只提了一个“已办/待办”列表,后来上线测试才发现不行,护理员经常会漏掉某个房间的血糖测量。所以我自己把工单状态拆成了五个节点:状态含义触发动作待接单排班已生成,护理员未确认管理员排班后自动创建已接单护理员确认接单点击“接单”按钮服务中护理员已到达现场开始服务扫码签到或定位签到已完成服务项全部执行完毕上传服务记录并确认已取消任务作废管理员取消或护理员申请这个状态机的好处是,后续所有通知逻辑都可以挂在状态变化上。比如“已接单”以后给家属推一条“护理员已出发”,“服务中”以后推一条“正在为老人测量血压”。事件推送我用的是 EventChannel,而不是轮询接口。轮询在服务中状态频繁变化时很容易漏消息,而且耗电;EventChannel 是持续性的双向通道,非常适合这种任务节点推进的场景。状态机里最容易忽略的是“超时”处理。我在每个状态节点都加了超时时间,比如“待接单超过 15 分钟未响应”就自动推送给管理员。实际运行下来,这个功能比我想象的更能提升响应率,因为养老机构的早班护理压力很大,很多护理员确实会忘记看手机。2.2 健康档案与异常上报的联动护理服务的第二条主线是“健康数据”。老人在服务过程中心率、血压、血氧这些数据不能只是采集完就丢,必须跟历史档案联动。我把数据结构设计成“一次服务产生一份护理记录,一份记录包含多个测量项”。这样数据库表就不会越写越乱,而且一个服务周期结束以后,可以很自然地生成健康趋势图。异常上报是这里最需要注意的模块。它不是简单的表单提交,而是要触发一系列动作:写入异常事件表、更新护理单状态、推送消息给值班医生和家属、以及在护理员端弹出一条醒目的确认提示。我在实现时特意把异常等级分成“普通提醒”和“紧急求助”两类。普通提醒只做红点提示,紧急求助则会在 App 顶部持续置顶,直到有人确认处理。这个设计来源于一次真实教训:最开始把所有异常都做成弹窗,结果护理员在服务过程中频繁被弹窗打断,反而影响操作。后来把等级拆开,普通提醒静默化,紧急求助高优展示,投诉率立刻降下来了。2.3 适老化体验的一些细节智慧养老 App 的使用者并不全是老人,但老人会通过床旁终端看服务进度,所以适老化不能只做表面功夫。我在 Flutter 层做了三个硬性要求:字体跟随系统缩放,同时页面内支持手动放大到 1.4 倍;所有核心操作按钮的点击区域不小于 48dp,并且按钮之间间距大于 8dp;每个关键卡片使用Semantics控件标注语义内容,配合读屏软件能读出“任务时间、老人姓名、注意事项”。这里有个坑:OpenHarmony 上系统字体缩放和 Flutter 的适配并不完全一致。同一套代码在 Android 上放大字体不会溢出,但在部分 OpenHarmony 设备上会出现文本截断。最后我在全局设置了MediaQuery.withClampedTextScaling来限制最大缩放比例,同时在列表项里用maxLines和Ellipsis做了兜底。这类视觉细节看起来不起眼,但在老年用户那里就是“看得清”和“看不清”的区别。3. 实战:从环境搭建到核心模块落地3.1 OpenHarmony 上跑通 Flutter 的完整环境配置先说环境。OpenHarmony 上的 Flutter 并不是直接在官方flutter/flutter里默认支持,而是需要拉社区维护的 OpenHarmony Flutter fork。我用的版本是 3.7 系列的 OpenHarmony 分支,配合 DevEco Studio 里的 OpenHarmony SDK。第一步先把 SDK 环境变量配好:export DEVECO_SDK_HOME/path/to/deveco/sdk export OHOS_SDK_HOME$DEVECO_SDK_HOME/10 export PATH$PATH:$DEVECO_SDK_HOME/openharmony/toolchains然后把 fork 下来的 Flutter SDK 路径加进 PATH:git clone https://gitee.com/openharmony-sig/flutter_flutter.git git checkout release-version export PATH$PATH:/path/to/flutter_flutter/bin flutter doctor注意flutter doctor在这个环境下不一定全绿。我第一次跑的时候 OpenHarmony 工具链那一栏是空的,后来发现是因为DEVECO_SDK_HOME指错了层级。不要把 SDK 根目录直接配到openharmony子目录,要配到包含toolchains的那一层。环境配好以后创建工程:flutter create --platforms ohos --org com.example smart_care_app生成成功的话,项目里会多出一个ohos目录。如果没出现,大概率是 fork 分支切错了,或者flutter create版本太新。我遇到过一种情况:直接 clone 的分支默认只支持 Android 和 iOS,必须切到 OpenHarmony 对应的 release 分支再创建。工程跑起来的第一个里程碑,不是做页面,而是先跑通一个空 Flutter 页面在真机上的渲染。这一步如果失败,后边加再多代码都是白搭。渲染引擎用 Flutter 自带的 Impeller 模式还是 Skia 模式,在部分低端 OpenHarmony 设备上差别很大。我建议做真机验证时先默认配置跑一遍,如果出现花屏或闪退,再尝试通过启动参数关闭 Impeller,改用兼容更好的渲染路径。3.2 用 MethodChannel 调起 OpenHarmony 定位与通知能力护理员上门服务时需要一个“签到”动作,最合理的方式是定位签到。这个能力在 Flutter 层不能直接拿,得通过 MethodChannel 调原生。Dart 侧的封装很简单:import package:flutter/services.dart; class LocationService { static const MethodChannel _channel MethodChannel( com.smart_care.ohos/location, ); FutureMapString, double getCurrentLocation() async { try { final result await _channel.invokeMethodMap(getLocation); return { latitude: (result?[latitude] as num?)?.toDouble() ?? 0, longitude: (result?[longitude] as num?)?.toDouble() ?? 0, }; } on PlatformException catch (e) { // 这里做统一异常收集,避免 UI 层直接看到平台错误 throw CareLocationException(e.code, e.message); } } }原生侧用 ArkTS 实现时,核心是注册 MethodChannel 并处理invokeMethod。我以定位为例写一个精简版:import { geoLocationManager } from kit.LocationKit; import { MethodCall, MethodChannel } from ohos/flutter_ohos; const channel new MethodChannel(com.smart_care.ohos/location); channel.setMethodCallHandler(async (call: MethodCall) { if (call.method getLocation) { const location await geoLocationManager.getCurrentLocation(); return { latitude: location.latitude, longitude: location.longitude, }; } return null; });注意不要在setMethodCallHandler里做长时间同步操作。OpenHarmony 的原生调用很多是异步回调,必须用 async 或返回 Promise,否则 Flutter 侧会一直等不到结果。另外一个高频能力是通知。护理员接单、老人异常上报、家属提醒都要用到系统通知。这里我踩过一个坑:OpenHarmony 的通知需要申请权限,且不同设备类型的权限弹窗文案不一样。如果权限被拒绝,通知会静默失败,不抛异常。排查这种问题非常痛苦,所以在 Dart 侧我封装了一个ensureNotificationPermission方法,专门检测权限状态,并在 UI 里提示用户去设置页打开。3.3 用 EventChannel 持续接收任务状态护理任务从“已接单”到“服务中”再到“已完成”,是一个持续变化的过程。如果用 MethodChannel 做推送,原生侧想主动通知 Flutter 侧是很别扭的。EventChannel 的设计就是用来干这个的。Dart 侧监听:import package:flutter/services.dart; class TaskStatusStream { static const EventChannel _channel EventChannel( com.smart_care.ohos/task_status, ); StreamString startListen() { return _channel .receiveBroadcastStream() .map((event) event.toString()) .handleError((Object e) { // 断线重连逻辑建议放在这里 return streamOnError(e); }); } }原生侧推送事件:import { EventChannel } from ohos/flutter_ohos; const eventChannel new EventChannel(com.smart_care.ohos/task_status); eventChannel.setStreamHandler({ onListen(arguments, eventSink) { taskManager.onTaskStatusChange((status) { eventSink.success(status); }); }, onCancel(arguments) { taskManager.removeListener(); }, });用 EventChannel 时最需要注意的就是重连。真机上网络切换、App 退到后台再回来,EventChannel 有可能断掉。我在业务层加了一个简单的状态同步策略:每次从后台回到前台时,调用一次fetchCurrentTaskStatus拉取全量最新状态,再和增量事件做合并。这样即使 EventChannel 丢了一两个事件,UI 也能通过主动拉取兜底。3.4 离线数据持久化与补传机制养老机构有个很现实的情况:电梯、地下室、老旧房间的 Wi-Fi 和移动信号都很差。护理员在服务过程中如果断网就录不了数据,那体验会很灾难。所以我把“保存动作”分成两步:先写本地,再上传云端。本地存储我用的是 sqflite 的 OpenHarmony 适配分支,当然如果你不想引入额外依赖,OpenHarmony 原生的关系型数据库也可以封装成 Flutter 插件。核心逻辑是这样:class CareRecordRepository { final Database _db; Futurevoid saveCareRecord(CareRecord record) async { await _db.insert(care_record, record.toMap()); // 写入补传队列 await _db.insert( sync_queue, {record_id: record.id, synced: 0, retry_count: 0}, ); } }补传队列需要一个独立的调度器,不能放在 UI 层。我在网络恢复时监听连接状态,然后批量取出synced 0的记录,按时间顺序上报。上报成功后更新标记,失败则累加retry_count,超过 5 次后转入人工处理列表。这套流程不复杂,但很依赖数据库事务。如果写入主表和队列不是同一个事务,很容易出现主表有数据、队列没记录导致永远不同步的情况。第一次做这个功能时我忽略了事务,结果测试时发现有两单记录丢失,排查了很久才发现是队列写入失败但没有回滚主表。后来所有写操作都包在db.transaction里,这个问题就再没出现过。4. 真机调试与问题排查实录4.1 高发问题速查表OpenHarmony 上跑 Flutter,很多问题跟 PC 上的模拟器表现完全不同。我整理了一张速查表,都是自己真机调试踩过的:问题现象可能原因解决办法flutter create没有生成 ohos 目录fork 分支不对或 SDK 未识别切换到对应的 ohos release 分支构建时报 OpenHarmony SDK 版本不匹配DEVECO_SDK_HOME 配错层级重新检查 SDK 路径,定位到 toolchains 上级真机首次启动白屏渲染引擎兼容性问题尝试关闭 Impeller,更新 GPU 驱动定位一直返回 0权限未授予或原生模块异常单独写一个测试页面调原生定位,排除 Flutter 层问题通知发送无提示通知权限被静默拒绝检查设置页通知权限,补齐二次引导页面切换后 Tab 状态丢失未使用 KeepAlive 机制使用AutomaticKeepAliveClientMixin或IndexedStack这里面最坑的是“通知发送无提示”。因为原生侧代码没有报错,Flutter 侧也收到了成功回调,但用户就是收不到推送提示。后来我把通知权限状态查了一遍,才发现权限弹窗和安装来源有关,部分设备上第一次没点同意,之后系统不再弹窗。最后我在 App 的“设置”页加了通知权限检测,每次冷启动时检查一次,如果未授权就提示用户手动打开。4.2 内存和性能优化的一次排查护理服务模块里有一个页面是“今日任务时间轴”,会一次性展示当天所有护理单,每张护理单里又能展开老人健康趋势图。第一版测试时,打开这个页面内存占用直线上升,监听到退出页面后内存也不释放。我逐段排查,问题出在健康趋势图的数据缓存上。为了快速重绘,我把每个老人的历史曲线数据全部加载进内存,并且没有设置缓存上限。小数据量没问题,但一个养老护理机构几百个老人,每个老人又有几十条测量记录,内存很快就爆了。解决思路是分级缓存:当前正在服务的老人数据全部加载;非当前服务对象只加载最近 7 天数据;超过 7 天的数据在用户滚动到对应位置时再按需加载。同时在 Flutter 侧调整了ImageCache的缓存上限。护理员头像、老人照片这些图片如果不限制缓存,全加载出来的话内存会比数据曲线还夸张。改完之后,时间轴页面的内存占用降了大约 40%,退出页面后也基本能回到初始水平。还有一个容易被忽略的性能点是列表里的高频率重建。护理任务状态一变化,整张列表setState刷新,导致本不该动的卡片也跟着重建。我后来把每条任务卡包装成独立的ConsumerWidget,用 Riverpod 监听单个任务的状态,其他卡片就不会再被重建。这个改动对低端 OpenHarmony 设备特别友好,滑动明显更跟手。4.3 Flutter 与 ArkUI 混合栈的跳转坑项目里并不是所有页面都用 Flutter 重新写了一遍。有些模块,比如系统设置、权限管理、设备绑定,用了原生 ArkUI 页面。这就会出现 Flutter 页面和 ArkUI 页面互相跳转的情况。第一个坑是页面返回和生命周期不一致。从 Flutter 跳 ArkUI 页面,返回时 Flutter 侧如果没有收到正确的生命周期回调,就可能会导致列表不刷新。我在跳转前通过 MethodChannel 告诉原生侧“等待返回结果”,原生侧在 finish 后回调具体的结果数据,再在 Flutter 侧根据返回码刷新页面。第二个坑是路由栈混乱。如果在 Flutter 侧用Navigator.push开了好几层页面,再跳到 ArkUI,返回时容易回到错误层级。我的处理比较粗暴:所有跨栈跳转都走一个统一的路由 Bridge,桥接到原生侧时使用完整的路由栈管理,避免 Flutter 和 ArkUI 各自维护一套 navigate 逻辑。混合栈的调试也比纯 Flutter 要麻烦。因为断点有时会落在 Dart 侧,有时又落在 ArkTS 侧,两边日志格式还不一样。我在能力层加了一套统一的调试日志输出,把时间点、模块名、事件名按固定格式打出来,再通过日志过滤单独看 Flutter 桥接记录。排错效率会高很多。4.4 热重载与多端适配的边界认识在 OpenHarmony 上做 Flutter 开发,热重载是一个一定要提前做好心理准备的点。部分设备支持热重载,但稳定性不如 Android。我见过最多的是:代码改动以后按r,界面没有变化,但控制台也没有报错。这个时候不要一直重试,先把 App 停掉重跑一次。如果重跑以后正常,那多半是热重载引擎没跟原生环境同步上,而不是代码本身的问题。多端适配方面,智慧养老场景会同时遇到手机、平板、智慧屏三端。Flutter 的响应式布局能解决大部分问题,但 OpenHarmony 上不同设备的窗口缩放比例差异比想象中大。我建议在工程里把MediaQuery当成一等公民来对待,不要用固定宽高写卡片。平板和智慧屏上,同样一个护理任务卡片,展示的信息密度应该不一样。这一点没有捷径,只能在真机上逐台调。这个项目做下来,我最大的感受是:在 OpenHarmony 上用 Flutter,不能只带“跨端”这一把锤子。真正能落地的核心是把 Dart 业务层和平台能力层彻底分开,同时花心思处理适老化、离线同步、状态事件这类非功能需求。如果你也在做类似迁移,我的建议是别急着全量铺开,先挑两个高频护理场景,比如接单和健康测量,从端到端跑通。把 OpenHarmony 上那几个跟 Android 表现不一致的地方摸清楚,才是这个项目真正绕过弯路的开始。