首页 / 资讯中心 / 文章详情

Flutter for OpenHarmony:智慧养老App紧急联系人模块实战解析

Flutter for OpenHarmony:智慧养老App紧急联系人模块实战解析 ★ FEATURED ARTICLE
在智慧养老App里“紧急联系人”听起来就是个通讯录加拨号功能但真正推到设备上做真机验证你会发现它跟普通社交App里的好友列表完全是两回事。它背后牵扯权限申请、平台通道、后台限制、误触防护、老年代交互适配一系列工程问题。最近我用 Flutter for OpenHarmony 做了一整套现代智慧养老App的实战把紧急联系人模块从需求评审一路推到真机运行中间踩了不少坑想把关键设计思路和排错记录整理出来。如果你也在评估OpenHarmony生态下的跨端方案或者正在做养老、健康类的应用这篇文章应该能帮你省下不少调研和联调的时间。1. 需求分析与方案选型1.1 紧急联系人不是“快捷拨号”很多产品经理会把紧急联系人做成系统通讯录里的快捷拨号觉得只要点一下就能打电话任务就完成了。真正拿到养老场景里这个功能的要求会复杂得多。老人可能跌倒后说不出话、走失后说不清位置也可能因为手抖误触。所以紧急联系人模块至少要有三个能力第一是一键触发最好不用解锁屏幕也能唤出第二是多联系人轮询第一个打不通自动切到下一个第三是附带位置信息让家人不仅知道“谁打的”还知道“人在哪”。另外还有一个被忽略的需求联系人名单必须固化在应用本地。不能每次实时读取系统通讯录因为老人可能在家庭成员的协助下才设置好列表读取系统通讯录意味着要处理一堆权限弹窗而且老人容易误选。更合理的方案是让家人在App里手动维护一组紧急联系人存到本地界面上的展示也做得足够大、足够清晰。我在需求评审阶段把这条链路画成了一张很直白的图用户按下SOS - 弹出确认并倒计时 - 调起拨号能力 - 播放等待音 - 失败后切换下一位 - 成功后把定位信息以短信或服务端通知发给联系人。这个流程里每一步都有技术选型问题后面会详细讲。1.2 为什么选 Flutter 来做 OpenHarmony 应用自研一套OpenHarmony原生应用当然可以但是如果你和我一样手里同时要维护Android和OpenHarmony两个平台的版本原生双端开发的人力成本会直接翻倍。Flutter最大的优势是UI自绘、逻辑层跨端一致OpenHarmony社区也已经有了可用的Flutter运行时。也就是说业务代码、状态管理、路由跳转这些可以沿用熟悉的Dart生态只有系统能力调用需要针对平台差异做适配。我这次选型时不光考虑了成本还考虑了团队接手难度。Flutter的代码可读性比较好一个刚入门的开发也能很快看懂页面结构和数据流。而OpenHarmony原生的ArkTS写法虽然文档很全但短期内不是所有团队成员都熟悉。紧急联系人这种功能业务不复杂但系统能力调用多用Flutter做UI层把拨号、定位、通知这几项封装成平台通道反而更清晰。当然也要说句公道话Flutter在OpenHarmony上毕竟不是一等公民。很多pub仓库里的插件没有针对OpenHarmony实现所以不能无脑把依赖拉进来必须先确认该插件是否支持当前平台。这个决策会直接影响后面开发节奏我建议在项目初期就拉一个“插件能力清单”把每个需要调用的原生能力列出来标注是否支持、是否需要自己封装避免中途才发现某个能力没有现成方案。1.3 整体技术框架与模块划分为了让这个模块可维护我没有把所有代码塞进一个页面文件而是做了简单分层。最上层是UI包括联系人管理页和首页的SOS大按钮中间是状态管理层用Provider处理触发状态、联系人列表、拨打进度再往下是数据层负责联系人列表的读写和JSON序列化最底层是平台能力层通过MethodChannel封装拨号、发短信、获取定位等系统能力。这种分层的目的一是方便测试二是方便替换底层实现。比如某一天不用MethodChannel改成某厂商提供的原生API只需要替换最底层的一个Service类UI和业务逻辑完全不用动。紧急联系人这种功能天然跟硬件能力绑定所以我建议从一开始就把底层能力抽象成接口别在页面上直接写MethodChannel的调用。还有一个容易忽视的点是日志。在真机上跑OpenHarmony应用很多问题不能靠断点调试必须打日志。我专门封装了一个LogService统一控制日志开关和TAG前缀排查平台通道问题时效率高很多。后面确实帮了大忙。2. 项目搭建与环境准备2.1 开发环境与工程创建开发前需要准备几样东西Flutter SDK建议用支持OpenHarmony的版本、OpenHarmony SDK及其IDE、以及自己手里的OpenHarmony开发板或模拟器。我在创建工程时用的是带平台参数的flutter create命令flutter create --platformsohos,android eland_app cd eland_app flutter pub get如果你手里的Flutter版本不支持ohos平台参数那就需要在已有工程上手动添加OpenHarmony的entry模块或者通过社区维护的Flutter for OpenHarmony工具链生成。这里给一个小建议先把官方示例工程跑起来再往里面加业务代码。我第一次就直接在空工程里写业务结果平台通道注册那块出了好几个莫名其妙的问题最后才发现是工程结构不对。创建完成后项目的根目录基本是这样的eland_app/ lib/ main.dart models/ pages/ services/ providers/ ohos/ entry/ src/main/ ets/ resources/ build-profile.json5 pubspec.yamlohos目录就是OpenHarmony侧的原生工程。业务代码全部写在lib目录里尽量不要在原生目录里写跟业务相关的页面这也是为了保证跨端一致性。2.2 依赖清单与版本建议在pubspec.yaml里我最初只引了状态管理和本地存储相关的依赖dependencies: flutter: sdk: flutter provider: ^6.1.2 shared_preferences: ^2.2.3后来加定位和短信能力时发现geolocator这类插件不一定支持OpenHarmony所以我没有盲目依赖pub仓库里的通用插件而是把核心能力通过MethodChannel自己实现。这样可能多写一点原生代码但可控性最好也不会被某个插件的维护节奏卡住。如果你也不想踩这个坑建议在项目初期把所有第三方插件的OpenHarmony支持情况列成一张表再决定哪些直接引入、哪些自己封装。2.3 系统权限与隐私管控在OpenHarmony应用里申请权限跟Android一样需要在项目配置文件里声明。紧急联系人功能用到的权限大致有三类拨打电话、获取定位、访问网络。其中拨打电话是最关键的我倾向于只申请“打开拨号盘”的权限而不是直接授权无感知拔打。原因后面会详细说。权限声明之后代码运行到对应场景时还会触发系统级弹窗。这里需要特别注意模拟器通常不具备真实的电话和定位能力权限弹窗行为跟真机也有差异。所以我会尽早安排真机调试不要等到联调阶段。权限不足时默认走降级逻辑比如打开拨号盘失败就先把号码显示出来让用户手动拨打。另外因为涉及老人隐私我做了双重保护联系人列表只存在本地不做云端同步App内查看联系人列表需要进入设置页避免误操作把敏感信息暴露出去。这一点在养老场景里很容易被忽略但评审时客户往往会专门问。3. 核心模块紧急联系人的实现3.1 数据模型设计紧急联系人的数据字段不算多但每一个都有讲究。我定义了一个模型类class EmergencyContact { final int id; final String name; final String phone; final String relation; final bool isPrimary; EmergencyContact({ required this.id, required this.name, required this.phone, required this.relation, this.isPrimary false, }); MapString, dynamic toJson() { return { id: id, name: name, phone: phone, relation: relation, isPrimary: isPrimary, }; } factory EmergencyContact.fromJson(MapString, dynamic json) { return EmergencyContact( id: json[id] as int, name: json[name] as String, phone: json[phone] as String, relation: json[relation] as String, isPrimary: json[isPrimary] as bool? ?? false, ); } }isPrimary这个字段用来标记主联系人。触发SOS时先打主联系人打不通再轮询其他联系人。relation字段存的是“儿子”“女儿”“家庭医生”这类关系展示在列表页方便老人和管理员快速识别。为什么不用系统通讯录的ID因为跨端的一致性。系统通讯录的ID在不同平台格式不一样而且需要额外读写系统联系人的权限。我们这套App的目标设备是固定给老人使用的联系人由家人在首次配置时录入本地存储完全够用。3.2 本地持久化SharedPreferences 还是 SQLite联系人数量一般不会超过十个人数据量很小我用shared_preferences直接存JSON字符串。优点是不需要引入额外的数据库初始化逻辑安装包更小。如果你预计以后还要存通话记录、位置历史那就用SQLite扩展性更好。我用一个Repository类把读写逻辑封装起来页面层不直接碰SharedPreferencesclass ContactRepository { static const _key emergency_contacts; FutureListEmergencyContact load() async { final prefs await SharedPreferences.getInstance(); final raw prefs.getString(_key); if (raw null || raw.isEmpty) return []; final list jsonDecode(raw) as List; return list .map((e) EmergencyContact.fromJson(e as MapString, dynamic)) .toList(); } Futurevoid save(ListEmergencyContact contacts) async { final prefs await SharedPreferences.getInstance(); final data contacts.map((e) e.toJson()).toList(); await prefs.setString(_key, jsonEncode(data)); } }这里有一个我自己踩过的小坑jsonDecode后的List元素运行时类型是Listdynamic直接转EmergencyContact会报类型错误。所以每一层都要做显式转换千万别图省事在map里省略类型断言。3.3 通过 MethodChannel 调用系统拨号在Flutter侧我封装了一个专门的EmergencyChannel类把所有平台能力调用集中在这里import package:flutter/services.dart; class EmergencyChannel { static const MethodChannel _channel MethodChannel( com.example.eland/emergency, ); Futurevoid callPhone(String phone) async { try { await _channel.invokeMethod(callPhone, {phone: phone}); } on PlatformException catch (e) { throw EmergencyCallException(e.message ?? 拨号失败); } } }在OpenHarmony原生侧需要实现这个MethodChannel的接收逻辑。如果你使用的模板是ArkTS大致结构是这样的import { MethodCall, FlutterResult, Plugin } from ohos/flutter_plugin; export class EmergencyChannel implements Plugin { onMethodCall(call: MethodCall, result: FlutterResult) { if (call.method callPhone) { const phone call.arguments[phone]; // 调用系统拨号能力携带 phone 参数 // 成功后 result.success(true) // 失败后 result.error(PERMISSION_DENIED, ...) } else { result.notImplemented(); } } }具体调起拨号界面时要申请对应的权限并构造一个指示拨号的URI。我通常倾向于先打开拨号盘让用户再按一次拨号键这样更安全。如果是SOS一键直拨则需要更高的拨号权限并且要在权限弹窗上给用户足够明确的说明。直接静默拨号在养老场景里看起来方便但隐私和误拨风险很高我建议默认别这么做。MethodChannel这个名字必须两边完全一致否则App运行时直接报MissingPluginException。我在项目里统一用“com.企业名.项目名/功能名”这种格式来命名可以避免很多低级问题。3.4 SOS触发与多联系人轮询逻辑SOS按钮不能按一下就立即拨号老人容易误碰。我做的方案是长按3秒触发触发后先播放一个明确的语音提示“正在联系家人如需取消请点取消”同时展示一个3秒倒计时确认弹窗。倒计时结束才开始拨号如果中途取消立即终止。触发逻辑放在Provider中便于页面统一刷新和状态管理。核心代码如下class SosProvider extends ChangeNotifier { final EmergencyChannel _channel; final ContactRepository _repo; ListEmergencyContact _contacts; bool _isCalling false; bool _cancelled false; Futurebool triggerSos() async { if (_isCalling) return false; _contacts await _repo.load(); if (_contacts.isEmpty) return false; _isCalling true; _cancelled false; notifyListeners(); // 优先拨打主联系人 final primary _contacts.where((c) c.isPrimary).toList(); final others _contacts.where((c) !c.isPrimary).toList(); final ordered [...primary, ...others]; for (final contact in ordered) { if (_cancelled) break; await _channel.callPhone(contact.phone); // 拨号后并不代表接通这里给用户一个“是否继续拨打下一人”的确认 final shouldContinue await showNextContactDialog(contact); if (!shouldContinue) break; } _isCalling false; notifyListeners(); return true; } void cancelSos() { _cancelled true; _isCalling false; notifyListeners(); } }这里最需要讨论的是“如何判断一个联系人拨打失败”。平台Channel只能保证把拨号请求发出去了但它不知道用户是否接通、对方是否拒接。所以我的做法是拨号后弹出提示询问“联系人A未确认是否继续联系B”由用户或现场护理人员确认。如果老人已经不能点屏幕默认等待一定时间后自动切下一个这个超时时间我设成30秒。3.5 把位置信息带出去紧急联系人不只要打电话还要让家里人知道老人在哪。我在触发SOS后会尝试获取一次位置拿到经纬度后通过两种方式发给联系人第一是拼接成一条短信通过系统短信服务发出第二是如果你们有自己的服务端把位置报给服务端由服务端推送到联系人App。这两种方式我都在工程里留了接口默认走短信因为不依赖网络和服务器。定位获取也需要走平台通道比如判定GPS是否打开、权限是否授予。如果定位失败我不会阻塞SOS主流程而是在短信里附上“位置获取失败请电话确认”这样的占位文本。这个降级策略十分关键否则可能出现老人按了SOS却什么都发不出去的极端情况。短信内容的格式我会固定为“紧急求助请尽快联系。当前定位纬度xxx经度xxx。发送时间xx。”如果因为权限问题没有位置就只发时间和提示。短信内容要简洁不要夹杂多余字符避免被运营商或被联系人当成垃圾信息。4. UI交互与老人场景适配4.1 联系人管理页大卡片、少层级联系人管理页是给老人或家人用的。老人操作时最大的障碍是手指精度下降、反应变慢。所以页面设计原则是卡片大、按钮大、文字大、层级少。我用ListView直接展示所有联系人每个联系人是一个完整的卡片卡片上左侧是姓名和关系右侧是大号电话图标点击卡片就能编辑滑动卡片右侧有删除按钮。卡片高度我设置在至少80dp以上姓名用22sp电话号码用20sp配色保持高对比度背景用米白色文字用深灰色。不要使用花哨的渐变和透明效果那会明显降低可读性。为了让家人快速区分主联系人我在主联系人卡片左上角加了一个“主”字的标签颜色用醒目的红色但面积不大避免视觉噪音。添加联系人的表单里每个输入框上方都有汉字标签。手机号输入框设置最大长度为11位并且强制数字键盘。这里的细节是不要阻止输入“”因为有的家庭成员在海外或使用座机转接号码格式可能更复杂。我在提交时做宽松校验只检查位数是否在6到20之间。4.2 SOS大按钮与防误触机制首页上SOS按钮应该占屏幕的视觉主体但不能一碰就触发。我做的方案是一个圆形的长按触发按钮按下时周围出现渐进的进度环长按3秒进度环满开始执行呼叫。如果中途手指移出按钮或松开进度环消失取消触发。这样设计既满足紧急情况下快捷操作又兼顾了日常防误触。Dart侧用GestureDetector加AnimationController实现进度环。核心片段GestureDetector( onLongPressStart: (details) { _controller.forward().whenComplete(_startCall); }, onLongPressCancel: () { _controller.reverse(); }, child: ScaleTransition( scale: Tween(begin: 0.9, end: 1.2).animate(_controller), child: CircularProgressIndicator(...), ), )这里有一个细节长按的默认时间是500ms对于老人来说太短了。我用的是自定义长按时长3秒所以不能直接用系统默认的onLongPress而是自己通过GestureDetector的onLongPressStart加Timer实现或者用LongPressGestureRecognizer的longPressDuration参数来设置。为了防止长按后弹出确认弹窗又被无意点掉确认弹窗放在屏幕中央弹窗上只有“呼叫”和“取消”两个按钮并且“取消”按钮默认不聚焦。倒计时结束时如果手指还按着按钮直接进入呼叫流程不再二次确认。4.3 字体缩放、无障碍与系统兼容OpenHarmony设备覆盖了手机、平板、柜机、开发板屏幕尺寸差异很大。我处理页面适配时没有一味用固定像素值而是把基础单位换算成响应式值并且支持系统字体缩放。具体做法是在build方法里读取MediaQuery.textScaler计算动态字号final textScale MediaQuery.textScalerOf(context).scale(1.0); final buttonSize 140 * textScale;不过字体缩放到2倍以上时布局很容易溢出。我已经在联系人卡片和SOS按钮上测试过字号超过1.8倍时按钮内文字会自动换行。这里建议不要无限放大而是给SOS按钮的文字设定一个最大字号超出后再通过语音提示而不是视觉提示来引导用户。无障碍方面我给SOS按钮增加了Semantics标签读屏软件可以朗读出“紧急呼叫按钮长按三秒触发”。联系人卡片也能准确朗读姓名和关系。这一点在App上架审核时经常被问到但也确实是老年用户非常看中的体验。还有一个容易被忽略的系统兼容问题OpenHarmony的版本之间状态栏高度、导航栏样式和权限弹窗样式都有差异。我在真机测试时发现某台设备的底部导航栏会遮挡SOS按钮解决办法是给页面外层加SafeArea并且在设备屏幕适配层统一处理安全区域。别只在一台模拟器上验证布局尽可能多找不同屏幕尺寸的真机跑一遍。5. 常见问题与排错实录5.1 MethodChannel 报 MissingPluginException这个问题在Flutter for OpenHarmony里太常见了我的排查顺序通常是先确认原生侧是否已经注册了这个Channel再确认通道名是否完全一致最后检查是不是重新构建后插件没生效。Flutter本地缓存有时候会让人反复踩坑我习惯在构建后先清理再重新编译命令一般是flutter clean然后重新执行编译。还有一个经验MethodChannel的通道名不要用模板默认名比如plugins.flutter.io/shared_preferences这种我们自己在原生侧实现的通道一定要用唯一的前缀例如com.example.eland/emergency避免和第三方插件冲突。5.2 真机上权限弹窗不出现或调用被拒我在一台OpenHarmony开发板上测试时发现拨号请求发出去后没有任何反应也没有弹权限框。后来定位到问题出在权限配置应用没有在模块配置文件中声明拨号相关权限。只写代码调接口而没有声明权限系统会直接拒绝且不一定有明确日志。所以遇到权限相关问题时第一步去看配置文件里的权限声明第二步看日志里的权限拒绝码。如果权限已经声明但弹窗仍不出现可能是在代码调起通道之前请求权限太早。我建议在用户长按SOS按钮后再动态申请权限并且在权限弹窗出现前先播放一段语音提示“需要拨打电话权限”让老人或在场的家属知道发生了什么。5.3 定位在室内经常超时养老场景大量发生在室内GPS信号弱定位超时很常见。我在获取位置时会给一个最短超时时间比如8秒。超时后不再等待短信直接使用“位置获取失败”占位。不要为了等定位而推迟呼叫呼叫永远是第一优先级。如果你有条件接入基站和Wi-Fi定位效果会好很多但成本和技术复杂度都会增加初期先做好降级即可。5.4 后台被清理后SOS失效有用户反馈App退到后台一段时间后再唤醒SOS按钮点击没有响应。我排查后发现问题不是按钮失效而是Flutter引擎被系统回收重新恢复时页面状态没有重建。解决思路是在main.dart里尽量保留全局Provider实例并且监听应用生命周期在AppLifecycleState.resumed时重新加载联系人数据。如果你的场景需要锁屏状态下触发SOS那要更复杂一些需要申请后台长时任务或使用系统支持的一键SOS能力这部分我建议单独做一版原生逻辑不要指望纯Flutter能稳定跑。5.5 联系人数据版本升级迁移紧急联系人字段不会停留在最初版本。我预上线后加过一个“是否可下发位置”的开关字段如果直接读取旧JSON新字段会缺失导致设置错误。在fromJson里我已经加了空值兜底例如json[showLocation] as bool? ?? true。以后凡是新增字段都必须在fromJson里给默认值并且做一次版本号校验。我在SharedPreferences里额外存了一个_dataVersion整数升级时发现版本低就执行一次轻量迁移避免用户数据丢失。最后再分享一个实操经验紧急联系人模块从代码量上说不大但它贴着硬件能力和系统限制走几乎所有问题都得在真机上才能暴露出来。我建议你在拿到OpenHarmony开发板的第一天就把拨号、定位、后台恢复这三条路径全部跑一遍不要等到整个App写差不多再联调。很多时候你以为的“小功能”愿意花时间细抠最后确实能救一个人。哪怕只是把日志打清楚、把降级策略做好这份工作都值得。
阅读完成 · 觉得有帮助?
咨询建站