OpenHarmony+Flutter实战:猫咪管家驱虫记录模块开发全解析 做猫咪管家App之前我其实没想过要把驱虫记录做成一个单独的实战主题来写。但这个功能在OpenHarmony上用Flutter实现的过程确实让我踩了不少坑也验证了一条很重要的结论Flutter for OpenHarmony的生态已经能让业务级功能稳定落地了只是你得知道哪些地方和Android上不一样。这篇文章就围绕“猫咪管家App的驱虫记录模块”把从数据设计、界面实现到平台适配的全过程拆开讲适合正在做Flutter跨平台迁移、或者准备在OpenHarmony设备上跑Flutter应用的朋友参考。驱虫记录这个功能表面上看就是“填个日期、存个药品名、到点提醒”但真动手做你会发现它牵扯到数据模型设计、本地存储选型、日期计算、通知提醒、平台差异化适配等一系列问题。猫咪驱虫这件事本身也有业务逻辑体外驱虫一般每月一次体内驱虫通常每三个月一次记录完上一次还得自动推算下一次时间不然铲屎官很容易忘。所以这个模块虽然小却是整个猫咪管家App里最能体现“工具类应用价值”的部分。1. 项目背景与需求拆解1.1 为什么要在OpenHarmony上用Flutter先说背景。我当时接到一个项目要把一套已经在Android和iOS上跑通的猫咪管家App迁移到OpenHarmony设备上。这套App的功能不算复杂猫咪档案、喂食记录、疫苗记录、驱虫记录、洗澡提醒核心就是帮养猫人把琐事管起来。选型的时候面临两条路一是用ArkTS重写一遍二是用Flutter for OpenHarmony做跨平台复用。重写意味着所有业务逻辑、UI组件、数据层都要在鸿蒙生态里重新实现一遍工作量翻倍而且以后Android/iOS和OpenHarmony两个版本要维护两套代码。Flutter for OpenHarmony的价值就在于Dart代码和Flutter框架层可以尽量复用底层只替换平台通道相关的实现。实际验证下来Flutter for OpenHarmony的渲染引擎已经能比较稳定地跑复杂UI了列表滚动、路由跳转、状态管理、本地数据库这些基础能力都没问题。第三方插件确实有些不能用但大部分都有替代方案或者可以自己写平台通道。驱虫记录这种中等复杂度的业务模块正是检验这套技术栈的最佳试金石——你说它简单吧它涉及数据持久化和通知你说它复杂吧又不需要太深的硬件联动。1.2 驱虫记录功能到底要解决什么猫咪驱虫不是一件可以随缘的事。体外驱虫药滴剂一般建议一个月一次体内驱虫药片剂通常三个月一次具体频率还会因猫的生活环境、是否外出、体重等因素调整。很多铲屎官不是不想记是记了也会忘或者根本不知道上次什么时候做的。所以驱虫记录模块的核心需求是记录每次驱虫的药品、剂量、方式体内/体外、日期、备注。根据驱虫方式自动推算下一次驱虫日期。在临近下一次日期时提醒用户。支持查看历史记录方便回溯。和疫苗记录不同驱虫记录是持续性、周期性的所以“推算下一次时间”和“提醒”是这个模块的灵魂。最初的产品需求里还有“按照猫咪个体分别记录”因为家里养多只猫的情况太常见了每只猫的驱虫周期是独立的。我把这个也放进了数据模型里宁可前期多设计一个catId字段也不要后面重构表结构。这个需求拆解做完之后我心里其实就有谱了这本质上是一个标准的CRUD 日期计算 本地通知的功能组合难点不在功能本身而在Flutter for OpenHarmony的适配细节上。2. 数据模型与本地存储方案2.1 设计一条驱虫记录需要哪些字段先看数据模型。驱虫记录表的设计我一开始想得很简单后来加了几个字段才变得实用。最终表结构如下CREATE TABLE deworming_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, cat_id INTEGER NOT NULL, product_name TEXT NOT NULL, dose TEXT, method TEXT NOT NULL DEFAULT external, -- external体外, internal体内 deworm_date TEXT NOT NULL, next_date TEXT NOT NULL, note TEXT, created_at TEXT NOT NULL );逐字段说一下思考过程。cat_id是必须的猫咪管家App天然支持多猫用户如果不加这个字段后面做“切换猫咪档案”功能时只能靠临时筛选查询效率低且逻辑混乱。product_name是用户最关心的信息之一。不同品牌的驱虫药覆盖的虫谱不一样跳蚤、虱子、蜱虫、心丝虫、钩虫等记录药品名能帮助用户判断是否需要补充驱虫。这个字段我设置成必填因为驱虫记录如果没有药品名基本等于白记。dose是剂量信息。滴剂通常按猫咪体重选择规格如0.5ml/支片剂按体重计算片数。这个字段允许为空因为有些用户确实只记得大概但出于后续统计的考虑宁可留空也不要填错单位所以我干脆不做单位换算只存原始字符串。method决定了时间推算逻辑。这是整个模块业务逻辑的核心分支点体外驱虫默认间隔30天体内驱虫默认间隔90天。我把默认值写死在配置里而不是硬编码在页面里这样后面如果需要支持“某些驱虫药有效期是45天”这类规则扩展起来更容易。deworm_date和next_date用纯字符串存格式统一为yyyy-MM-dd。为什么不用时间戳因为日期计算和显示都只到天级别时间戳在这种场景下只会增加转换成本而且SQLite里直接按字符串比较日期也更直观。note字段存备注比如“猫咪有点应激反应”“这次换药了”之类的信息。created_at字段存记录创建时间用于列表按时间排序。这两个字段看着冗余但实际排查问题时非常有用。2.2 本地持久化sqflite在OpenHarmony上的适配数据存储我一开始尝试了两条路。第一路是用JSON文件直接存列表简单粗暴但数据量大了之后每次读写都要全量序列化而且查询、更新、删除都需要遍历体感很差。第二路是直接用OpenHarmony的RelationalStore关系型数据库功能没问题但相当于把数据层绑死在鸿蒙平台上后续如果有回归Android的需求数据层就要重写。最终我选择了sqflite的OpenHarmony适配版本。关于sqflite在OpenHarmony上的适配说实话社区已经做过不少工作有编译好的支持OpenHarmony的插件分支可以直接通过修改pubspec的git依赖来使用。dependencies: sqflite: ^2.3.0 sqflite_common_ffi: ^2.3.0这里有个容易踩的坑如果直接把标准sqflite插件拿来用在OpenHarmony设备上初始化时会报找不到平台实现MissingPluginException。解决方法是使用sqflite_common_ffi它会通过Dart的FFI去调用SQLite的原生实现绕开Android/iOS专属的平台通道。在OpenHarmony上实测下来这个方案完全可行读写性能也够用。数据库初始化我封装成了单例模式并且加了版本管理。class AppDatabase { AppDatabase._(); static final AppDatabase instance AppDatabase._(); Database? _db; FutureDatabase get database async { if (_db ! null) return _db!; _db await _open(); return _db!; } FutureDatabase _open() async { final dbPath await getDatabasesPath(); final path $dbPath/cat_manager.db; return openDatabase( path, version: 1, onCreate: (db, version) async { await db.execute( CREATE TABLE deworming_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, cat_id INTEGER NOT NULL, product_name TEXT NOT NULL, dose TEXT, method TEXT NOT NULL DEFAULT external, deworm_date TEXT NOT NULL, next_date TEXT NOT NULL, note TEXT, created_at TEXT NOT NULL ) ); }, ); } }这里最需要注意的是getDatabasesPath()在OpenHarmony上返回的路径。在Android上它通常是/databases目录在OpenHarmony上实测返回的是应用沙箱内的data路径这个差异看起来无关痛痒但如果你后续要做数据库备份、导出功能路径写死了就会出问题。我建议把这个路径打日志打出来真机调试时确认一次别想当然。数据库的工作做完了接下来要看日期计算和提醒这才是驱虫记录真正区别于普通CRUD的地方。3. 界面与交互的实现3.1 记录列表页的设计与状态管理UI层我采用了一个比较常规的三段式结构列表页、新增/编辑页、详情弹窗。列表页是入口承担历史记录展示和下次驱虫日期的状态提醒。状态管理选型上这个项目用的是Provider。为什么不选Riverpod或者Bloc因为猫咪管家App整体规模不大Provider足够简洁而且OpenHarmony的Flutter引擎对Provider这种基于InheritedWidget的机制支持得非常好不需要额外的原生插件参与几乎不会出现平台适配问题。团队里如果有新人也更容易上手。列表页的核心逻辑可以概括为从数据库查询当前选中猫咪的记录按创建时间倒序排列然后根据“当前时间是否超过next_date”来决定卡片的状态标签。具体状态我做了三种未到期距next_date还有超过7天绿色标签“按时”。临近到期距next_date在7天以内橙色标签“即将驱虫”。已过期当前时间已超过next_date红色标签“已过期”。这个状态判断看起来不复杂但有一个细节容易忽略你需要在Dart层把记录的next_date转成DateTime对象再比较不能直接用字符串比较因为yyyy-MM-dd的字符串虽然字典序等于时间序但你无法直接计算“差多少天”。我封装了一个工具方法int daysUntil(DateTime target) { final now DateTime.now(); final today DateTime(now.year, now.month, now.day); final targetDay DateTime(target.year, target.month, target.day); return targetDay.difference(today).inDays; }这里我先把时间归一化到当天零点再计算差值避免时分秒带来的误差。比如现在是晚上23点距目标日期还有2天但这2天里实际只剩下1小时如果直接用DateTime.now()去减inDays可能就是0了——这种细节不处理干净用户会看到“明明明天就到期了App却说还有0天”体验很糟糕。列表项我用Card组件实现左侧放“驱虫方式”图标和药品名中间放日期信息和备注摘要右侧放状态标签。整个卡片在构建时尽量使用const构造减少不必要的重建。有一次我测试列表滚动发现滑动有点掉帧后来用Flutter DevTools的Performance分析了一轮罪魁祸首是列表项里有一个动态生成的BoxShadow每次重建都重新计算。改成静态样式之后滑动明显顺畅了。这种问题在Android上不算致命但在OpenHarmony模拟器上会被放大所以写列表页面时一定注意控制组件重建的粒度。3.2 新增记录表单与日期选择新增和编辑我复用同一个页面通过传入一个可空的记录对象来区分传入null就是新增传入已有记录就是编辑。这个页面包含以下控件猫咪选择器如果用户有多只猫药品名称输入框驱虫方式选择体外/体内驱虫日期选择器下次驱虫日期选择器默认根据方式自动推算备注输入框保存按钮药品名称和驱虫方式是必填项日期默认是今天下次驱虫日期默认自动推算。自动推算的逻辑是这样的DateTime _calculateNextDate(DateTime dewormDate, String method) { final intervalDays method external ? 30 : 90; return dewormDate.add(Duration(days: intervalDays)); }用DateTime.add(Duration(days: 30))处理跨月、跨年都没问题不需要自己写进位逻辑。这里要纠正很多新手会犯的错误不要用dewormDate.add(Duration(days: 30))然后去格式化字符串手动拼接也不要尝试用“月底最后一天”这种逻辑判断Dart的DateTime已经在内部处理好月份长度了。实测下来1月31日加30天会得到3月2日因为2月只有28天这是说得通的。如果你希望得到“3月1日”那就需要改成以自然月为单位计算但驱虫药的有效期一般按天算就够了。日期选择器用showDatePicker这个组件在OpenHarmony上运行正常不过有一个小问题日期选择器的弹窗样式用的是Material风格在部分OpenHarmony设备上主题字体渲染会稍微偏小。我当时在真机上看到日期选择器里的文字有点发虚后来在ThemeData里显式设置了textTheme把字体大小调大了一档问题就缓解了。这个属于视觉微调不影响功能但如果做的是面向终端用户的产品值得花时间打磨。表单校验我选择在保存按钮的onPressed里统一校验而不是每个输入框单独实时校验。原因很简单这是一个表单页用户填写的顺序不固定实时校验会造成不必要的红色错误提示干扰。统一校验逻辑如下药品名为空时提示“请填写药品名称”并聚焦到对应输入框。驱虫方式未选择时提示“请选择体内或体外驱虫”。下次驱虫日期早于本次驱虫日期时提示“下次日期不能早于本次日期”。这里有个小的交互技巧。校验失败时不要只弹SnackBar最好同时把焦点聚焦到出错的控件上用户能马上知道问题在哪。SnackBar的时长设为2秒到3秒太短来不及读太长会挡住表单下方的按钮。3.3 提醒功能的实现与兼容处理驱虫记录不做提醒就失去了一半价值。提醒功能我分成了两个层次第一层是App内的状态提示。在首页或者猫咪详情页显示“距离下次驱虫还有X天”这个是即时查询数据库算出来的不需要任何通知权限属于兜底方案。第二层是系统级的本地通知到点了在通知栏推一条“该给XX做驱虫了”。第二层在OpenHarmony上的实现是本次开发里最折腾的部分。Android上我习惯用flutter_local_notifications插件但在OpenHarmony上直接使用这个插件会遇到平台通道缺失的问题。经过排查解决方案是用OpenHarmony的本地通知接口写一个简单的平台通道由Dart端调用。平台通道的核心思路很简单Dart端通过MethodChannel调用原生侧ArkTS原生侧调用OpenHarmony的NotificationManager按传入的时间和内容创建通知。我实现了三个方法scheduleNotification按指定时间创建一次性通知。cancelNotification取消指定ID的通知。hasNotificationPermission检查权限。这里必须说清楚一个问题OpenHarmony的通知需要申请通知权限而且不同版本的API权限方式还不完全一样。我当时在某个版本的模拟器上测试NotificationManager.requestEnableNotification弹出了授权弹窗但真机上因为系统设置里通知默认关闭弹窗没有出现导致我在真机上一直收不到通知。排查了半天最后发现是“通知”总开关没打开。这个经验告诉我在OpenHarmony上调试通知功能拿到设备后第一件事就是检查设置里的通知总开关别等代码写完才去看。提醒的调度逻辑也很关键。新增记录保存时先计算next_date然后在当天早上9点安排通知。这个设计是综合用户习惯选的早上9点提醒驱虫比半夜提醒更容易被用户看到并执行。实现上我把“通知时间”预设成next_date当天的09:00。在保存记录时调用平台通道安排通知并把通知的ID设置为记录的id这样删除记录时能按id取消通知不会出现“记录删了但通知还在”的尴尬情况。4. 常见问题与排查实录4.1 第三方插件的兼容性排查Flutter开发最大的倚仗就是pub.dev上海量的插件但在OpenHarmony上这个倚仗打了不少折扣。我这次至少遇到三个插件层面的问题第一个是path_provider。标准path_provider在OpenHarmony上同样会报MissingPluginException。解决思路和sqflite一样找支持OpenHarmony的适配分支或者直接用getDatabasesPath替代。我后来看了下实现OpenHarmony的Flutter适配社区已经为几个常用插件做了fork遇到问题先去查社区适配列表比自己写平台通道省事得多。第二个是intl。日期格式化我用的是intl包里的DateFormat正常思路是DateFormat(yyyy-MM-dd).format(date)。但OpenHarmony设备上我发现默认语言环境可能没有正确回退到中文导致某些月份、星期的中文显示不出来。解决方法是初始化时手动指定localeimport package:intl/intl.dart; void main() { Intl.defaultLocale zh_CN; runApp(CatManagerApp()); }这里还有一个更隐蔽的坑intl的locale数据在Flutter引擎里是按需加载的如果只设置了locale但对应的date_symbol_data没有初始化某些手机上还是会显示英文。稳妥做法是调用initializeDateFormatting(zh_CN)后再格式化或者干脆不用intl直接用padLeft手写格式化逻辑。我最终选择了一个折中方案核心日期展示全部自定义格式化只有统计图表里的月份简写才用intl。第三个是flutter_local_notifications。如上所述Android/iOS的实现依赖各自的平台插件OpenHarmony上直接跑不了。我的建议是不要在这个插件上死磕直接自研平台通道。喵咪提醒这种需求其实很简单原生侧创建一个定时任务到点发送一条通知。写不了50行ArkTS代码。4.2 热重载、数据库路径、列表性能问题适配新平台总有一些让人哭笑不得的小问题。这里挑三个我认为最有代表性的分享。热重载不全OpenHarmony的Flutter引擎在我用的那个版本中热重载偶尔不彻底。有时候改了Dart代码点击热重载界面没变化但代码逻辑确实变了。排查这个问题浪费了我不少时间一度怀疑是页面缓存导致的状态不同步。后来摸索出的规律是热重载不生效时手动触发一次整页路由跳转比如从首页推到详情页再返回新代码就会生效。如果还不行只能冷重启App没有更好的办法。建议在OpenHarmony上开发时养成“改核心逻辑后直接热重启”的习惯别依赖热重载提效。数据库路径不适配前面提到过getDatabasesPath在OpenHarmony上返回的路径和Android不同。具体来说Android上通常是/data/user/0/包名/databases而OpenHarmony上返回的是应用沙箱路径/app/data/...。如果你的App有数据备份、导出CSV这些功能注意路径通用性。我最后把自己的数据库操作全部收敛到AppDatabase这一个类里路径只在这里算一次后续不裸用字符串。列表性能下降这是最容易被忽略的。在Android模拟器上跑起来流畅的列表在OpenHarmony模拟器上可能明显掉帧。我按老经验先把Card的阴影改成无色再避免每帧重建依然不够。真正解决问题的是找到罪魁祸首列表项里使用了自定义字体的Text而每个Text在重建时都重新加载了字体资源。把字体加载缓存到全局变量后列表滚动就顺了。所以遇到OpenHarmony上列表卡顿优先排查资源加载字体、图片、图标而不是盲目优化build方法。4.3 驱虫日期状态计算的边界情况日期计算看着简单实际用起来会遇到不少边界情况。我整理了几个必须处理的场景第一补记历史记录。有些用户可能已经很久没驱虫了现在突然要记录选的日期是三个月前。这时候计算出来的next_date是过去的时间列表页会正确显示“已过期”新增记录保存后App内也会有提示。这个逻辑本身没问题但要注意不要在保存时因为“已过期”就拦截用户否则用户永远补记不了历史记录。第二编辑已有记录时如果用户改动了驱虫日期next_date要同步更新。但用户也可能手动改了next_date这时不能再用deworm_date method去覆盖否则用户手动设定就白做了。我的处理方式是只监听deworm_date和method的变化只有这两个字段改动时才自动刷新next_date手动改过next_date后自动推算不再触发。第三删除记录时同步取消通知。如果用户删除了某条驱虫记录但通知还挂在系统里到了时间照样弹出来用户体验极差。我的做法是删除记录时先拿到记录id调用cancelNotification取消对应通知再执行数据库删除删除成功后再刷新列表。顺序不能反过来否则刷新列表时记录已经不存在了你就拿不到id了。5. 实操心得与后续扩展方向驱虫记录这个模块从开发到稳定前后大概用了一周时间大部分精力都花在平台适配和细节体验上。如果要我给准备做Flutter for OpenHarmony项目的朋友三条建议第一是“数据库和通知这两块核心能力尽早做技术验证”不要等到UI都写完才发现数据层跑不通第二是“日期计算、状态判断、通知调度这些业务逻辑全部分离出来写单元测试”这能帮你快速验证平台差异之外的正确性问题第三是“多准备几台不同版本的真机”模拟器只能帮你验证交互通知权限、字体渲染、数据库路径这些必须在真机上才能暴露问题。我自己做完这个模块最大的感受是Flutter for OpenHarmony的成熟度已经超过了我的预期。渲染性能、基本组件、网络请求都能正常工作只是生态工具链还需要时间补齐。驱虫记录这个切入点选得比较小但它把Flutter开发中几个最核心的环节都覆盖到了UI布局、状态管理、本地存储、日期处理、平台通道。一个模块走通后续做疫苗记录、洗澡记录、喂食记录基本就是复制这套方法论。后续扩展我目前计划做两个方向一是把提醒功能升级为支持自定义时间并且能按月重复的周期提醒目前是单次通知二是把本地数据通过云同步能力备份防止用户换手机后历史记录丢失。这两块都需要更深入的OpenHarmony平台能力接入但有了驱虫记录这个模块打底接下来就是顺着同一条路走的事。最后再多说一句如果你也在做类似项目建议把一个核心功能模块先打透再铺开做其他页面这种实战节奏虽然慢一点但每一步都踩得实后面返工的概率会小很多。