
这几天在训练营里做到 Day 18-19正好把备忘录这个项目里最麻烦、也最出彩的两个功能啃下来了提醒调度和日历视图。如果你正在用 Flutter 开发 OpenHarmony 应用或者准备把自己的跨平台 App 跑到 OpenHarmony 设备上那这篇内容应该能帮你少踩不少坑。这个训练营不是单纯讲 UI 怎么写而是把 Flutter 的跨平台能力和 OpenHarmony 的底层能力真正接起来尤其是日历这种需要大量日期计算的界面以及提醒这种既要后台调度、又要触发系统通知的功能每一个拿出来都够折腾半天的。我先把这两天的成果放到一个具体场景里说清楚你的备忘录 App 里每条备忘可以设置一个提醒时间到时候系统会弹通知同时主界面有一个日历日历上能直接看到哪一天有备忘点某一天能查看当天的所有待办事项。听起来很常规对吧但真正在 OpenHarmony 上跑通这一套流程涉及的东西比想象中多日期处理、状态管理、本地存储、通知权限、插件适配、ArkTS 侧的双端通信……接下来我按实际的开发顺序把这些内容拆开讲尽量把我实测的细节和踩过的坑都写出来。1. 项目整体设计与思路拆解1.1 为什么在 OpenHarmony 上做备忘录要用 Flutter先说结论用 Flutter 做 OpenHarmony 应用核心目标只有一个复用代码。你的 UI 层、业务逻辑、数据模型甚至很多 Dart 插件理论上都可以从 Android/iOS 端平移过来不用为 OpenHarmony 单独维护一套原生实现。这在一款工具类应用上特别划算备忘录这种功能相对标准化的 App完全可以把精力放在功能本身而不是平台适配。这个训练营的实际做法是使用 OpenHarmony 适配过的 Flutter SDK通过构建 Flutter Engine 和 Dart 代码把 Flutter 模块作为一个完整的 UI 界面嵌入到 OpenHarmony 的工程里。也就是说项目的宿主工程是 OpenHarmony 的工程用 DevEco Studio 构建Flutter 模块负责渲染全部业务页面两边通过平台通道来调用 OpenHarmony 的系统级能力。这套架构和 Flutter 在 Android 上的 embedder 思路基本一致对熟悉 Flutter 的开发者来说上手成本确实低。1.2 备忘录提醒与日历视图的功能拆解我习惯在写代码之前先画功能清单避免开发中途被细节带偏。这次拆下来大概是四块数据层备忘录的增删改查提醒时间字段备忘和日期的对应关系。日历视图层月视图展示、切换月份、选中日期、标记有备忘的日期。提醒调度层设置提醒、取消提醒、重复提醒规则、触发后的回调。通知展示层系统通知的权限申请、通知栏展示、点击通知跳回详情页。这四个模块之间的依赖关系也理清楚了数据层是地基日历视图和提醒调度都要读同一份备忘数据提醒调度依赖系统通知所以权限申请要提前做日历视图纯粹是 UI 和日期计算和业务解耦可以独立开发。这样拆完以后我和队友一人负责日历视图一人负责提醒调度互相只需要约定好数据模型并行开发压力小很多。1.3 技术选型状态管理、本地存储与通知方案状态管理我直接用了 Provider不是因为它有多高级而是项目规模决定了越简单越好。CalendarState、MemoListState 两个 ChangeNotifier 就够了日历选中的日期变化、备忘列表的更新都通过 notifyListeners 驱动界面刷新逻辑清晰调试也方便。如果你用 riverpod 或者 bloc 也完全没问题关键是别在备忘录这种小项目里引入过重的工作流。本地存储选了 sqflite。因为 OpenHarmony 上已经有适配过的 sqflite 插件而且备忘数据的查询条件经常是“按日期范围查”“找出未来 7 天要提醒的备忘”用 SQL 处理比手写 JSON 文件靠谱得多。提醒的重复规则比如每天、每周、每月也用字段存不做复杂的 cron 解析。通知方案是这次最需要谨慎的地方。Flutter 生态最常用的 flutter_local_notifications 在 OpenHarmony 上的实现成熟度还不高我没有直接用它来做核心功能而是走 MethodChannel 去调 OpenHarmony 原生的通知接口。这样虽然多写了一层桥接代码但通知能力是可控的不会因为插件适配问题导致整个功能不可用。后面我会把双端代码都贴出来。2. 环境准备与工程适配2.1 Flutter SDK 与 OpenHarmony SDK 的版本匹配这个环节最折磨人因为版本不匹配会让编译报出各种奇怪错误。我的环境是 Flutter 3.22 的分支OpenHarmony SDK 4.1 左右的版本OpenHarmony 适配版的 Flutter SDK 通过 git 拉取后切到对应的 release 分支然后在 flutter 命令行工具里指定使用这个 SDK。这里有一个关键点OpenHarmony 的 Flutter 适配版和官方 Flutter 的版本号不是严格对齐的必须看适配版的 release note。比如官方 Flutter 3.22 对应的适配版可能是 3.22.x-ohos不要只根据主版本号判断。版本不匹配的典型表现是编译时报framework not found、CMake 配置失败或者运行后页面白屏。遇到这种问题第一步不要翻代码先回退版本。2.2 创建 Flutter 工程并接入 OpenHarmony 工程这个训练营用的是“已有 OpenHarmony 工程新增 Flutter 模块”的方案。具体步骤我理一下在 OpenHarmony 工程里用flutter create --templatemodule --platformsohos生成一个 Flutter 模块。在 OpenHarmony 侧的 Entry Module 里依赖这个 Flutter 模块。修改 Flutter 模块的 build.gradle 和 ohos 工程里的 module.json5注册 Flutter 的 Ability。最后用 DevEco Studio 构建整个工程生成 hap 包安装到设备。这个流程第一次跑会有点难因为 Flutter 模块的 Gradle 配置和原生工程之间需要来回校准。我的经验是不要手动改配置而是用 OpenHarmony 适配版 Flutter 提供的模板命令自动生成生成完再检查差异。手动改容易漏资源路径。2.3 设备调试与日志查看调试这块OpenHarmony 设备主要通过 hdc 工具连接类似于 Android 的 adb。连接后可以用hdc shell查看设备状态用hdc file send安装 hap 包。日志查看用hdc hilog可以按进程、按标签过滤比如只看 Flutter 相关的输出hdc hilog | grep -i flutter如果 Flutter 侧 Dart 代码有报错hilog 里能看到相关信息如果是 ArkTS 侧或者 native 崩溃就要看原生日志。我一般开两个终端一个跑hdc hilog盯全局日志一个用flutter run的调试模式看 Dart 侧输出两边对照定位问题。需要注意OpenHarmony 适配版 Flutter 的flutter run支持情况和 Android 不完全一样有时候不能直接热重载只能用 DevEco Studio 重新构建开发节奏比原生 Flutter 要慢一些。2.4 权限与依赖配置备忘录提醒要用系统通知必须先在 module.json5 里声明通知权限。OpenHarmony 的通知权限是通过ohos.permission.NOTIFICATION_CONTROLLER之类的高级别权限来控制的普通应用需要引导用户在系统设置里开启通知开关代码层面不能无条件直接弹通知。另外如果要支持精确时间的提醒比如“每天上午 9:30 提醒我喝水”还需要确认设备是否允许应用在后台触发通知。OpenHarmony 目前对后台任务的能力限制比较严格普通应用在退到后台后难以保证按时触发。这个我在提醒调度部分详细说是个大坑。依赖配置方面我的 pubspec.yaml 核心依赖如下dependencies: flutter: sdk: flutter provider: ^6.1.2 sqflite: ^2.3.3 path: ^1.9.0 intl: ^0.19.0 dev_dependencies: flutter_test: sdk: flutter没有额外引入复杂的包能少依赖一个就少依赖一个因为每次引入新插件都要确认它是否支持 OpenHarmony这个排查成本很高。3. 日历视图开发核心实现3.1 日历视图的需求不再只是“显示日期”日历界面做成什么样直接决定用户对备忘录的第一印象。我的设计是这样的默认显示月视图顶部有左右切换月份的按钮中间显示当前年月下面是一个 7 列的表头周一作为一周的第一天日期格子里如果某天有备忘就在日期下方显示一个小圆点标记选中某天后点击下方列表按钮可以查看当天全部备忘。这个需求听起来简单但实际操作细节很多。最典型的一个问题一个月可能占 4 到 6 行如果强行固定 4 行某些月份就会挤到一起如果固定 6 行视觉上又要保证格子高度自适应。所以我最后选择了固定 6 行网格不管当月实际有多少天都渲染出完整的 6x7 格子多余格子留空这样整个视图高度稳定切换月份时不会跳动用户体感会好很多。3.2 用 GridView 搭建日历网格日历主体我用 GridView.buildercrossAxisCount: 7physics: NeverScrollableScrollPhysics()让日期网格完全交给父级滚动避免嵌套滚动冲突。构建 item 的时候计算每个格子对应的 DateDateTime getDateForIndex(int index) { final firstDay DateTime(now.year, now.month, 1); final offset firstDay.weekday - 1; // 周一作为第一天 return DateTime(now.year, now.month, 1 index - offset); }这段代码是日历的核心虽然只有三行但注释得说清楚DateTime.weekday从周一到周日分别是 1 到 7如果想周一开始布局偏移量就是weekday - 1如果习惯周日开始偏移量改成weekday % 7。渲染格子时要判断这个日期是否属于当前展示的月份用date.month ! now.month判断非当月日期直接压低透明度显示然后判断是否为今天、是否为选中日期、是否有备忘标记普通 cell 就是这些状态叠加。状态多但逻辑很直白只要把判断顺序固定住就能避免样式冲突。3.3 日期状态管理与跨月切换日历的状态管理我用了一个CalendarState里面维护三个值当前展示的年月viewYear、viewMonth当前选中日期selectedDate。切换月份时只改 viewYear 和 viewMonth然后重新生成日期列表触发 UI 刷新。选中日期后把 selectedDate 更新同时通知下方的备忘列表刷新。这里有个容易被忽略的点当你在某个月选中了一个日期然后切到别的月份再切回来选中状态应该保留还是重置我的选择是保留因为用户可能是在对比不同日期的安排。如果每次切换都重置选中日期体验会很割裂。所以选中日期和当前展示月份是两个独立状态切月不重置选中日期只有用户手动点击格子才更新选中日期。3.4 自定义日历控件的细节处理日历控件我坚持自己写没有用第三方日历库。原因一是 flutter 生态里的 calendar 控件大多没有适配 OpenHarmony二是备忘录用的日历功能不需要农历、假期、日程拖拽这些复杂特性自己写可控性最强。自己写的过程里要处理几个细节第一是算法。闰年判断用 Dart 内置的DateTime.utc(year, 2, 29)捕获异常的方式来判断或者直接实现“四年一闰百年不闰四百年再闰”的规则我直接写了一个函数bool isLeapYear(int year) { return (year % 4 0 year % 100 ! 0) || (year % 400 0); }第二是每个月的天数。很多新手喜欢写 if-else 判断月 2 月 29 天等但更稳妥的做法是直接用DateTime(year, month 1, 0).dayDart 会帮你算出上一个月的最后一天彻底避免 28 天还是 29 天的判断错误。第三是表头中文字显示。我用intl包格式化月份名称比如DateFormat(yyyy年M月).format(viewDate)周表头直接用固定列表周一、周二……周日和国际习惯保持一致。3.5 日历点击与备忘列表的联动这个联动是整个备忘录功能的主干逻辑。点击日期后在 CalendarState 里更新 selectedDate然后触发notifyListeners。下方备忘列表组件监听到 selectedDate 变化后去数据库查询这一天的备忘记录并刷新列表。查询逻辑也很简单我用的 SQL 如下SELECT * FROM memos WHERE date(remind_at) date(?) ORDER BY remind_at ASC;其中?是选中的日期字符串。这里有个时间精度问题remind_at 如果存的是完整时间戳直接用等号对比日期会不匹配比如2025-03-18 09:30:00和2025-03-18永远不相等。所以查询时要用date()函数格式化日期后再比这一点很容易被忽略测出来结果为空时先检查它。4. 备忘录提醒功能的实现4.1 本地通知方案插件还是自研通道提醒功能逃不开系统通知而这一步在 OpenHarmony 上比 Android 麻烦得多。原因很简单OpenHarmony 的通知接口有自己的设计flutter_local_notifications 这个跨平台插件并没有完全适配好很多版本在 OpenHarmony 上要么编译不过要么运行时没有任何反应。我的选择是把通知能力通过 MethodChannel 在 Flutter 和 ArkTS 之间做一层轻量封装。Dart 侧只负责传参数、收结果ArkTS 侧负责调 OpenHarmony 的 NotificationKit 真正发通知。这样既绕开了插件适配问题也保证了功能完全在自己掌控中。4.2 MethodChannel 双端通信实现先写 Dart 侧我定义了一个系统通知服务class NotificationService { static const _channel MethodChannel(com.example.memo/notification); static Futurebool checkPermission() async { final bool result await _channel.invokeMethod(checkPermission); return result; } static Futurevoid requestPermission() async { await _channel.invokeMethod(requestPermission); } static Futurevoid scheduleNotification({ required int id, required String title, required String body, required DateTime remindAt, }) async { await _channel.invokeMethod(scheduleNotification, { id: id, title: title, body: body, timestamp: remindAt.millisecondsSinceEpoch, }); } static Futurevoid cancelNotification(int id) async { await _channel.invokeMethod(cancelNotification, {id: id}); } }ArkTS 侧在我的 EntryAbility 或者一个专门的 Service 里注册这个方法通道并实现对应的通知逻辑import notificationManager from ohos.notificationManager; import { BusinessError } from ohos.base; function registerNotificationChannel(engine: any) { engine.on(scheduleNotification, (event: any) { const { id, title, body, timestamp } event.arguments; const request { id: id, content: { title: title, body: body, notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION, }, deliveryTime: timestamp, trigger: { time: timestamp, repeatType: notificationManager.RepeatType.NOT_SUPPORT, }, }; notificationManager.publish(request).catch((err: BusinessError) { console.error(publish failed: ${err.message}); }); }); }ArkTS 侧这个代码我做了简化实际工程里还要处理回调和错误分支。重点是明白这个链路Dart 发起调用到 Flutter EngineOpenHarmony embedder 把 MethodChannel 消息转发给 ArkTS 注册的监听器监听器解析参数后调用系统通知接口。这个链路一旦打通后面加重复提醒、取消提醒都只是参数变化而已。4.3 提醒时间的数据模型与调度策略提醒的数据模型我放在 memo 表里和备忘本身存在一起不单独建表。字段包括remind_at提醒的具体时间存毫秒时间戳。repeat_type0 表示不重复1 表示每天2 表示每周3 表示每月。调度策略上这又回到了 OpenHarmony 后台限制的问题。如果你的备忘录 App 只是用户在前台时被打开过然后用户退到后台那靠 Flutter 侧 Dart 代码里的 Timer 来触发提醒是不可靠的因为应用随时可能被系统挂起或杀死。我试过用 Timer 写在应用生命周期内提醒结果 App 退到后台五分钟后再回来通知一个都没弹后来才明白这是系统后台能力限制导致的。那怎么保证提醒按时触发更靠谱的做法是使用系统级的闹钟 API 或者后台任务能力把提醒时间注册到系统侧。OpenHarmony 提供了 alarm 接口应用可以设置一次性或重复闹钟到点后系统会拉起一个接收器应用可以在接收器回调里发通知。这一部分在我的项目里做了简化Day 18 我只实现了“App 在前台时利用 Timer 通知服务弹提醒”的演示方案真正全后台可用的方案留到训练营后面的进阶课程里处理。这个限制必须在需求阶段就和产品对齐不然用户会投诉“提醒不响”。4.4 通知权限的申请与检查权限在 OpenHarmony 上有两层第一层是应用需要在 module.json5 里声明通知权限第二层是用户必须在系统设置里打开通知开关。代码层面无法强制打开只能引导。我在 Flutter 侧做了一个“检查权限 - 未授权则跳转到系统设置”的引导逻辑。跳转方式同样走 MethodChannel在 ArkTS 侧调用wantManager打开应用详情页。这段逻辑很影响体验不能省略因为用户如果没开通知权限你前面做的调度做得再好也是白费。如果通知一直不弹先自查三步权限声明和用户开关是否都打开。deliveryTime时间戳是否正确我遇到过时区引起的偏差建议统一用 UTC 毫秒传参。设备是否处于勿扰模式或者通知类别是否被系统拦截。4.5 提醒触发后的用户行为处理通知发出来不代表功能完成用户点通知后要能跳转到对应的备忘详情页。实现方式是在通知的 content 里塞一个wantAgent信息用户点击通知后系统拉起应用并携带一个路由参数。Flutter 侧怎么接收这个参数呢我在 Dart 里监听应用启动的 MethodChannel从原生直传一个启动参数里面带上 memoId_platformChannel.setMethodCallHandler((call) async { if (call.method onLaunch) { final memoId call.arguments[memoId] as int?; if (memoId ! null) { router.push(/memoDetail, arguments: memoId); } } });这个功能容易忽略但它是提醒闭环的重要一环。用户都点开通知了如果只进入 App 首页还得自己再找那条备忘那就很失败。5. 常见问题与排查技巧实录5.1 Flutter 插件在 OpenHarmony 上的兼容性问题做 OpenHarmony 适配最让人头疼的就是插件生态。flutter_local_notifications、shared_preferences 这些插件在官方 Flutter 生态里很成熟但 OpenHarmony 适配进度参差不齐有的插件根本没有实现有的实现了但接口行为不一致。我的排查套路是新引入一个插件前先到 OpenHarmony 的 Flutter 插件仓库查一下有没有对应的 ohos adapter。没有的话再确认这个插件是否是纯 Dart 实现如果它依赖了平台通道那就必须自己写原生适配。这个工作最好不要等到项目快上线再补前期做技术选型的时候就该排掉这些雷。有一类插件虽然官方没适配但可以通过覆写 Dart 端的方法通道来救一下。例如如果你非要用某个插件可以在运行时把 MethodChannel 的 handler 截胡自己实现 OpenHarmony 端逻辑。这种做法有点 hack但当时能解燃眉之急。5.2 日历表头对齐和周末样式问题日历对齐问题几乎每个人都会遇到。我自己第一次做的时候计算偏移用的是firstDay.weekday但这个值在不同的系统区域设置下可能不同Dart 的 weekday 是不会变的但如果你用了DateFormat本地化可能会影响星期一的判断这个要格外小心。周末颜色我做了特殊处理周六周日使用较淡的底色但为了让整体视觉保持统一非当月日期、选中日期、今天的颜色优先级要这样排选中日期 今天 周末 普通日期。颜色优先级不对会出现“周六被选中的时候看不清”这种问题。另外如果你在日历里做了“有备忘标记”的小圆点要记得这个小圆点也算原子组件的一部分需要给它的父容器足够空间否则和日期数字重叠。我在实践中直接把日期数字和圆点放到了一个 Column 里用固定高度 12 的圆点区域来隔离视觉。5.3 通知不触发时的排查思路这个坑我在开发期间反复踩。一次是通知权限没开通知直接静默失败控制台也不报错一次是 deliveryTime 传的是本地时间戳但系统按 UTC 解析导致通知提前或推迟了几个小时还有一次是重复提醒的 repeatType 传错了枚举被系统拒收。我的建议是写一个“测试通知立即触发”的按钮调试时点一下如果能正常弹出来就说明通道和权限没问题问题出在调度逻辑如果连立即触发都不弹就先查 ArkTS 侧日志。把问题一分为二排查效率会高很多。5.4 状态更新不刷新 UI 的情况用 Provider 最常犯的错误是在 setState 里改了数据但实际修改的并不是同一个对象。我在日历联动时遇到过一次备忘列表组件使用context.watchMemoState()之后备忘数据源确实变了但 UI 没变查了很久才发现是查询数据库返回的是一个新 List 实例但 Provider 内部比较对象引用时认为没有变化。解决办法是在 memo 数据变更后手动调用notifyListeners()或者确保 replace 数据时用List.unmodifiable包装新列表让 Provider 一定能感知到变化。5.5 日历滑动卡顿的优化日历页面如果包含大量 AnimatedContainer 和动画在低端设备上可能会出现掉帧。我的优化方法是把日期格子做成 const 构造的组件避免每次 rebuild 都重新创建标记点用普通 Container 而不是动画月份切换时只更新必要的文本和日期集合不重建整个 GridView。还有一个优化点是禁用日历网格的滚动日历的滑动交给外层列表。嵌套滚动一旦处理不好手势冲突和 GPU 过度绘制会直接拖垮帧率。6. 实操心得与后续扩展6.1 复盘这次训练营的踩坑方向如果把这次开发的经验浓缩成三条第一是平台能力要先摸清楚再做方案尤其是后台调度和通知权限这种受系统限制的功能越早知道边界越好。第二是能少用插件就少用插件跨平台方案最怕的其实是“看似跨平台实则处处要适配”。第三是日期处理千万别手写所有逻辑Dart 内置的 DateTime 已经解决了 90% 的问题剩下的用 intl 包补齐格式化就好。6.2 后续还能怎么扩展备忘录提醒和日历视图做完以后这个项目还有很多可以扩展的方向。最实用的是把重复提醒的 repeatType 支持完整不光是每天、每周还支持“每月的最后一个工作日”这种规则。这需要你在提醒调度层做一个规则解析器把自然语言转换成具体的触发时间。另一个方向是桌面小组件在 OpenHarmony 上把某一天的备忘直接展示在桌面上。Flutter 的 UI 无法直接渲染到系统桌面小组件上这里需要借助 ArkTS 写一个原生卡片然后通过数据共享把备忘内容传过去。这个工作量和日历实现差不多但体验提升很大。6.3 给同样在折腾跨平台开发的你一点提醒如果你也是第一次在 OpenHarmony 上跑 Flutter我建议先别急着把完整业务迁移过来而是先从一个极小的功能走通全链路Flutter 模块创建、原生工程集成、MethodChannel 双向通信、系统通知弹出。这四个点跑通后面的业务开发就只是填代码而已。这个链路里的每一个环节都有各自的版本兼容问题早踩早安心。如果让我重新做一遍这个训练营的任务我会先把日历和提醒拆成两个独立 demo不急着合并到备忘录主项目里。分开做的时候每条链路都更清晰等两边都跑通了再合并问题会少很多。这个经验值得记下来。