# 日历提醒统一交互设计(iOS/Android) ## 1. 背景与问题 当前日历提醒模块存在以下问题: 1. iOS 通知动作("稍后提醒"/"取消")在横幅场景下可见性不稳定,用户误判为无按钮。 2. Android 与 iOS 的通知动作回调链路不一致,导致按钮点击在部分状态下无效。 3. 前台提醒体验依赖系统默认样式,Android 观感较弱,不符合产品视觉语言。 4. 提醒动作与弹窗交互代码存在历史分叉,维护成本高,且存在潜在无用旧代码残留。 ## 2. 目标与非目标 ### 2.1 目标 - 支持 App 关闭状态下的到点提醒(系统通知主触达)。 - 统一提醒动作语义: - `稍后提醒` = 延后 10 分钟。 - `取消` = 归档日历事件。 - 前台状态提供一套跨平台复用的应用内提醒面板,提升视觉质量。 - 无论动作来自系统通知还是应用内面板,都进入同一业务执行链路。 - 在新链路稳定后,清除无用旧代码与重复入口。 ### 2.2 非目标 - 不改变提醒策略(仍按当前 reminderMinutes + 重复提醒策略)。 - 不改动日历事件核心数据结构与后端协议。 - 不在本次引入新的提醒类型(例如自定义延后时长、多级动作)。 ## 3. 总体方案 采用“系统通知主触达 + 前台应用内面板增强”的混合方案: 1. **系统通知层(平台差异化)** - Android/iOS 继续使用 `flutter_local_notifications`。 - 平台分别补齐通知动作接收能力,确保前台/后台/终止态都可触发动作。 2. **动作执行层(跨平台统一)** - 以 `ReminderActionExecutor` 作为唯一动作入口。 - 内部动作 ID 固定为:`ReminderAction.snooze10m` 与 `ReminderAction.archive`。 - UI 文案中的“取消”仅为展示文案,内部统一映射到 `archive`。 3. **前台呈现层(跨平台复用)** - 新增应用内 `ReminderActionSheet`(共享组件,遵循设计 token)。 - 仅在应用前台触发,用于替代系统默认弹窗体验。 4. **展示策略(避免双提醒)** - 前台(App active):默认只展示 `ReminderActionSheet`,不展示系统通知横幅。 - 后台/终止态:只展示系统通知。 ## 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` 统计)。