HarmonyOS 7 Notification Kit + WantAgent:任务进度通知同 ID 更新中的覆盖一致性、快速动作幂等与前台回收【鸿蒙心迹】
NotificationActionLab 模拟一个离线渲染任务:进度从 18% 更新到 72%,最后到 100%,通知里还带一个“重试”按钮。
第一版出现三个问题:每次进度都生成一张新通知;连续点两次“重试”真的执行两遍;进入前台以后 100% 的旧通知还留在通知中心。
所以本文只解决三件事:同 ID/tag 覆盖更新、快速动作幂等、前台消费后的精确回收。
本次数据:
- Session:
notify_update_20261001_18 - Notification ID:
32018 - Tag:
render_job - Progress:
100% - Update Count:
6 - Action Taps:
2 - Action Applied:
1 - Duplicate Actions:
1 - WantAgent:
retry_render - Foreground:
true - Cancelled:
true - State:
CLEARED - Last Update:
19:04:16

一、进度通知更新,先把“任务身份”固定下来
Notification Kit 的更新规则非常适合任务进度:同一 ID 和 tag 的新通知可以替换旧通知,updateOnly 还能明确表达“这里只允许更新已有通知,不要意外创建新卡片”。
所以我不再给每次 progress 生成新 ID,而是把渲染任务固定映射到:
id = 32018
tag = render_job
首次发布时创建通知,后续更新保持相同身份。
private async publishInitial(
progress: number
): Promise<void> {
const retryAgent =
await this.buildRetryWantAgent()
const request: notification.NotificationRequest = {
id: 32018,
label: 'render_job',
content: {
contentType:
notification.ContentType
.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: '正在生成预览图',
text: `进度 ${progress}%`
}
},
actionButtons: [{
title: '重试',
wantAgent: retryAgent
}]
}
await notificationManager.publish(request)
this.updateCount++
}
这一步只负责让第一张卡片存在。正式项目应维护独立的 notification ID 映射,避免不同通知类型之间冲突。
二、后续 progress 必须显式走 updateOnly
任务进入 72%、100% 时,我会继续使用相同 id 和 label,并打开 updateOnly。
private async updateProgress(
progress: number
): Promise<void> {
const request: notification.NotificationRequest = {
id: 32018,
label: 'render_job',
updateOnly: true,
content: {
contentType:
notification.ContentType
.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: '正在生成预览图',
text: `进度 ${progress}%`
}
}
}
await notificationManager.publish(request)
this.progress = progress
this.updateCount++
}
关键不是调用 publish 几次,而是每次都表达同一个通知身份。本次日志依次为 publish id=32018 progress=18、updateOnly progress=72、updateOnly progress=100,通知中心始终只有一张 render_job。updateOnly 还能避免用户手动清掉通知后,后台进度变化又意外创建新卡片。
三、WantAgent 只负责把动作带回来,幂等仍然是业务自己的责任
通知按钮可以绑定 WantAgent。用户点击以后,应用会在未来某个时间点收到对应 Want。
我给“重试”动作放了两个参数:action=retry_render 和一个 action sequence。真正执行之前,先检查这个序号有没有处理过。
private handledActionSeq:
Set<number> = new Set()
private handleRetryAction(
seq: number
): void {
this.actionTaps++
if (this.handledActionSeq.has(seq)) {
this.duplicateActions++
return
}
this.handledActionSeq.add(seq)
this.actionApplied++
this.retryRender()
}
本次 Action Taps=2,但 Action Applied=1、Duplicate Actions=1。WantAgent 只负责把动作送到 Ability,真正的幂等 token 仍由业务定义。重要操作应持久化 token 或交给服务端判重。
四、快速动作不要直接复用“按钮点击时间”当唯一键
我最初偷懒,用 Date.now() 当 seq。后来很快发现,这并不能表达“同一逻辑动作”。
如果同一 Want 被恢复链路再次带回来,时间戳会变化,去重会失效。更稳妥的是发布通知时生成一次 actionToken,后续只更新文案和 progress,不重新生成 token。
五、任务进入前台后,通知生命周期也要收口
第三个问题是页面进了前台,通知还挂着。
Notification Kit 提供按 ID 取消通知的能力。我的规则是:用户已经进入任务详情页,并且页面确认这张通知对应的任务已经被消费,就取消 ID 32018。
private async clearConsumedNotification():
Promise<void> {
if (!this.isForeground) {
return
}
await notificationManager.cancel(32018)
this.cancelled = true
this.state = 'CLEARED'
}
这里不用 cancelAll(),避免误伤其他有效通知。本次 Demo 的规则是“进入渲染详情页即认为这条通知已消费”。
六、为什么我把“发布、动作、回收”拆成三个对象
工程里 TaskNotificationPublisher 只负责发布、更新和 cancel;ActionDeduper 只负责 action token 幂等;EntryAbility 只负责把 Want 里的参数交给业务。
这样后面即使通知 UI 改了,也不会影响 retryRender 的幂等逻辑。
HiLog 固定输出:
publish id=32018 progress=18
updateOnly progress=72
updateOnly progress=100
retry tap seq=2 applied=1 duplicate=1
foreground cancel id=32018
State: PUBLISHED -> CLEARED

七、最终结果看三组数字就够了
运行页最终是:
- Notification ID:
32018 - Progress:
100% - Update Count:
6 - Action Taps:
2 - Action Applied:
1 - Duplicate Actions:
1 - WantAgent:
retry_render - Foreground:
true - Cancelled:
true - State:
CLEARED
第一组证明同 ID 更新最终到了 100%;第二组证明两个点击只执行一次;第三组证明页面进入前台以后,通知已经完成生命周期回收。

八、正式项目还要处理几个通知边界
正式项目还要处理通知授权、首次发布、动作幂等和“运行中/完成”是否共用 ID 这四个边界。
九、这次真正修掉的是通知和业务任务之间的身份漂移
最后状态链路是:
PUBLISHED → UPDATING → ACTION → FOREGROUND → CLEARED
通知 ID/tag 负责识别“这是同一张卡片”,WantAgent 负责把动作送回来,业务 action token 负责判断“这是不是同一次操作”,cancel 负责通知被消费后的回收。
把这几层分开以后,通知不会越发越多,连续点击不会重复执行,前台页面和通知栏最终能收敛到同一个任务结果。
更多推荐




所有评论(0)