ARTICLE DETAIL

资讯详情

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

Flutter鸿蒙跨平台开发实战:搭建家庭健康档案管理应用

Flutter鸿蒙跨平台开发实战:搭建家庭健康档案管理应用 家里有老人的朋友应该都有这种体会老人的体检报告、血压血糖记录、用药情况要么记在纸质本子上要么散落在各个医院的App里真正要查的时候翻箱倒柜。去年我给自己家做了个小工具顺便把项目跑到了新出的鸿蒙设备上。用Flutter框架做跨平台开发一套代码同时搞定Android、iOS和鸿蒙整个系统的核心就是一个家庭健康档案管理应用——成员档案、健康指标记录、用药提醒、趋势图表全都有。这篇文章我把完整思路和实操过程整理出来你能照着搭出同一套系统也能理解Flutter在鸿蒙上开发的关键节点。这套方案特别适合两类人一是想在鸿蒙生态里快速落地的跨平台开发者二是家里有慢病老人、想自己做健康管理工具的Flutter爱好者。我会把工程怎么初始化、数据怎么存、图表怎么画、鸿蒙上的平台差异怎么处理这些硬骨头逐个说清楚。1. 整体设计思路为什么选Flutter来啃鸿蒙这块硬骨头1.1 鸿蒙生态现状与Flutter的切入机会鸿蒙系统从诞生那天起就有一个无法回避的问题原生生态不够丰富。虽然自家的开发工具和语言在快速迭代但想让海量第三方应用在短时间内全部用ArkTS重写一遍既不现实也没必要。这时候跨平台框架的适配价值就体现出来了。Flutter是市面上少数能在鸿蒙上跑得比较顺畅的跨平台方案。社区里从2021年就有团队开始做OpenHarmony的Flutter适配到现在已经积累了好几个稳定的分支和镜像。这套适配不是简单地把Flutter引擎编译成鸿蒙能识别的产物而是重新实现了一套渲染层、平台通道和生命周期绑定让Dart代码能直接跑在鸿蒙的ArkUI体系之上。我评估过其他方案React Native在鸿蒙上的适配进度一直不稳定部分原生模块缺失严重小程序容器方案只能覆盖业务逻辑无法做复杂UI。相比之下Flutter的优势有三点自绘引擎与平台UI解耦在鸿蒙上仍然能保持高度一致的渲染效果Dart语言层完全复用业务代码几乎不用改社区活跃度高适配问题有人维护踩坑有人分享1.2 家庭健康档案管理系统的需求拆解这个项目不是普通的增删改查练习它有几个比较实际的需求维度家庭成员管理。健康档案的主体不是一个人而是一家人。要有成员列表、成员详情、每个成员独立的数据空间。健康指标记录。核心场景是血压、血糖、心率、体重这四类数据的录入和查询。每一条记录需要关联到具体成员、具体指标类型同时要记录测量时间和备注信息。用药提醒。家里的老人经常忘吃药这个模块要能设置用药计划到期弹出通知提醒还能查看某天的用药完成情况。数据可视化。数据录进去只是第一步关键是要能看出趋势。这里需要按时间维度生成折线图和柱状图让用户直观看到血压的变化曲线或者血糖的波动范围。数据安全与备份。健康数据属于高敏信息本体存储必须本地化同时提供加密导出功能方便用户自己备份。以上需求拆解完之后整个项目的技术映射就很清晰了Flutter负责UI和业务逻辑本地数据库用sqlite方案通知用local_notifications插件图表用fl_chart状态管理用Riverpod。每一个选型我都考虑了鸿蒙端是否兼容后面逐个说明。2. 开发环境配置与鸿蒙Flutter工程初始化2.1 OpenHarmony分支的Flutter SDK怎么配这里要特别强调一个坑官方flutter SDK目前是不支持鸿蒙的你需要使用社区维护的ohos分支。我用的是OpenHarmony/flutter_flutter的master分支它维护了OHOS的引擎适配、工具链集成和示例工程。配置步骤跟你平时装Flutter几乎一样但有三个关键差异点我整理成了一张速查表配置项常规Flutter鸿蒙Flutter分支仓库地址https://github.com/flutter/flutter.githttps://gitee.com/openharmony-sig/flutter_flutter.git引擎仓库官方预编译包需要额外拉取ohos-engine产物设备连接adbhdc鸿蒙设备调试工具构建产物apk/ipahap鸿蒙应用包拉取分支之后要把flutter目录的bin路径加进PATH然后运行flutter doctor检测。第一次可能要等比较久因为需要下载鸿蒙引擎的编译产物。我建议网络条件允许的话直接配置镜像源来加速依赖下载。2.2 创建鸿蒙Flutter工程的两条路径路径一用命令创建标准工程然后手工加入鸿蒙壳工程。执行flutter create --org com.example health_family创建标准Flutter项目然后在项目根目录下加入鸿蒙壳工程的目录结构核心是ohos目录里面放着module.json5、entry模块的配置、以及Flutter引擎的加载入口。路径二直接使用社区提供的鸿蒙Flutter示例工程作为模板把lib目录替换成自己的业务代码。这种方式更省事因为模板里已经配好了工程间依赖、签名信息、打包脚本。我第一次用的是路径一结果在工程配置上花了两天。后来才发现模板里已经处理好了大量配置文件之间的耦合关系。强烈建议新手走路径二先跑通一个Demo再去研究壳工程内部逻辑。2.3 module.json5与权限声明鸿蒙工程与Android工程最大的不同在于应用配置文件。在Android里写AndroidManifest.xml在鸿蒙里则是一个module.json5。健康档案管理用到的权限包含{ module: { requestPermissions: [ { name: ohos.permission.KEEP_BACKGROUND_RUNNING }, { name: ohos.permission.STORE_PERSISTENT_DATA }, { name: ohos.permission.GET_NETWORK_INFO }, { name: ohos.permission.NOTIFICATION_CONTROLLER } ] } }这里有一个细节STORE_PERSISTENT_DATA表示允许应用在设备本地持久化存储用户数据它是健康数据落库的关键权限NOTIFICATION_CONTROLLER是发送通知提醒相关的权限用药提醒必须依赖它。如果你是开发阶段使用自动签名部分权限系统会自动加入但通知类权限还是要手工声明。我还建议在module.json5里配置好应用的图标和标签不然真机上看到的会是一个默认问号图标。图标资源放在resources目录下注意鸿蒙对图标尺寸有明确要求直接沿用Android的mipmap尺寸通常能适配。3. 核心功能模块的数据层与界面实现3.1 家庭成员的档案模型设计健康档案系统的第一张表是家庭成员表。我用的数据库是drift这个Flutter库它本身是一个SQLite之上的ORM同时支持类型安全的查询API。相比sqflite需要手工管理SQL语句drift更能减少低级错误尤其是字段名拼写错误。家庭成员表的结构如下class FamilyMembers extends Table { IntColumn get id integer().autoIncrement()(); TextColumn get name text().withLength(min: 1, max: 30)(); IntColumn get gender integer().withDefault(const Constant(0))(); DateTimeColumn get birthday dateTime().nullable()(); TextColumn get relation text().nullable()(); TextColumn get avatarPath text().nullable()(); TextColumn get bloodType text().nullable()(); TextColumn get allergyHistory text().nullable()(); TextColumn get chronicDiseases text().nullable()(); DateTimeColumn get createdAt dateTime().withDefault(currentDateAndTime)(); }gender字段我用0表示未知、1表示男、2表示女而不是直接用布尔值因为实际录入时老人信息经常不完整需要三态。relation字段存的是与当前用户的关系比如父亲、母亲、配偶、子女。在设计时我遵循了一个原则不主动要求用户填写所有字段。因为家庭健康管理的使用场景往往是先录入核心信息、后续慢慢补充强制校验会导致录入门槛太高。所以在UI层除了姓名之外的所有字段都允许为空慢性病史和过敏史这类字段还支持多值输入用逗号分隔即可。3.2 健康指标记录与多维度查询健康指标表是这个系统里数据量最大、查询条件最复杂的表。我这里引入了指标类型的概念用枚举值区分血压、血糖、心率、体重四种数据。enum HealthMetricType { bloodPressure, bloodSugar, heartRate, weight } class HealthRecords extends Table { IntColumn get id integer().autoIncrement()(); IntColumn get memberId integer()(); IntColumn get metricType integer()(); TextColumn get value text()(); TextColumn get unit text()(); DateTimeColumn get measuredAt dateTime()(); TextColumn get note text().nullable()(); DateTimeColumn get createdAt dateTime().withDefault(currentDateAndTime)(); }value字段我用Text存储而不是数值因为血压是128/86这种两段式结构体重可能是60.5血糖有5.6统一传字符串便于后端解析。有人会觉得这样不够严谨但从实际使用角度出发健康指标的格式本来就多样字符串能在灵活性和查询需求之间取得平衡。查询时如果需要做数值比较Dart侧用double.parse转换即可。UI层面我做了三个维度的入口按成员查、按指标类型查、按时间范围查。核心列表使用CustomScrollView加SliverList实现滚动分页每次加载20条记录。页面底部有一个快捷录入按钮点击后弹出底部表单表单会根据指标类型动态切换输入控件——比如体重用数字键盘血压用两段数字框。3.3 用药提醒模块的技术实现用药提醒这个模块是全家都觉得有用的功能但它涉及到的技术点也是最杂的本地通知、定时任务、状态恢复、用户交互回调。用药计划表结构class MedicationPlans extends Table { IntColumn get id integer().autoIncrement()(); IntColumn get memberId integer()(); TextColumn get drugName text()(); TextColumn get dosage text()(); TextColumn get frequency text()(); TextColumn get remindTimes text()(); // 逗号分隔的时间点如 08:00,14:00,20:00 DateTimeColumn get startDate dateTime().nullable()(); DateTimeColumn get endDate dateTime().nullable()(); IntColumn get isEnabled integer().withDefault(const Constant(1))(); }提醒能力我用的是flutter_local_notifications插件。在Android和鸿蒙上这个插件都有对应的平台实现鸿蒙端通过通知服务发送通知。配置通知渠道时我单独建了一个medication_reminder渠道并设置了较高的优先级保证提醒消息能弹到通知栏最前面。有一点要提示一下即便设置了精确的提醒时间系统进程也可能在某些情况下回收资源。严谨的做法是把提醒任务持久化到本地数据库在应用启动或从后台恢复时主动检查当前时间与计划时间把错过的提醒重新补发。HealthKit里有个catch-up notification的说法就是这个思路。3.4 趋势图表的Flutter实现图表部分我用的是fl_chart这个成熟库它在鸿蒙适配过程中表现还算不错因为它的渲染完全基于CustomPaint不依赖任何原生控件。血压趋势图我做了两种维度7天趋势和30天趋势。7天展示每天各时段的测量值30天展示每日收缩压和舒张压的平均值。LineChart( LineChartData( lineBarsData: [ LineChartBarData( spots: systolicSpots, // 收缩压曲线点 color: const Color(0xFFE53935), barWidth: 2, dotData: const FlDotData(show: false), ), LineChartBarData( spots: diastolicSpots, color: const Color(0xFF1E88E5), barWidth: 2, dotData: const FlDotData(show: false), ), ], titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles(showTitles: true, reservedSize: 40), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: _isWeekly ? 1 : 5, getTitlesWidget: (double value, TitleMeta meta) { // 根据value显示日期或时间 }, ), ), ), ), )这里真正考验性能的是数据量。如果某个成员连续记录了好几年一次要渲染上千个点显卡压力会很大。我做了数据聚合超过200个点时先按天分组求均值再绘图。这个处理放在Dart层用集合分组完成实测在低端鸿蒙机型上也能保持60帧滚动。4. 鸿蒙适配过程中的平台差异与原生能力调用4.1 Flutter引擎在鸿蒙上的渲染与生命周期在深入编码之前先理解一下Flutter在鸿蒙上的运行机制。鸿蒙系统的应用层组件叫ArkUI而Flutter App在鸿蒙上运行时会创建一个全屏的SurfaceView实际是自绘的OhosSurface来承载引擎渲染的画面。你写的所有Flutter Widget最终不是在ArkUI的组件树里渲染而是由Flutter引擎直接画在这个Surface上。这个差异带来的影响是你不能在一张Flutter页面中嵌入鸿蒙原生组件比如原生的地图组件也不能直接用原生控件叠在Flutter镜头层上。如果需要嵌入原生地图、扫码预览这类能力得用PlatformView通道做视图拼接这个过程和Android上嵌入原生View的套路类似只是鸿蒙端的PlatformView实现尚在完善中。生命周期绑定方面鸿蒙的PageAbility或者新版Stage模型的Ability与Flutter引擎的Activity生命周期并不一一对应。你需要手动在Ability的onStart/onStop/onForeground/onBackground回调中同步调用Flutter引擎的对应生命周期方法。模板工程里已经实现了这层封装但如果你做大窗口适配或者多实例场景要格外关注生命周期转发的完整性。4.2 MethodChannel平台通道本地相册选择与事件回调健康档案系统需要支持上传检查报告图片。图片选择功能在Android上通常调用image_picker插件鸿蒙端这个插件也有对应的适配版本。但如果你想对图片做压缩、旋转处理还是需要走原生代码。以图片保存到系统相册为例鸿蒙的MediaLibrary Kit提供的接口与Android完全不一样。我这里通过MethodChannel封装了一段原生代码// Dart侧 const platform MethodChannel(health_family/media); final String? savedPath await platform.invokeMethod( saveToGallery, {path: tempPath, title: health_report_${DateTime.now().millisecondsSinceEpoch}}, );鸿蒙原生侧在kotlin通过ArkTS的扩展机制里实现这个methodoverride fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { if (call.method saveToGallery) { val path call.argumentString(path) val title call.argumentString(title) // 调用MediaLibrary的PhotoAccessHelper写入系统相册 result.success(savedUri.toString()) } else { result.notImplemented() } }MethodChannel在鸿蒙上的实现跟Android基本一致主要区别是原生侧代码不是Android的Java/Kotlin插件形式而是鸿蒙的AbilityExtension模块。写这个通道时要注意线程切换原生侧耗时操作不能直接在主线程执行要用TaskPool分发。4.3 鸿蒙权限弹窗与Android的差异权限请求是移动开发的老大难。鸿蒙的权限体系从Stage模型开始跟Android非常接近但有几个细节差异值得单独说明动态权限是在Ability的onStart阶段检查的Flutter侧通常用permission_handler插件统一封装。鸿蒙端这个插件的适配程度还可以但需要注意插件在请求权限后会返回对应的授权状态鸿蒙对部分敏感权限比如精确定位支持仅本次使用授权这与Android的仅本次允许逻辑一致。测试权限时发现一个容易踩的坑在鸿蒙开发者模式中某些权限的弹窗会聚合在一个界面中显示与Android的逐条弹窗不同。如果你的App同时申请多个权限用户很可能只看到一次弹窗然后所有权限同时被授予或拒绝。所以在UI引导层最好给用户讲清楚权限用途降低批量拒绝的概率。5. 常见问题与踩坑排查实录5.1 编译失败找不到OpenHarmony的依赖很多人在拉取鸿蒙分支后直接构建会遇到类似Could not resolve com.github.OpenHarmony:flutter_flutter的错误。这个问题的根源是Gradle无法从默认仓库中找到鸿蒙适配产物。解决方法是在ohos/ohos-bom.gradle里手动添加Maven仓库地址allprojects { repositories { maven { url https://developer.huawei.com/repo/ } maven { url https://gitee.com/openharmony-sig/flutter_flutter/-/raw/master/ohos-repo } // 其他默认仓库 } }同时确认你拉取的flutter_flutter分支版本和ohos目录下的依赖版本一致。版本错位会出现引擎API不匹配的诡异报错而且是那种编译能过、运行必崩的隐性坑。5.2 真机运行时白屏开发阶段常见的白屏问题我排查下来的原因集中在三个方面一是引擎产物没有正确打包进hap多发生在flutter build hap之后没有清理旧产物二是自定义的FlutterEngine初始化失败日志里能看到Failed to load flutter engine之类关键词三是页面路由的初始路径没配对。排查方法有一个好用的技巧在鸿蒙侧日志中打开Flutter引擎调试开关。在entry模块的MainAbility中通过Intent传入--enable-dart-profiling参数同时用hdc工具抓取完整日志hdc shell hilog | grep flutter这样能看到Dart VM初始化、引擎启动、第一帧渲染的完整过程白屏原因基本一眼定位。5.3 PlatformView嵌入时的手势冲突如果系统里加入了视频指导页面讲解如何测量血压就需要嵌入鸿蒙原生的视频播放控件。这就会用到PlatformView。在实际调试中发现原生视频组件与Flutter的GestureDetector存在手势竞争主要表现为在视频区域上下滑动时页面会同时滚动导致视频界面和列表滚动互相抢手势。解决办法是在Flutter侧对平台视图区域使用Listener包裹并设置behavior: HitTestBehavior.opaque将这个区域的命中测试完全交给原生的视频播放器处理同时配合PlatformView的layoutDirection和creationParams参数控制尺寸。这套处理在Android上已经成熟鸿蒙端需要稍微调整生命周期回调的时序但只要不强行在PlatformView上层叠加Flutter手势组件整体还是可控的。5.4 性能表现与优化建议用同一台鸿蒙平板对比过release包和debug包的表现差距非常明显。核心原因是debug模式下的Dart代码通过JIT执行配合DevTools调试钩子性能开销会放大两三倍。发布前务必构建release包实测页面冷启动时间能从2秒多降到800毫秒左右。另外两个通用优化技巧也值得更新到你的代码里列表页的图片用cached_network_image或本地缩略图避免加载原尺寸图片图表页在切出时暂停动画减少不必要的帧渲染5.5 数据备份与导出直接可用的小工具因为健康数据本地存储用户一旦换设备就需要导出数据。我做了一个简单但完整的功能支持一键导出CSV格式的成员健康记录并且把CSV文件保存到下载目录。FutureString exportMemberData(int memberId) async { final records await db.healthRecordsDao.getRecordsByMember(memberId); final buffer StringBuffer(记录时间,指标类型,数值,单位,备注\n); for (final record in records) { buffer.write(${record.measuredAt}, ${record.metricType}, ${record.value}, ${record.unit}, ${record.note}\n); } final dir await getApplicationDocumentsDirectory(); final file File(${dir.path}/member_$memberId.csv); await file.writeAsString(buffer.toString()); return file.path; }CSV文件导出之后再通过分享面板发送到云盘或电脑。这个功能操作量不大但对用户信任度的提升非常明显——很多人不怕操作复杂就怕数据被绑定在某个App里出不来。写在最后的一点个人体会项目做完之后最大的感受是Flutter的跨平台能力在鸿蒙上确实是能打的。从工程搭建到功能跑通真正被业务卡住的场景很少更多的时间花在了平台适配层的调试上。这也是我建议每一位做Flutter跨平台开发的人尽早接触鸿蒙分支的原因——技术栈本身不复杂但平台差异的敏感度需要时间积累。给后来者两个具体建议第一环境配置阶段直接采用社区模板工程起步不要从零配壳工程能少走很多弯路第二健康类应用务必把数据备份做扎实这是用户留存的关键节点。后续我会继续完善这个项目计划接入鸿蒙的分布式能力让家人的健康数据在平板和手机之间无缝流转到时候再来分享更多踩坑经验。
返回列表