8.6 KiB
日历提醒统一交互设计(iOS/Android)
1. 背景与问题
当前日历提醒模块存在以下问题:
- iOS 通知动作("稍后提醒"/"取消")在横幅场景下可见性不稳定,用户误判为无按钮。
- Android 与 iOS 的通知动作回调链路不一致,导致按钮点击在部分状态下无效。
- 前台提醒体验依赖系统默认样式,Android 观感较弱,不符合产品视觉语言。
- 提醒动作与弹窗交互代码存在历史分叉,维护成本高,且存在潜在无用旧代码残留。
2. 目标与非目标
2.1 目标
- 支持 App 关闭状态下的到点提醒(系统通知主触达)。
- 统一提醒动作语义:
稍后提醒= 延后 10 分钟。取消= 归档日历事件。
- 前台状态提供一套跨平台复用的应用内提醒面板,提升视觉质量。
- 无论动作来自系统通知还是应用内面板,都进入同一业务执行链路。
- 在新链路稳定后,清除无用旧代码与重复入口。
2.2 非目标
- 不改变提醒策略(仍按当前 reminderMinutes + 重复提醒策略)。
- 不改动日历事件核心数据结构与后端协议。
- 不在本次引入新的提醒类型(例如自定义延后时长、多级动作)。
3. 总体方案
采用“系统通知主触达 + 前台应用内面板增强”的混合方案:
-
系统通知层(平台差异化)
- Android/iOS 继续使用
flutter_local_notifications。 - 平台分别补齐通知动作接收能力,确保前台/后台/终止态都可触发动作。
- Android/iOS 继续使用
-
动作执行层(跨平台统一)
- 以
ReminderActionExecutor作为唯一动作入口。 - 内部动作 ID 固定为:
ReminderAction.snooze10m与ReminderAction.archive。 - UI 文案中的“取消”仅为展示文案,内部统一映射到
archive。
- 以
-
前台呈现层(跨平台复用)
- 新增应用内
ReminderActionSheet(共享组件,遵循设计 token)。 - 仅在应用前台触发,用于替代系统默认弹窗体验。
- 新增应用内
-
展示策略(避免双提醒)
- 前台(App active):默认只展示
ReminderActionSheet,不展示系统通知横幅。 - 后台/终止态:只展示系统通知。
- 前台(App active):默认只展示
4. 关键设计决策
4.1 是否需要 iOS/Android 各写一套弹窗组件
不需要。应用内提醒组件采用一套 Flutter 共享实现。
需要分平台处理的是系统通知配置与回调桥接,不是应用内 UI 组件本身。
4.2 到点提醒是否继续使用“弹窗”
主流做法是系统通知,不是纯应用内弹窗。原因:
- App 关闭态仅系统通知可达。
- 锁屏、通知中心具备天然可达性与系统一致性。
- 可直接承载动作按钮(稍后提醒、取消并归档)。
前台场景再补应用内面板,兼顾体验与一致行为。
4.3 iOS 动作按钮显示问题
iOS 横幅通常不保证直接展示全部动作按钮,需展开通知查看动作。该行为属于系统 UI 规则。
此外 iOS 通知 category 存在缓存特性,category 变更后可能需要重装或升级 category id 才能稳定生效。
5. 模块与职责划分
5.1 保留并增强
-
apps/lib/core/notifications/local_notification_service.dart- 仅负责通知调度与平台动作回调桥接。
- 不负责前台 UI 展示。
- 补齐后台动作回调接入。
-
apps/lib/features/calendar/reminders/reminder_action_executor.dart- 作为唯一动作执行器。
- 保持归档 outbox 重试机制。
5.2 新增
-
apps/lib/features/calendar/reminders/ui/reminder_presentation_coordinator.dart- 感知 app 前后台状态。
- 前台触发应用内提醒面板。
- 作为前台展示唯一入口,禁止其他模块直接弹出提醒面板。
-
ReminderActionSheet(共享组件)- 展示事件摘要 + 两个动作按钮。
- 保证与系统通知动作语义一致。
5.3 平台接入补齐
-
Android:
- 在
apps/android/app/src/main/AndroidManifest.xml增加ActionBroadcastReceiver。 - 配置并接入
onDidReceiveBackgroundNotificationResponse。 - Android 13+ 先做
POST_NOTIFICATIONS授权检查;未授权时降级应用内提示并记录埋点。 - 后台回调函数必须为 top-level 且加
@pragma('vm:entry-point')。
- 在
-
iOS:
- 在
apps/ios/Runner/AppDelegate.swift配置 plugin registrant callback(用于后台 action isolate)。 - 后台回调函数必须为 top-level 且加
@pragma('vm:entry-point')。 UNNotificationCategory在应用启动早期完成注册(早于提醒调度)。- 为 category id 增加版本化策略(
calendar_reminder_v{n}),避免缓存导致动作更新不生效。
- 在
6. 动作流转(统一)
6.1 稍后提醒
触发源(系统通知按钮或应用内面板按钮)
-> 统一映射为 ReminderAction.snooze10m
-> ReminderActionExecutor._snoozeEvent
-> 重新计算下一次时间并 scheduleReminderAt
6.2 取消(归档)
触发源(系统通知按钮或应用内面板按钮)
-> 统一映射为 ReminderAction.archive
-> 取消本地提醒
-> 写 outbox 并调用归档接口
-> 成功标记 done,失败进入 retry/backoff
6.3 动作回传契约(前台/后台/终止态统一)
- 每次动作生成幂等键:
actionExecutionId = notificationId + actionId + fireTimeBucket。 - 执行前先查重(本地持久化幂等表);命中时直接 ACK,不重复执行业务副作用。
- 终止态动作进入 cold-start queue 回放,按接收时间顺序处理。
- 单条动作失败不阻塞后续动作;失败进入 retry/backoff 并可观测。
7. 旧代码收集与清理计划
7.1 旧代码清单建立
改造前先建立“提醒模块迁移清单”,按三类标记:
保留:仍由新架构使用。替换:保留接口,重写实现。删除:无引用、重复职责、历史临时逻辑。
迁移清单字段必须包含:文件路径、符号名、处理决策(保留/替换/删除)、责任人。
7.2 清理时机
- 新链路在 Android+iOS 均验证通过后,立即执行删除。
- 不做“先保留一版再说”的长期并存。
7.3 清理范围
- 无效弹窗触发入口。
- 不再使用的提醒动作映射分支。
- 重复回调注册与过时常量(旧 action id / 旧 category id)。
- 不再有保护价值的旧测试与旧 fixture。
7.4 清理验收
- 以“旧标识 0 引用”为验收标准,至少覆盖:旧 action id、旧 category id、旧入口函数名。
- 输出固定 grep 关键字清单并逐条验收。
- 提醒链路测试通过。
- 删除项对应测试通过,且无悬挂 fixture/snapshot 引用。
- 手工回归覆盖前台/后台/终止态三种状态。
8. 测试与验证
8.1 自动化
- 新增/更新 reminders 相关单测:
- 动作映射正确性(notification/app sheet -> executor)。
archive的 outbox 行为与重试退避逻辑。snooze10m在边界时间的调度行为。- 同一
actionExecutionId重复投递仅执行一次(幂等)。 - 终止态 cold-start queue 回放不丢失且顺序一致。
- 前台面板与系统通知并发触发时仅产生一次业务副作用。
8.2 手工验证矩阵
-
Android:
- 前台:面板按钮可用。
- 后台:通知动作可用。
- 杀进程:通知动作可用。
-
iOS:
- 前台:面板按钮可用。
- 后台/锁屏:通知展开后动作可用。
- 安装升级后 category 动作可用。
9. 风险与缓解
-
风险:iOS category 缓存导致动作更新不生效。
- 缓解:category id 版本化 + 明确重装验证步骤。
-
风险:后台动作 isolate 未正确注册导致点击丢失。
- 缓解:AppDelegate/Manifest 严格按插件要求配置,并做终止态回归。
-
风险:前台面板与系统通知并发触发造成重复操作。
- 缓解:PresentationCoordinator 增加去重窗口与事件级幂等保护。
-
风险:通知权限未授权导致后台提醒不可达。
- 缓解:启动期权限检查 + 降级提示 + 埋点追踪。
10. 完成定义(DoD)
- App 关闭状态下,系统通知可触达并可执行两个动作。
取消在业务上严格等价于归档。- 前台统一提醒面板上线,Android 样式符合项目视觉语言。
- 动作执行链路唯一,平台仅保留桥接差异。
- 历史无用代码完成清理,且通过验证。
- 手工矩阵 6/6 场景通过(Android 前台/后台/杀进程 + iOS 前台/后台锁屏/升级后)。
- 动作日志可追踪,重复执行率=0(基于
actionExecutionId统计)。