拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Flutter鸿蒙适配实战:家庭用药清单跨平台开发全解析

Flutter鸿蒙适配实战:家庭用药清单跨平台开发全解析 直接做这个项目之前我先说个背景我手里正好有个医疗健康类的跨平台项目要同时覆盖 Android、iOS 和鸿蒙Flutter 几乎是绕不开的选择。这次把“家庭用药清单”这个模块从零到一完整跑通了一遍中间踩了不少鸿蒙适配的坑也把 Flutter 在鸿蒙上的插件机制、状态管理、通知提醒这些链路都捋顺了。这篇就把整个项目的设计思路、核心实现、鸿蒙适配流程和排查实录一次性写清楚给正在做同类需求或者准备接鸿蒙适配的朋友一个完整参考。1. 项目背景与整体设计思路1.1 家庭用药管理到底在解决什么问题先别急着聊技术想想这个需求背后的真实场景。家里有老人慢性病用药的有小孩感冒发烧临时用药的还有常备的维生素、外用药、急救药这些药品混在一起很容易出现几个问题不知道上次什么时候吃的、不知道药还剩多少、不知道有没有过期、老人记性差漏服或者重复服药。这些不是靠一个闹钟能解决的需要的是一个“以药品为核心、以提醒为闭环”的管理工具。所以家庭用药清单这个项目核心不是做一个漂亮界面而是把三类数据管好药品基础信息名称、规格、剂量、有效期、用药计划频次、时间、疗程、用药记录每次服用时间、剩余量变化。在此基础上再做智能提醒和过期预警这才是“智能管理”四个字的真正含义。从技术选型上说这个项目天然适合跨平台方案因为用户可能用安卓手机、也可能用苹果手机今年又多了一个鸿蒙。如果每个系统都写一套原生维护成本直接翻三倍更别说后续还要加功能。Flutter 在这条赛道上的优势很明显一套 Dart 代码编译到多个平台UI 一致性好插件生态成熟而且对鸿蒙的适配已经有了一条相对清晰的路。1.2 为什么选 Flutter 而不是 uni-app 或原生 ArkTS这个项目立项时其实对比过三条路线我把当时的评估过程完整分享一下。首先是鸿蒙原生 ArkTS优点是系统能力调用最直接、性能最好但问题是只能跑在鸿蒙上以后要重新覆盖 Android 和 iOS 就得另起炉灶对于一个小型工具类应用来说成本太高。其次是 uni-appVue 语法上手快但遇到复杂交互和自定义渲染时性能和灵活性都会打折扣尤其是后续要接入系统级通知、后台任务这些能力时绕弯路的概率比较大。最终选了 Flutter有几个硬指标支撑。第一Flutter 的渲染引擎是自己实现的不依赖系统 WebView 或者原生控件这使得它在不同平台上的表现一致性极好对“同一套 UI 跑三端”这个需求是决定性的。第二Flutter 的插件机制是标准化的 MethodChannel / EventChannel鸿蒙适配层已经有社区方案可以接这意味着我写的 Dart 业务代码基本不用动只需要补平台侧的实现。第三Flutter 的状态管理和本地数据库生态非常成熟像 sqflite、shared_preferences、provider 这些库都有鸿蒙适配版本或替代方案开发效率很高。1.3 整体架构设计一套核心、三端壳这个项目的架构我定为“一套核心、三端壳”的思路。核心层完全是 Flutter Dart包含数据模型、数据库操作、提醒逻辑、页面路由和状态管理这部分代码在 Android、iOS、鸿蒙三端完全共用。壳层是针对各平台的入口配置和系统能力适配比如鸿蒙的 module.json5 配置、Android 的清单文件、iOS 的权限声明以及需要调用的系统 API 在各自平台上的实现。数据流上采用单向数据流用户操作 - ViewModel 更新状态 - 状态驱动 UI 重建 - 数据变更持久化到 SQLite。提醒模块通过一个独立的服务层管理启动时注册定时任务触发时在本地通知通道发送提醒。这样设计的好处是后续如果要把提醒做成云同步或者接入穿戴设备核心层不用动只需在服务层增加新的实现。2. 核心功能拆解与数据模型设计2.1 药品信息字段设计不能只存一个药名药品管理的第一步是把数据结构设计好这决定了后面所有功能的开发顺畅度。我用一个 Medicine 模型承载药品基础信息字段设计如下字段类型说明补充理由idString主键UUID避免自增ID在多端同步时的冲突nameString药品通用名用于列表展示和搜索specString规格信息如 0.25g*24片用户核对药品时需要dosageString单次剂量描述老人容易看错尽量用自然语言unitString剂量单位片/粒/毫升与 dosage 配合stockdouble当前剩余量用于缺药提醒initialStockdouble初始总量用于计算消耗趋势expiryDateDateTime有效期过期预警的核心usageTimesList每日服药时间点如 08:00, 20:00提醒任务的数据源frequencyint每日服药次数用于计算剩余可用天数categoryString分类慢性病/感冒/外用药等家庭多人用药时按成员分类更实用ownerString用药人结合家庭成员管理noteString备注如饭后服用、忌酒医嘱信息除了 Medicine还需要一个 UsageRecord 模型记录每次服药行为字段包含药品ID、服药时间、剂量、操作人。这个表的作用有两个一是让用户能看到“今天吃了没有”的历史记录二是给智能预警提供数据基础比如连续漏服两次就发一个特别提醒。这里有个经验供参考剩余量的计算尽量用“初始总量 - 已消耗量”而不是每次服药时手动减因为手动减会污染操作记录。我采用的方式是每次记录一条 UsageRecord同时更新 Medicine.stock initialStock - 已消耗累计这样既能保留完整历史又能快速拿到当前剩余量。2.2 提醒与管理逻辑定时任务与智能判断提醒功能是这个项目的灵魂。我的实现分三层时间规则解析层、调度层、通知触发层。时间规则解析层负责把 usageTimes 里的字符串解析成每天的具体提醒时间点同时支持一个简单的一次性用药计划比如“连吃三天每天两次”用疗程开始时间和疗程天数来控制提醒的启用和停用。这里要注意时区问题我直接用的本地时间没有做时区转换因为家庭用药场景不涉及跨时区旅行做了反而容易出错。调度层的设计我没用第三方的 cron 库而是自己实现了一个简单的轮询机制应用启动时计算下一个提醒时间点注册一个定时器到点触发通知后立即计算再下一个。应用被杀掉之后安卓和鸿蒙场景下依赖系统 AlarmManager 的能力这部分我留在后面鸿蒙适配章节详细讲因为这是坑最多的环节。智能判断层处理三类预警过期预警药品有效期小于 90 天时开始提示、缺药预警剩余可用天数小于 7 天时提示、漏服预警超过设定时间 2 小时无服药记录时发提醒。这三类判断用 Dart 写起来很直接遍历药品列表结合当前日期时间和 UsageRecord 做除法计算即可不需要引入复杂的规则引擎。2.3 本地数据库选型SQFlite 与 Isar 的权衡本地数据库我一开始纠结过是 sqflite 还是 Isar。sqflite 是老牌方案SQL 语句灵活问题排查容易但需要自己写表结构和迁移逻辑。Isar 的性能更好查询语法更现代化还自带反应式查询但它在鸿蒙上的适配成熟度不如 sqflite。考虑再三我选了 sqflite。原因很简单稳定性优先。项目要同期适配三个平台sqflite 的插件实现了 Flutter 官方标准接口鸿蒙侧的适配工作只需要补一个 Sqflite 鸿蒙实现不需要修改我的数据访问层。Isar 虽然查询体验更好但底层是原生代码鸿蒙适配需要重新编译原生库工作量和风险都不可控。数据库建表时我留了一个升级友好的设计每个表都带一个 schema_version 字段后续加字段时通过迁移脚本处理避免用户升级 App 后直接崩溃。这个看起来是小细节但在真实项目中救过我好几次。3. 鸿蒙适配实操从环境搭建到真机运行3.1 Flutter 鸿蒙适配的环境准备鸿蒙适配和常规的 Android 开发完全是两个体系。我这里说的是 HarmonyOS NEXT纯血鸿蒙它不再兼容 Android APK所以 Flutter 项目必须使用 OpenHarmony 适配分支的 Flutter SDK 才能编出鸿蒙原生应用。环境准备的完整步骤如下安装 DevEco Studio当前我用的是 5.x 版本配置 HarmonyOS SDK拉取 Flutter 的 OpenHarmony 适配分支 SDK这里强调别用官方 pub.dev 的 Flutter SDK要用社区维护的 flutter_flutter 项目下针对 ohos 的分支配置本地 Flutter SDK 路径到环境变量并在 DevEco Studio 中设置好 HarmonyOS SDK 位置通过命令行创建 Flutter 项目后执行flutter build hap编译鸿蒙应用包这套环境我装了一整天坑主要出在 SDK 路径和插件版本不匹配上。建议拿到一个新环境后先跑一个空 Flutter 项目做鸿蒙编译确认基础链路是通的再开始接业务代码。另外一定要确认 Flutter 适配分支的版本号和项目里用到的插件兼容版本错位是编译报错的最常见原因。3.2 创建项目与目录结构改造鸿蒙适配项目的目录结构和普通 Flutter 项目有区别。标准的 Flutter 工程是 android/ 和 ios/ 两个平台目录鸿蒙适配后会在工程根目录多出一个 ohos/ 目录里面是一个完整的鸿蒙原生模块。我在实际项目中遇到的一个问题是用flutter create生成的项目不会自动带 ohos 目录需要用flutter create --platformsohos .这种命令或者从模板工程复制我当时是手撸的工程文件老实说效率不高建议用社区提供的 create 模板。ohos/ 目录下的关键文件是 entry/src/main/module.json5、entry/build-profile.json5 和 hvigorfile.ts。module.json5 负责声明应用包名、权限、页面路由这里要特别注意权限声明比如通知权限、后台运行权限、闹钟权限都必须在 module.json5 里显示声明否则运行时拿不到。创建完基础工程后我把业务代码全部放在 Flutter 的 lib/ 目录下ohos 目录只做入口和桥接。也就是说鸿蒙端的原生代码是严格“壳化”的不掺业务逻辑。这样做的好处是后续鸿蒙 SDK 升级时壳层跟着升就行核心代码不受影响。3.3 关键配置权限声明与打包调试鸿蒙打包调试的完整链路是编写 Dart 业务代码 -flutter build hap --debug生成 HAP 包 - 在 DevEco Studio 里连接真机或模拟器安装调试。这里有几个非常容易踩的坑第一权限声明必须在 module.json5 的 requestPermissions 里配置。我做的这个项目需要三个权限通知用于用药提醒、闹钟和提醒用于定时任务、后台任务用于应用切后台后提醒仍能触发。少任何一个权限声明真机上就会表现为提醒不响或者定时任务被系统杀掉。第二Flutter 层的 Android 和鸿蒙平台的权限请求逻辑不完全一致我统一封装了一个 PermissionService通过检查Platform.isOhos来判断当前是鸿蒙环境还是安卓环境走不同的权限请求分支。这段逻辑在测试时极其重要因为没有它经常出现“安卓能提醒、鸿蒙静悄悄”的诡异现象。第三打包时建议用 release 包测试提醒功能debug 包在鸿蒙上的后台存活策略和 release 差异很大debug 包经常会出现“前台正常、后台被杀”的现象容易让人误判为功能 Bug其实是包模式的问题。4. 核心功能实现与代码要点4.1 药品列表页多条件搜索与过期状态标注药品列表页是应用的主界面我用 Flutter 的ChangeNotifierProvider做状态管理数据库查询结果缓存在内存中通过搜索关键字实时过滤。核心逻辑是维护一个 TextEditingController每次输入变化时触发notifyListeners列表页通过Consumer监听并刷新 UI。列表项的视觉设计上我把药品的过期状态直接做成了三种颜色标识绿色是正常、橙色是 90 天内过期、红色是已过期或剩余量低于 7 天。这个设计看起来简单但对家庭用户非常友好——老人不需要点击进详情页扫一眼列表就知道哪盒药要处理了。这里分享一个经验不要把Medicine模型直接传给 UI而是包一层MedicineViewModel在 ViewModel 里计算好展示状态过期天数、剩余可用天数、今日是否已服药UI 只消费这些已计算好的展示字段。这样 UI 层逻辑简单测试也方便状态计算的逻辑可以单独写单元测试覆盖。4.2 提醒功能实现本地通知与定时调度提醒功能分为两部分定时器调度和通知弹出。调度部分我封装了一个MedicationScheduler单例维护一个Timer和一个nextRemindTime字段。每次触发后重新从数据库读取所有药品的最新 usageTimes计算出下一个要提醒的时间点。这个重构逻辑有一个关键点在重新计算前必须清除旧的 Timer 实例否则多次初始化会创建重复的定时器导致同一条提醒弹两次。通知弹出我用了flutter_local_notifications插件但鸿蒙上它没有原生实现所以我走 MethodChannel 自己实现了鸿蒙的通知能力。具体做法是在 Dart 层定义一个抽象接口NotificationService分别提供AndroidNotificationService和OhosNotificationService两个实现通过工厂方法按平台创建。鸿蒙侧的通知创建逻辑用 ArkTS 写通过 Channel 接收 Dart 传来的标题、内容、提醒时间参数调用鸿蒙的通知接口创建通知。这段时间代码量不大但逻辑链比较长我画过一张时序图核心链路是Timer.periodic 触发-查询下个提醒时间-构造 NotificationRequest-调用平台 Channel-鸿蒙通知服务弹出通知。每一步都要处理失败异常任何一步出问题都会导致用户无感。4.3 状态管理的响应式联动用药记录的联动是这个项目状态管理最需要注意的地方。比如用户在详情页点击“确认服药”这个操作涉及三处更新新增一条 UsageRecord、减少 Medicine 的 stock、更新列表页的“今日是否已服药”状态。如果这三处更新分别写在不同的状态对象里很容易出现界面数据不一致。我的方案是定义一个全局的MedicineStore它作为唯一的可信数据源所有页面的状态都从这一个 Store 派生。MedicineStore内部持有数据库引用对外暴露loadAllMedicines()、recordUsage(medicineId, dosage)、deleteMedicine(id)等异步方法。每次方法执行完毕后Store 从数据库重新读取全量数据并notifyListeners()所有依赖它的页面自动刷新。这套模式虽然每次刷新会多查一次数据库但数据一致性极好本地 SQLite 查询速度也在可接受范围内。4.4 页面导航与状态保持Flutter 的 Navigator 在页面切换时默认会保留下层页面的状态但在鸿蒙适配中我用了一个容易被忽略的点Navigator.push和Navigator.pushReplacement的行为差异。在“从列表进入详情”这个场景中如果用 pushReplacement上一个页面的状态就会直接被销毁返回操作也不是 pop 而是重新创建列表页看起来像数据丢了一样。我全程使用了Navigator.push并且让列表页的加载逻辑充分依赖MedicineStore的数据刷新机制即使页面在栈中重新可见也能通过didChangeDependencies触发一次数据检查。另外涉及到系统返回键时鸿蒙的返回手势和 Android 的返回键行为并不完全一致需要判断是 EdgeBackGesture 还是常规返回动作在页面里统一做拦截处理。5. 常见问题与排查实录5.1 Flutter 鸿蒙适配中的典型编译错误开发过程中我遇到了大量编译报错这里挑几个有代表性的复盘。第一个是热词中提到的You are applying Flutters main Gradle plugin imperatively using the apply这个报错。这个问题本质上是 Flutter Gradle 插件的应用方式从apply脚本式改为 declarative 声明式造成的。解决办法是在项目的 android/settings.gradle 里显式声明插件版本并去掉 build.gradle 里的apply from: $flutterRoot/packages/flutter_tools/gradle/flutter.gradle这种命令式引用。涉及鸿蒙时逻辑类似但是需要在 ohos 的工程文件里检查 Flutter API 依赖是不是以 library 的方式正确声明。第二个是Could not close i开头的打包错误完整报错通常是java.lang.AssertionError或者是文件句柄没有关闭导致的无响应。这个问题出现在 Flutter 3.x 版本内置的 Gradle 任务与本地 Gradle 版本冲突时导致打包中间产物损坏。排查方法是把项目根目录下的 build/ 和 .gradle/ 目录清空后重新构建如果还报错就检查 JDK 版本鸿蒙打包要求 JDK 17用 JDK 11 一定会有奇怪的问题。第三个是热词里提到的 Flutter SDK 不受支持提示The current configured Flutter SDK is not known to be fully supported。这个问题在鸿蒙开发中尤其突出因为需要使用非官方适配分支的 SDK版本号经常与工具链预期不一致。我的解决方法是锁定一套经过验证的组合版本并写进团队的 README 里比如 Flutter 3.22 OpenHarmony SDK API 12 DevEco Studio 5.0避免不同成员用不同版本互相传递问题。5.2 TabBar 点击消除动画与页面切换状态保持热词里有一条“Flutter tabbar点击取消动画效果”我在项目里也碰到了类似的交互调优需求。TabBar 是管理“我的药品”和“用药记录”两个分类的标配组件但它默认自带一个点击动画在鸿蒙上由于 GPU 调度差异偶尔会显得卡顿。想关掉很简单在 TabBar 的Indicator上设置一个透明的BoxDecoration或者直接自定义 controller 的index赋值而不走animateTo方法就能实现点击即时切换、无滑动动画。“flutter navigator切换页面后丢失状态”这个问题也值得展开。我用的是 Navigator 1.0 API因为项目比较简单。常见丢状态的原因是用了pushNamedAndRemoveUntil或者把路由定义成了匿名路由导致状态无法重建。解决方案是全局只使用一个NavigatorKey所有页面都注册具名路由在切换后通过 Provider 或InheritedWidget让上层数据重新注入子页面。如果后续项目复杂度上去了需要多页面栈管理再迁移到 Navigator 2.0 的 Router API。5.3 Flutter Web 引擎启动慢的排查思路热词里有“flutter web 引擎启动慢”虽然这个项目的主目标是移动三端但我在调试便利性上确实会临时用 Flutter Web 跑 UI 预览。Web 启动慢的根源Flutter Web 应用启动时会把 CanvasKit 渲染库作为二进制文件从服务器加载如果网络慢启动时间会显着增加。解决方法是把渲染器换成 HTML 模式flutter run -d chrome --web-renderer html或者把 CanvasKit 静态资源部署到本地 CDN。值得一提的是这种 Web 预览不要用来验证平台相关的 Channel 调用因为浏览器里根本没有原生通道会一直报 MissingPluginException。5.4 鸿蒙平台调试方法论最后我把鸿蒙适配期的调试方法论总结一下。如果你手头没有鸿蒙真机又不想等模拟器DevEco Studio 的 Previewer 是唯一能预览 UI 的工具但它的 Value 局限很明显Previewer 无法触发 Flutter 的 Engine 启动只能看到 ArkTS 壳层的静态界面。想看 Flutter 真实渲染效果必须用模拟器或真机。模拟器我推荐用 DevEco 自带的 Phone 模拟器虽然启动慢了点但至少能完整跑通 Flutter Engine 加载和图像渲染。真机调试时有一个很实用的技巧在鸿蒙设置里开启“开发人员选项”的 USB 调试后用命令行hdc shell查看 logcatFlutter 的 Dart 层异常会以flutter标签打印出来。遇到 Dart 层看不到、原生层也看不到的诡异问题就主动加日志去标记函数的入参和出参二分定位到底哪一层出了问题。这个方法虽然笨但在跨端适配阶段效率最高。我的经验是接到鸿蒙适配需求千万不要慌不要试图一次性理解所有鸿蒙平台细节而是按“跑通链路 - 逐功能适配 - 打磨细节”三个阶段推进。家用药清单这个规模的项目以 Flutter 为基础用一套 Dart 代码覆盖三端鸿蒙侧只需要认真补好通知、定时、权限这几个原生能力的桥接整体工作量完全可以控制。之后要做扩展的话这个项目可以往三个方向迭代一是增加家庭成员管理让不同成员的用药记录独立统计但底层数据模型不用变二是把本地 SQLite 换成支持同步的数据库方案比如 CloudKit 或鸿蒙的分布式数据服务实现手机和平板之间的数据互通三是把提醒从“单机通知”升级成“可穿戴设备联动”正好利用 Flutter 已有的 wearable 插件生态。这些扩展都不需要推翻现有的架构这也是我坚持用 Flutter 做这个项目最核心的原因。
返回列表