接手这个项目的时候我其实有点低估了它的复杂度。需求听起来很简单做一个药品服用记录器能添加药品、设置服药计划、到点提醒、记录每次实际服用情况。但真正动手之后才发现这题的隐藏考点是“跨平台鸿蒙开发”。客户手里的设备既有老旧安卓机也有新款鸿蒙手机还有几台鸿蒙平板。客户明确要求一套Flutter代码同时跑通并且要有完整可靠的服药提醒机制——毕竟这是跟健康相关的工具类App记录错了、提醒漏了都是事故。如果你也是第一次把Flutter工程跑到鸿蒙设备上或者正准备做工具类App的鸿蒙适配这篇文章应该能帮你省下不少排查时间。我会从选型理由讲起到工程搭建、核心功能实现、真机适配坑点再到用药记录类App特有的验证清单完整走一遍这个项目的开发链路。1. 选型阶段为什么是Flutter以及鸿蒙适配如今做到什么程度1.1 这类工具类App的跨端需求画像先说需求背景。药品服用记录器属于典型的轻量工具类应用核心页面就那么几个药品列表、服药计划编辑、今日记录、服药日历。没有复杂的动效不需要重型3D渲染但有几个刚性的技术诉求。第一本地数据可靠性。服药记录是敏感数据用户不希望因为断网就丢记录所以核心数据必须以本地数据库为主云同步只是备份手段。第二通知提醒稳定性。到点提醒是这类应用的灵魂意味着必须能拿到系统的通知权限并且在应用被清理后仍能恢复提醒调度。第三多端一致性。用户可能在手机上设置计划在平板上查看记录两边的界面和交互必须一致。第四开发成本和维护成本。这类应用通常不是大团队产品可能就一两个人维护跨平台方案几乎是必然选择。从这几个诉求往前推Flutter的优势非常清晰单代码库覆盖Android、iOS、鸿蒙自绘引擎保证UI一致性本地SQLite生态成熟后台任务和通知有对应的插件或Channel方案。1.2 主流的跨平台方案对比以及Flutter的具体优势这个项目立项时我也认真对比过其他方案这里列个表给大家参考都是我实际用过的感受。方案跨端覆盖UI一致性原生能力调用鸿蒙适配成熟度适合场景Flutter多端强自绘引擎通过Channel调用较为成熟已有适配方案与构建流程自绘UI要求高、逻辑复用多的应用React Native多端依赖原生组件两端需调依赖原生桥接适配程度与第三方库生态有关偏逻辑复用、UI走原生样式的产品uni-app多端一般依赖各家小程序规范有适配思路但复杂原生能力仍需自建插件小程序与跨端快速上线场景纯原生双端强强最强各自独立开发团队规模大、对原生体验极致追求说实话如果只做一个平台原生是最省心的。但药品记录器这种工具类应用多端同步上线是刚需。两边各写一套的时间成本翻倍后续维护还要两个人盯对小型团队很不友好。Flutter的加分项在于自绘UI引擎让每端渲染效果几乎一致日期选择器、时间选择器、卡片列表这些交互组件不需要针对性适配逻辑层天然跨端数据库、状态管理、文件存储都能复用同一套Dart代码再加上鸿蒙设备的Flutter适配链路已经能走通工程配置、产物构建、设备部署都有清晰流程这个方案就顺利成章地定下来了。1.3 鸿蒙适配的现状能跑、能调、能上架的完整链路很多人的印象还停留在“鸿蒙不能跑Flutter”的阶段实际这两年变化挺大。要做到鸿蒙设备上跑Flutter应用目前走的路径是基于鸿蒙系统的开源兼容层配合Flutter引擎的鸿蒙适配分支来完成编译和运行。具体落地时分三层底层是鸿蒙系统的应用运行环境中间是Flutter引擎在鸿蒙环境下的适配层负责把Dart代码生成的UI指令转换为鸿蒙可执行的渲染调用上层就是你熟悉的Flutter框架层所有业务代码写在这一层完全不用区分平台。这意味着用户平时写的Flutter业务代码、pub依赖、本地数据库逻辑绝大多数可以直接复用。真正需要额外处理的是构建配置和系统能力接入比如通知权限、桌面图标、启动页、文件路径这类涉及平台特性的东西。这里必须说清楚我下面写的是基于适配SDK的通用操作流程不同的SDK版本和开发环境会有细节差异但完整的可操作链路是存在的。实际执行时请以你拉取的那份适配文档为准。2. 工程搭建Flutter鸿蒙开发环境配置与首个Demo2.1 环境准备与版本组合这个项目的环境配置我前前后后折腾了两天记录一下最终稳定可用的组合方便大家少走弯路。操作系统Windows 10/11 或 macOS我用的是 WindowsFlutter版本使用支持鸿蒙适配的Flutter SDK分支而不是修改版模拟器方案鸿蒙开发工具安装DevEco Studio并在SDK Manager里安装对应的鸿蒙SDK适配SDK按Flutter官方或适配项目文档拉取构建所需的鸿蒙适配层代码这里有一个最容易踩的坑版本组合。鸿蒙SDK的版本、Flutter SDK分支的版本、编译工具链的版本三者必须匹配否则会在构建阶段报一堆看不懂的错。我建议直接看官方适配仓库的README上面通常会写明已经验证过的版本组合。有时候你看到别人电脑上跑得好好的工程自己一构建就失败大概率就是SDK版本对不上。环境的另一个坑是环境变量。鸿蒙SDK和Flutter SDK的路径都要加入环境变量而且要保证命令行的访问是通畅的。我自己遇到过一次Flutter能正常编译但一走到鸿蒙打包阶段就找不到SDK路径最后发现是环境变量只加了PATH没有加DEVECO_SDK_HOME这个独立变量。2.2 新建工程、运行到鸿蒙设备环境配好后创建一个新的Flutter工程flutter create medication_recorder这个命令没什么特别的生成的是标准Flutter工程。接着的关键步骤是把鸿蒙平台接入工程。不同适配SDK的接入方式略有区别有的是在项目根目录执行初始化命令有的是提供一个模板让你覆盖工程文件。以我用的这套方案为例执行初始化命令后工程目录下会多出一个可供DevEco Studio打开的鸿蒙工程目录这时候工程就具备了双平台构建条件。在命令行里执行flutter devices如果一切正常你就能在设备列表里看到鸿蒙设备。连接方式有USB调试和无线调试两种我个人建议优先用USB稳定很多。无线调试偶尔会出现设备列表能看到、但应用安装失败的情况日志信息又很模糊排查起来很被动。确认设备可见后首次冷启动依然建议用DevEco Studio打开鸿蒙工程目录把工程完整跑通一遍。不要一上来就在命令行里强行运行命令行模式虽然能简化流程但对新工程而言如果出现缺失授权或签名问题命令行报错远没有DevEco Studio里那么直观。等到在DevEco里跑通了再回到命令行执行日常的flutter run也不迟。2.3 工程目录里值得提前了解的差异跑通第一个Demo后别急着写业务代码先花半天时间把工程目录的结构差异搞清楚。跨平台开发的很多适配问题根源都在这里。鸿蒙工程的构建产物配置、权限声明、桌面图标资源都集中在鸿蒙平台目录里跟Android的AndroidManifest.xml和iOS的Info.plist是同一类角色。你需要重点确认几个文件应用的配置入口、权限声明文件、桌面图标目录、启动页配置。然后还有一个非常关键的地方应用包名。Flutter工程默认的Application ID是com.example.xxx如果你打算后续正式发布请从第一天就改成你自己的域名倒序格式。这个东西在后期改动牵扯的配置面特别广——鸿蒙侧、应用内上报、推送服务注册到处都会用到。我见过太多项目在后期改包名改到怀疑人生。3. 药品服用记录器的核心模块从数据模型到提醒逻辑3.1 药品库与服药计划的数据模型设计药品服用记录器的业务不复杂但数据模型不能拍脑袋写。这种健康工具类应用最怕的是“编辑期随意用起来崩溃”。我设计了三张核心表药品表、服药计划表、服用记录表。药品表存基础信息药品名称、规格、单位、备注。字段不多但要注意一个细节药品名称不要直接做成字符串查询字段而是同时保留一个全拼首字母索引字段方便列表页按字母快速定位。工具类应用做久了会发现用户对“找到药”这个路径的效率要求非常高。服药计划表是核心它描述的是“哪一天、哪个时间点、吃多少”。这里我建议不要用单一的重复规则字段而是拆成两个维度时间点和重复类型。重复类型包含每天、每周某几天、每隔N天、按具体日期等。拆开的好处是后续做日历视图时可以直接根据计划生成一整月的时间轴不需要每次展开复杂的规则解析。服用记录表则记录用户实际的服药行为计划ID、计划时间、实际服用时间、服用剂量、状态。状态字段至少要有“按时服用”“延迟服用”“漏服”“手动补录”四种。有了这个状态日历上就能展示不同颜色的标记用户一眼就知道自己本周执行得怎么样。三张表的关联关系并不复杂唯一需要认真处理的是索引设计。高频查询场景是“某天范围内所有计划及其实际状态”所以服用记录表上必须建立一个包含“计划时间”和“状态”的复合索引否则数据量到几千条的时候日期筛选就会明显变慢。3.2 本地数据库封装与服药记录写入数据层我用的是sqflite插件这是Flutter生态里最成熟的SQLite方案。封装思路不搞花活就是一个单例数据库管理器提供各表的增删改查方法。这里说一下建表细节。时间字段我统一存INTEGER类型也就是毫秒时间戳。对比一下有人喜欢存TEXT类型格式方便调试但查询时做大小比较就很别扭。毫秒时间戳在工具类应用里是最省心的选择日期筛选、排序、时区转换都方便。class MedicationDatabase { static final MedicationDatabase _instance MedicationDatabase._(); static const _dbName medication_recorder.db; static const _dbVersion 1; Database? _db; MedicationDatabase._(); FutureDatabase get database async { _db ?? await _openDb(); return _db!; } FutureDatabase _openDb() async { final dbPath await getDatabasesPath(); final path $dbPath/$_dbName; return openDatabase(path, version: _dbVersion, onCreate: (db, version) async { await db.execute( CREATE TABLE medicines ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, spec TEXT, unit TEXT, short_code TEXT ) ); await db.execute( CREATE TABLE schedules ( id INTEGER PRIMARY KEY AUTOINCREMENT, medicine_id INTEGER NOT NULL, time_in_day INTEGER NOT NULL, repeat_type INTEGER NOT NULL, dosage REAL NOT NULL, note TEXT ) ); await db.execute( CREATE TABLE intakes ( id INTEGER PRIMARY KEY AUTOINCREMENT, schedule_id INTEGER NOT NULL, plan_time INTEGER NOT NULL, actual_time INTEGER, dosage REAL, status INTEGER NOT NULL DEFAULT 0 ) ); await db.execute( CREATE INDEX idx_intakes_plan_time ON intakes(plan_time)); await db.execute( CREATE INDEX idx_intakes_status_time ON intakes(status, plan_time)); }); } }上面这段是简化版核心骨架重点看两个地方一是status字段和plan_time的复合索引二是时间字段全部用INTEGER。这两点决定了后续日历页和统计页的查询性能。业务写入流程也需要有一个统一入口。每次用户点击“我已服用”或系统触发提醒后自动记录都走同一个方法。这个方法内部要做两件事记录本次实际服药状态同时根据计划时间判断本次是按时、延迟还是提前服用。具体判断逻辑是拿实际时间和计划时间做对比时间差超过30分钟标记为延迟早于计划时间超过30分钟标记为提前。这个规则要与产品明确后再固化成常量不要写在各处业务代码里。3.3 服药提醒通知权限、调度策略与平台通道提醒模块是药品记录器里技术含量最高的部分也是鸿蒙适配期间问题最多的部分。我把它拆成三层通知展示层、调度层、权限层。通知展示层使用Flutter本地通知插件负责展示提醒标题、正文和点击跳转。这里要强调一件事通知渠道的区分。药品提醒和高频业务提醒要使用不同的通知渠道Android和鸿蒙对渠道的管理逻辑类似用户可以在系统设置里单独静默某一个渠道。如果你把所有通知塞进一个默认渠道用户一旦觉得提醒推送太频繁而关掉渠道药品提醒也连带着没了这是很危险的。调度层的实现要实用一些。方案A是使用本地通知库自带的周期调度能力方案B是自己写一个后台任务。我的建议是优先方案A因为它调试简单、行为可预期而且不会因为应用进程被清理而失效。以每天定时提醒为例直接把调度注册到通知库的周期任务里对每隔N天的重复规则在每次应用启动或计划编辑后重新计算下一次触发时间手动注册一次性的通知。第三层是权限层。这里有一点非常重要不能只检查通知权限还要检查应用是否被允许后台启动。鸿蒙和Android的后台管理机制都趋向严格用户可能已经打开了通知权限但应用被系统清理后提醒无法恢复。因此应用启动时需要做一次状态自查如果发现应用被后台限制或电池优化锁定应引导用户调整设置。权限调用同样通过平台通道完成。我封装了一个PermissionChannel用MethodChannel处理权限检查、权限请求、后台任务恢复状态查询等操作。通道调用的代码并不复杂核心是要在UI层处理好权限回调的时序不要一进入首页就弹一堆权限请求框用户会很反感。正确做法是首次进入先弹出说明页讲清楚需要哪些权限、用于什么功能用户点确认后再逐个请求。这个模块在鸿蒙适配中踩了我最大的一个坑后面单独开一节详细说排查过程。4. 适配期真正头疼的问题完整排查链路与修复过程4.1 白屏与首帧问题构建产物打包不完整这是我遇到的第一个严重问题应用能安装到鸿蒙真机上但打开后一直白屏没有任何报错日志。Flutter应用在调试模式下跑得好好的一打包成release版本就白屏这是最典型的“构建产物不完整”信号。排查链路我走了一遍值得分享第一步查设备端日志。用命令行工具抓取应用运行日志重点看Flutter引擎是否启动。如果日志一条都没有说明压根没执行到引擎如果引擎已经起来但没有渲染UI那是另一个方向。第二步确认产物打包阶段是否正常。打包发布版时如果流程要求先执行Flutter编译命令、再把生成的产物拷贝到鸿蒙工程目录那么任何一个步骤失败或路径写错都会导致安装包里的Flutter资源缺失。第三步手动检查安装包。把生成的安装包解包确认里面是否存在必要的Flutter产物目录。如果你看到app目录下没有对应架构的so文件或者assets目录缺少flutter_assets那问题就定位了。我这次白屏的根因确实是打包产物缺失。适配SDK的构建脚本要求先由Flutter侧生成特定格式的产物再通过脚本注入鸿蒙工程我原本是手动执行拷贝的漏了一步导致资源没进去。修复方式是把整套打包流程固化为一个自动化脚本而不是靠手动操作。提示跨平台适配阶段凡是遇到“调试正常、发布白屏”第一时间怀疑构建产物不必先怀疑代码逻辑。4.2 通知渠道不响权限、通道名与应用内设置联动问题提醒不响这个问题最折磨人因为“用户角度没声音”和“代码里已经触发”是两回事。我排查了整整一个下午才定位。完整的排查过程是这样先用测试按钮触发一次通知发现通知栏没有任何显示。第二步检查权限发现通知权限是开启的。第三步检查渠道设置发现默认渠道被停用了——原因是在测试阶段我高频触发通知系统检测到频繁推送后自动把该渠道静音了。这个问题的教训很深药品提醒一旦依赖系统通知渠道就必须在渠道维度做自愈。应用启动时检查关键渠道是否被关闭如果是关键渠道被关闭可以提示用户恢复。不能用一个“通知总开关”检查糊弄过去因为用户可能关的是某个渠道而不是全部。对鸿蒙适配来说通知渠道的配置也需要在应用内对应声明而且要在首次初始化时调用创建渠道的接口。多端的渠道策略必须统一否则会出现“Android上渠道有效、鸿蒙上渠道失效”的差异表现。4.3 桌面图标、启动页与中文渲染的几个细节这些比较碎但也很能影响体验。桌面图标方面鸿蒙工程要求的图标尺寸和Android不同切勿直接复用Android的mipmap资源。如果直接复用可能出现图标边缘被裁剪、模糊、尺寸过大或过小的问题。正确做法是按鸿蒙工程的资源目录要求单独生成一套图标资源放进去。启动页方面Flutter默认的启动页是一块白屏或品牌色块在真机上的表现是点击图标后有一段时间的空白如果不配置启动页图片用户会感觉应用“没反应”。这个在白底UI上尤其明显。我的做法是准备一张带应用Logo的启动图放进鸿蒙启动页配置中至少让冷启动那几百毫秒有一个视觉反馈。中文渲染方面Flutter默认字体对中文支持没有问题但要注意自定义字体的资源加载路径。如果你在全局用自定义字体一定要把字体文件打包进assets目录并在pubspec.yaml中声明。否则会出现界面英文和数字是自定义字体、中文悄悄回落成系统字体的割裂现象。5. 用药安全类应用独有的正确性验证清单5.1 日期边界与服药间隔跨天、时区、重复用药场景工具类App的测试往往比较宽松但涉及用药记录的App边界情况的测试一定要严否则可能产生记录缺失或记录错误。我总结了一个验证清单发布前逐项过。第一跨天重复计划。用户设置每天08:00和20:00服药时间跨到第二天日历标记和当天待办列表必须正确显示。测试方法手动把设备时间调到23:50观察跨零点后当天列表是否切换到新一天的记录。第二每隔N天的计划。这个容易在计算下一次日期时出错。比如每隔3天服一次计划日期必须是“起始日期 3天”而不是“自然周的某一天”。很多人在日期的星期计算上栽过跟头。第三时区变化。用户出差跨时区服药计划时间按本地时间展示和提醒还是按原时区时间这个要在产品层面定清楚。我的建议是提醒触发按设备本地时间但记录要保留原始计划时区的计划时间戳和本地实际服用时间戳两者都存。这样事后分析不会混淆。第四重复用药防护。用户在08:00提醒时记录了“已服用”然后在10:00又手动点了一次“补录本次”——这时候如果代码没有判断就会出现同一计划同一天被记录两次服药的脏数据。我的做法是在服用记录写入前先查该计划当天是否已经有状态为“按时/延迟”的记录若存在则弹出确认对话框而不是无脑写入。5.2 提醒频控与应用常驻进程被杀后的恢复机制用药提醒失效最常见的原因不是代码写错了而是应用进程被系统杀掉后没有恢复机制。针对这个场景我在应用层做了三件事。第一启动时自检所有未失效的提醒重新注册。用户清理后台应用后下一次手动打开App系统会立刻把提醒调度补齐。第二如果应用被系统冻结基础通知如果注册到了系统通知中心仍能按时展示但需要应用在注册时指定正确的触发策略。第三在“关于页面”增加一个“自检提示”引导用户检查后台运行权限和电池优化设置这是面向普通用户最直接的兜底手段。另外要注意不要滥用后台任务。有些开发者习惯用后台任务保持进程存活这对工具类应用来说很不合适既耗电又容易被系统判定违规。稳定的提醒调度优先级一定是“系统通知中心调度”而不是“进程活着没活着”。5.3 数据备份与迁移换机不丢记录的兜底逻辑最后说数据兜底。记录类App不做数据备份迟早要被骂。推荐方案是用本地文件导出和导入提供一个导出功能把SQLite数据库文件加密压缩后导出到用户指定的本地位置导入时反向上传并替换。云同步可以考虑但作为工具类应用本地导出导入是必须有的保底能力成本低、逻辑清晰、不依赖任何第三方服务。这里的实现坑点在于数据库文件的读写时机。导出前必须确保数据库连接没有被占用否则导出的文件可能是不一致状态。我的做法是导出一个独立的只读事务在事务里读取记录并生成导出文件完成后立即提交。导入时则相反先校验文件完整性再备份现有数据库最后完成替换。关于数据库迁移版本控制在建表阶段就要留好。我用的dbVersion初始版本是1后续不管是加字段还是加表版本号递增并在onUpgrade回调里按版本号逐段执行变更SQL。绝对不要真的去改已发布的建表语句那会让老用户升级后直接崩溃。这套验证逻辑走完之后这个项目基本就进入稳定期了。回想整个过程最花时间的其实不是业务功能本身而是跨平台适配时那些“看起来没问题、用起来全是问题”的隐性坑。药理记录器这种应用用户可能一天要打开三四次每一次提醒响不响、每一条记录准不准都会被直接感知。如果你正在做类似项目建议把前面几个章节提到的适配细节和验证清单打印出来发布前逐项对照一遍能替你拦下不少线上事故。