Flutter 插件 device_calendar_plus 开源鸿蒙适配实战:从权限弹窗失灵到循环日程跑通

难度等级:⭐⭐⭐☆☆

前置知识:Flutter 联合插件(federated plugin)结构、ArkTS 基础、鸿蒙权限机制

本文对应适配仓库:https://atomgit.com/oh-flutter/device_calendar_plus(上游:bullet-to/device_calendar_plus 0.12.0)

文章目录

一、为什么要做这次适配

团队的项目需要在鸿蒙设备上读写系统日历------创建日程、查询某段时间的安排、处理每周重复的例会。社区里的 device_calendar_plus 是一个联合插件,Android 走 Kotlin、iOS 走 Swift,但没有任何鸿蒙实现,在鸿蒙设备上调用会直接抛 MissingPluginException。

选库前先在 AtomGit 的 oh-flutter 组织里查重,确认尚无该库的适配版本,于是立项自适配。本文记录这次适配的完整过程:架构怎么搭、权限弹窗为什么拉不起来、循环日程怎么展开、错误码为什么全部长一样,以及每一类问题最终怎么验证。

环境与版本(先交代清楚,后文结论都基于此):

项 版本
鸿蒙 SDK HarmonyOS 26.0.0(API 26),compatibleSdkVersion / targetSdkVersion 均为 26.0.0
Flutter 3.47.5-ohos-1.0.0(Flutter OH 版)
Dart 3.13.x(workspace 约束 sdk: ^3.12.0)
IDE DevEco Studio 26.0
上游基线 device_calendar_plus 0.12.0

二、适配架构:一个新包,两行注册

联合插件的适配原则是"不动上游、独立成包"。Android 和 iOS 实现一个字不改,所有鸿蒙代码收敛进新增的 device_calendar_plus_ohos 包:

text 复制代码
device_calendar_plus/
├── packages/
│   ├── device_calendar_plus/                  # 主包(只加 ohos 平台声明)
│   ├── device_calendar_plus_platform_interface/
│   ├── device_calendar_plus_android/          # 未改动
│   ├── device_calendar_plus_ios/              # 未改动
│   ├── device_calendar_plus_ohos/             # 新增:鸿蒙实现
│   │   ├── lib/                               # Dart 层,MethodChannel 转发
│   │   └── ohos/                              # ArkTS 原生层(HAR)
│   │       └── src/main/ets/pages/DeviceCalendarPlusOhosPlugin.ets
│   └── device_calendar_plus/example/ohos/     # 新增:鸿蒙示例工程

Dart 层实现平台接口,所有调用经 device_calendar_plus_ohos 方法通道转发;原生层用 @kit.CalendarKit 的 calendarManager 做日历/日程 CRUD,用 abilityAccessCtrl 申请权限。整个原生插件约 1376 行 ArkTS。

dart 复制代码
/// 鸿蒙实现包入口:注册为平台默认实例,主包无感知切换
class DeviceCalendarPlusOhos extends DeviceCalendarPlusPlatform {
  @visibleForTesting
  final methodChannel = const MethodChannel('device_calendar_plus_ohos');

  static void registerWith() {
    DeviceCalendarPlusPlatform.instance = DeviceCalendarPlusOhos();
  }
}

整条调用链路如下:

主包改动只有两处:pubspec.yaml 增加 ohos 平台声明,workspace 根加入新包参与统一依赖解析。业务侧引用时不发布到 pub.dev 的实现包,用 dependency_overrides 指到 Git 仓库:

yaml 复制代码
dependencies:
  device_calendar_plus:
    git:
      url: https://atomgit.com/oh-flutter/device_calendar_plus.git
      path: packages/device_calendar_plus
      ref: 0.12.0-ohos-1.0.0

dependency_overrides:
  device_calendar_plus_ohos:
    git:
      url: https://atomgit.com/oh-flutter/device_calendar_plus.git
      path: packages/device_calendar_plus_ohos
      ref: device_calendar_plus_ohos-0.12.0-ohos-1.0.0

两个日历权限都是 user_grant 类型,reason 和 usedScene 必须声明,缺一个安装期就过不了校验:

json5 复制代码
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_CALENDAR",
        "reason": "$string:read_calendar_reason",
        "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
      },
      {
        "name": "ohos.permission.WRITE_CALENDAR",
        "reason": "$string:write_calendar_reason",
        "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
      }
    ]
  }
}

三、踩坑实录:四个必须跨过去的坎

适配第一版跑通主流程后,真机验证阶段接连暴露问题。以下每个坑都来自实际提交(e6b1cf5、5a4cc8a 两个 fix 提交),附现象、根因、解决和验证方式。

1:权限弹窗永远不弹,requestPermissions 静默返回 denied

现象 :第一版在真机上点"Request Calendar Permissions",界面毫无反应,返回值直接是 denied。不是用户点了拒绝------是系统弹窗根本没拉起来。

根因 :插件初始化时拿到的是 ApplicationContext,而 requestPermissionsFromUser 要求 UIAbility Context 才能拉起系统弹窗。用 ApplicationContext 调用,弹窗不报错也不弹出,直接走失败分支。

解决 :插件实现 AbilityAware 接口,在 onAttachedToAbility 里持有 UIAbility Context;同时 CalendarManager 也用这个 Context 重建------只用 ApplicationContext 时还有个隐藏症状"能读日历、不能建账户"。

typescript 复制代码
// 弹窗与 CalendarManager 都依赖 UIAbility Context
if (!this.abilityContext) {
  result.success('denied');
  return;
}
const atManager = abilityAccessCtrl.createAtManager();
const permissions: Permissions[] = writeOnly
  ? [PERMISSION_WRITE_CALENDAR]
  : [PERMISSION_READ_CALENDAR, PERMISSION_WRITE_CALENDAR];
atManager.requestPermissionsFromUser(this.abilityContext, permissions)
  .then((res): void => {
    const allGranted = res.authResults.every(
      (r: number): boolean => r === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED);
    result.success(allGranted ? 'granted' : 'denied');
  });

验证 :真机重跑 demo,系统弹窗正常拉起,读写两个权限依次询问,授权后回读状态为 granted。

2:hasPermissions 永远区分不出"从未询问"

现象 :修复坑 1 后,hasPermissions 只能返回"已授权"和"已拒绝"两种状态。主包的 AutoPermissionMode 逻辑依赖 notDetermined(从未询问过)来自动发起申请,鸿蒙上这个分支永远不触发。

根因 :用 checkSelfPermission 做布尔判断,无法区分"用户明确拒绝"和"还没问过"。

解决 :改用 abilityAccessCtrl.getSelfPermissionStatus(API 20+),把权限状态三分类完整映射回平台接口契约。

验证 :卸载重装后首次调用返回 notDetermined,拒绝后返回 denied,与 Android/iOS 行为对齐。

3:createCalendar 真机失败,所有错误长得一模一样

这是排查时间最长的一个坑,也是改动收益最大的一个。

现象 :真机上创建日历偶发失败,demo 弹窗只有一句 CreateCalendar failed!。同一个失败文案背后,可能权限没给、参数非法、能力不支持、也可能是 CalendarKit 内部错误------完全无法定位。

根因有两层:

  1. 原生侧只回传了 BusinessError.message,丢弃了 err.code。201(权限拒绝)、401(参数错误)、801(能力不支持)、23900004(dataShare 内部错误)全部坍缩成同一句话;
  2. 参数解析写在 try 块之外,缺参时异常逃逸且不回包------Dart 侧的 Future 永远挂起。

解决 :错误统一按 code: message 格式回传;demo 弹窗同时展示 errorCode 和 details:

typescript 复制代码
/// BusinessError 统一渲染为 "code: message"
/// code 才能区分:201 权限 / 401 参数 / 801 能力 / 23900004 内部错误
private describeError(err: BusinessError): string {
  if (!err) return 'Unknown error';
  const message: string = err.message ? err.message : '';
  const code = err.code;
  if (typeof code !== 'number') {
    return message !== '' ? message : String(err);
  }
  return message !== '' ? `${code}: ${message}` : `Error ${code}`;
}

顺带修掉了另一个隐患:CalendarKit 对重复创建同名日历会直接拒绝,改成先按名称查找、复用已有日历的 get-or-create 语义,避免失败后留下不可见的重名账户。

验证:真机复现失败路径时,弹窗能给出具体错误码,可按码定位(权限类走设置页引导,参数类修调用方)。错误分文与修复前对比:

场景 修复前 修复后
权限被拒 CreateCalendar failed! 201: Permission denied(引导去设置页)
参数非法 CreateCalendar failed! 401: Parameter error(修调用方)
设备不支持 CreateCalendar failed! 801: Capability not supported

4:循环日程查询不展开,一个 RRULE 只返回一条

现象 :每周重复的例会,用 listEvents 查一周区间,只返回起点那一天的一条记录,后续发生全部丢失。

根因 :CalendarKit 的 queryEvents 返回的是日程主记录;循环日程的"每次发生"要用 queryEventInstances(API 18+)展开。同时 RRULE 的 UNTIL 字段双向解析格式不一致------原生生成的带 THHMMSSZ 后缀,主包 Dart 解析器按纯日期解析,来回一趟就解析失败。

解决 :listEvents 与 getEvent(带 timestamp 时)全部改走 queryEventInstances;实例 ID 按 eventId@instanceStartTime 编码,单发事件的实例 ID 等于事件 ID。UNTIL 统一为 YYYYMMDD[THHMMSSZ](UTC)格式,与主包解析器对齐。

验证 :创建 FREQ=WEEKLY、BYDAY=MO,WE 的日程,查询一周区间返回全部发生;单次修改某一 occurrence 后该次被 excludedDates 排除、生成独立日程,系列其余不变。

循环日程"本次及以后"的拆分是近似实现,逻辑如下:

附带修正:提醒的单位陷阱

文档与实现曾按 Alarm.alarmOffset(毫秒)映射提醒,实测核对后修正为 Event.reminderTime------CalendarKit 该字段单位是整分钟,传毫秒会导致提醒时间扩大 60000 倍。跨平台字段单位差异这类问题,靠读 API 文档容易漏,靠真机设一次提醒看触发时间才能抓到。

四、运行效果与功能验证

示例工程在真机上完整跑通:权限申请、日历创建(支持选色、同名 get-or-create)、日程创建(含提醒)、循环日程的单次/系列操作、系统表单跳转。

验证方式分三层:

  1. Dart 单元测试 :覆盖方法通道参数序列化、Patch 清除字段语义、Android 专属参数忽略与空写入短路;
  2. 真机手工验证:示例工程的 Manual Checks 页按平台区分文案,逐项核对权限层级、系统表单、设置页往返;
  3. 错误路径验证:构造权限拒绝、非法 calendarId、无日历设备等场景,核对错误码回传。

按 ID 定位日程的读写操作统一改用 EventFilter.filterById 串行查询,消除了对同一 MethodResult 重复回调的隐患,也避免遍历全量日程。

五、能力边界:适配后仍做不到的事

如实列出,供使用前评估:

上游能力 鸿蒙现状 原因
日历改名 不支持,明确返回 OPERATION_FAILED CalendarKit 无改名 API(改色可用)
事件的 url / availability 字段 不持久化 CalendarKit 事件模型无对应字段
参会人列表 最多读回 1 条 模型只有单个 attendee 字段
thisAndFollowing 拆分 近似实现 极端情况下新系列创建成功但截断失败,会残留一条重复
excludedDates 修改单次 必须精确到秒 需使用插件返回的实例 ID 时间戳

API 覆盖面上,showEventModal 依赖 API 26+ 的 openEventEditPage,实例展开依赖 API 18+,权限三态依赖 API 20+------整包因此要求 HarmonyOS 26.0.0(API 26)起。设备侧要求真机或带 CalendarKit 能力的模拟器。

六、接入总结与后续计划

回顾整个适配,收益最大的三个决策:

  1. 独立成包、不动上游------后续跟进上游版本只需 rebase,Android/iOS 用户零影响;
  2. 错误码优先透传 ------没有 code: message 这条改造,坑 3 类问题每次都要靠猜测定位;
  3. CHANGELOG 按坑记录------每次"现象/根因/解决/验证"四要素齐全,本文一半内容直接从中来。

后续计划:向社区反馈 CalendarKit 缺失的改名 API 与多参会人诉求;跟进上游新版本;thisAndFollowing 拆分失败时的自动清理策略。

适配仓库:https://atomgit.com/oh-flutter/device_calendar_plus(Issue 入口同仓库,欢迎反馈)。本文参与 AtomGit 开源鸿蒙跨平台技术征文(方向一:三方库适配),代码均已在真机验证可运行。

如果本文对你有帮助,欢迎点赞、收藏、转发!有任何问题或建议,请在评论区留言交流。

相关推荐
恋猫de小郭2 小时前
Flutter 开始支持 Jetpack Compose Platform View
android·前端·flutter
贾伟康2 小时前
【共创稿事节】HarmonyOS 7.0 BindSheet半模态浮层:从接入到生产验证
性能优化·实战·harmonyos·arkts·arkui
JosieBook2 小时前
【鸿蒙PC GUI适配】chiaki-ng 移植的工程实践:Qt6/QML 与 ArkUI 的混合窗口架构
华为·架构·harmonyos
Dream-Y.ocean12 小时前
鸿蒙PC平台宅匠 3D 室内设计适配实战:基于 Electron 壳方案的 Three.js 双渲染引擎与参数化户型编辑器开发
javascript·3d·harmonyos
行者-全栈开发12 小时前
【共创稿事节】HarmonyOS 应用上架全流程中的隐私合规:权限最小化、数据本地化与 AppAnalyzer 体检
harmonyos·鸿蒙·隐私合规·应用上架·agc·appanalyzer
SoaringHeart13 小时前
Flutter 查缺补漏:@JsonKey 属性全解,让模型层不再被后端字段牵着走
flutter
贾伟康13 小时前
HarmonyOS API 26 UKey PIN 授权门禁:状态查询、失败归因与资源闭环
华为·harmonyos
Dream-Y.ocean19 小时前
鸿蒙PC平台星图遥感适配实战:基于 Electron 壳方案的纯 JavaScript 遥感图像处理引擎开发
javascript·electron·harmonyos
七月稻草人1 天前
鸿蒙平板能当电脑第二块屏幕吗?ToDesk、向日葵镜像/副屏实测
华为·电脑·harmonyos