Files
social-app/docs/superpowers/specs/2026-03-19-calendar-reminder-unified-interaction-design.md
T

8.6 KiB
Raw Blame History

日历提醒统一交互设计(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.snooze10mReminderAction.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 统计)。