这几天在训练营里做到 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 --template=module --platforms=ohos生成一个 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.builder,crossAxisCount: 7,physics: NeverScrollableScrollPhysics(),让日期网格完全交给父级滚动,避免嵌套滚动冲突。构建 item 的时候,计算每个格子对应的 Date:
DateTime 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).day,Dart 会帮你算出上一个月的最后一天,彻底避免 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 Future<bool> checkPermission() async { final bool result = await _channel.invokeMethod('checkPermission'); return result; } static Future<void> requestPermission() async { await _channel.invokeMethod('requestPermission'); } static Future<void> 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 Future<void> 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 Engine,OpenHarmony embedder 把 MethodChannel 消息转发给 ArkTS 注册的监听器,监听器解析参数后调用系统通知接口。这个链路一旦打通,后面加重复提醒、取消提醒都只是参数变化而已。
4.3 提醒时间的数据模型与调度策略
提醒的数据模型我放在 memo 表里,和备忘本身存在一起,不单独建表。字段包括:
remind_at:提醒的具体时间,存毫秒时间戳。repeat_type:0 表示不重复,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.watch<MemoState>()之后,备忘数据源确实变了,但 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,不急着合并到备忘录主项目里。分开做的时候,每条链路都更清晰;等两边都跑通了,再合并,问题会少很多。这个经验值得记下来。