【HarmonyOS 7新能力|034】闪控窗工程封装:把接入逻辑放进可维护的分层结构
【HarmonyOS 7新能力|034】闪控窗工程封装:把接入逻辑放进可维护的分层结构

闪控窗适合承载用户需要持续关注、又不值得长期占据完整页面的实时状态,例如计时、传输、导航或设备协同进度。真正困难的部分不是“显示一个浮层”,而是业务事件、前后台切换、重复更新、异常恢复和用户操作同时发生时,界面仍然只呈现一个可信状态。本文不罗列某个版本的接口签名,而是从工程封装入手,建立可替换、可测试的接入结构。
说明:文中的
FlashWindowAdapter、FlashBallAdapter等是教学用抽象,不代表系统 SDK 的真实类名。实际能力范围、权限、设备支持与 API 签名,请以当前 HarmonyOS SDK 和华为官方文档为准。
1. 先把问题定义成“持续状态”,而不是一个弹窗
普通弹窗通常围绕一次确认展开,创建、交互、关闭即可。闪控窗面对的却是一段有生命周期的业务状态:任务可能开始、暂停、恢复、完成,也可能由于网络、进程或设备变化而中断。若页面直接调用平台接口,每个页面都会各自拼装标题、进度和操作,最终出现同一任务在不同入口显示不一致的问题。
更稳妥的做法是先定义领域状态。它不关心当前呈现为窗还是球,只描述用户真正需要知道的事实。展示形式是策略的结果,而不是业务数据的一部分。
export type TaskPhase = 'idle' | 'running' | 'paused' | 'done' | 'failed'
export interface LiveTaskState {
taskId: string
title: string
phase: TaskPhase
progress?: number
updatedAt: number
recoverable: boolean
}
这里的 taskId 是幂等更新的关键,updatedAt 用于拒绝过期事件,recoverable 决定异常后是否保留恢复入口。模型越克制,后续适配不同形态越容易。
2. 四层结构分别解决什么

建议把接入拆成页面层、领域服务层、平台适配层和状态仓库。页面层只订阅状态并接收操作;领域服务层负责事件归一化与规则判断;平台适配层隔离具体 SDK;状态仓库维护单一事实来源,并承担必要的恢复。
依赖方向必须稳定:页面依赖领域接口,领域服务依赖抽象端口,平台适配器实现端口,仓库负责数据。这样 SDK 升级时主要改适配层,业务规则变化时主要改领域服务,页面不会被平台细节污染。
export interface FlashSurfacePort {
showWindow(view: FlashViewModel): Promise<void>
switchToBall(view: FlashViewModel): Promise<void>
update(view: FlashViewModel): Promise<void>
dismiss(taskId: string): Promise<void>
}
端口只表达项目需要的最小能力。不要为了“完整封装”把 SDK 的每个参数原样透传,否则只是换了文件位置,并没有形成边界。
3. 用统一事件入口消除多来源竞争
状态可能来自用户点击、后台任务回调、系统生命周期和定时检查。如果这些来源直接更新 UI,先到后到会决定最终画面,偶发问题很难复现。领域服务应提供唯一入口,把外部事件转换成统一事件。
export type LiveEvent =
| { kind: 'START'; taskId: string; title: string; at: number }
| { kind: 'PROGRESS'; taskId: string; value: number; at: number }
| { kind: 'PAUSE'; taskId: string; at: number }
| { kind: 'COMPLETE'; taskId: string; at: number }
| { kind: 'FAIL'; taskId: string; recoverable: boolean; at: number }
事件进入后依次完成校验、去重、归并和持久化,再生成展示模型。这样无论事件来自哪里,规则都只有一份。对于同一 taskId,早于当前 updatedAt 的事件直接丢弃,避免迟到回调把“已完成”覆盖回“进行中”。
4. 把窗与球的选择写成可读策略
闪控窗和闪控球不是两个互不相关的功能,而是同一状态在不同注意力级别下的表现。前台且刚发生关键变化时,可以展示信息更完整的窗;应用进入后台、状态稳定或空间受限时,切换成更轻量的球;完成或不可恢复失败后,按产品规则收尾。
export type SurfaceMode = 'hidden' | 'window' | 'ball'
export function decideMode(
state: LiveTaskState,
appInForeground: boolean,
detailRequested: boolean
): SurfaceMode {
if (state.phase === 'idle') return 'hidden'
if (state.phase === 'done' && !detailRequested) return 'hidden'
if (appInForeground && detailRequested) return 'window'
return 'ball'
}
策略函数保持纯函数,输入相同就得到相同输出,单元测试不需要真实设备。产品调整规则时,也能明确知道修改会影响哪些分支。
5. 状态流转要有闭环

完整链路是:业务事件进入领域服务,生成标准状态,展示策略决定形态,适配器执行显示或更新,执行结果再回到状态记录。最后一步经常被忽略。如果平台调用失败却仍把本地标记为“已展示”,后续更新会走错误分支。
可以给展示结果增加一个轻量快照:期望模式、已确认模式、版本号和最近错误。下一次调度时先比较期望与确认状态,而不是盲目重复创建。
export interface SurfaceSnapshot {
taskId: string
desired: SurfaceMode
confirmed: SurfaceMode
revision: number
lastError?: string
}
这种闭环还能支持启动恢复:进程重新进入后,根据业务状态重新计算 desired,再与可确认的系统状态对齐。
6. 幂等更新比频繁刷新更重要
实时进度可能高频变化,但用户并不需要每个细粒度事件都触发平台更新。频繁调用既浪费资源,也可能造成视觉跳动。建议同时使用版本幂等、内容摘要和最小更新间隔。
function shouldPush(prev: FlashViewModel, next: FlashViewModel): boolean {
if (prev.phase !== next.phase) return true
if (prev.primaryAction !== next.primaryAction) return true
if (Math.floor(prev.progress / 5) !== Math.floor(next.progress / 5)) return true
return next.updatedAt - prev.updatedAt >= 3000
}
示例中的阈值只是演示,应根据业务及时性和官方约束验证后配置。完成、失败、暂停等语义变化应立即更新;连续进度则可合并。不要仅靠防抖,因为防抖可能让持续事件永远推迟最后一次更新。
7. 生命周期切换不要散落在页面
页面退出不等于任务结束,应用退到后台也不等于应该销毁状态。如果在每个页面的生命周期回调里直接关闭或创建闪控窗,就会出现返回页面后重复实例、后台状态丢失等问题。
应由产品级协调器接收应用前后台信号,再触发一次策略重算。页面销毁只取消自己的订阅,不改变任务本身。
export class LiveSurfaceCoordinator {
constructor(
private readonly store: LiveStateStore,
private readonly surface: FlashSurfacePort
) {}
async onForegroundChanged(isForeground: boolean): Promise<void> {
const current = await this.store.getActive()
if (!current) return
await this.reconcile(current, isForeground)
}
}
协调器应由与业务任务相匹配的生命周期持有,避免把短生命周期页面对象保存在全局上下文中。
8. 用户操作必须回到业务命令
闪控球上的暂停、继续或打开详情,不应直接修改显示文字。正确路径是把操作转成业务命令,业务执行成功后产生新事件,再由状态流驱动界面。这能保证主页面、通知区域和闪控窗看到同一个结果。
export interface LiveCommandHandler {
pause(taskId: string): Promise<void>
resume(taskId: string): Promise<void>
openDetail(taskId: string): Promise<void>
cancel(taskId: string): Promise<void>
}
重复点击也要处理。执行中的按钮进入禁用或加载状态,命令层以 taskId + command + revision 去重。取消这类不可逆业务操作应提供明确反馈,不能用“界面已消失”代替“任务已取消”。
9. 异常恢复分为三类
第一类是业务可恢复错误,例如短暂断连,保留球形入口并显示可重试状态;第二类是平台展示失败,业务仍在运行,应降级到应用内状态页或其他合规提示;第三类是状态损坏或任务已不存在,需要清理陈旧展示并记录诊断信息。
async function safeApply(port: FlashSurfacePort, vm: FlashViewModel): Promise<void> {
try {
await port.update(vm)
} catch (error) {
await diagnostics.record('FLASH_SURFACE_UPDATE_FAILED', vm.taskId, error)
await fallbackPresenter.showInApp(vm)
}
}
日志不要记录用户敏感内容,只保留任务类别、阶段、版本和错误码。恢复机制必须透明、可逆,并遵循平台公开能力与权限边界。
10. 平台适配层如何应对 SDK 变化
适配层负责把领域展示模型转换为当前 SDK 所需参数,也负责把平台回调翻译成领域命令。这里可以集中处理版本判断、能力可用性、资源映射和错误码归一化。上层不应根据具体错误码写分支。
export interface CapabilityProbe {
isFlashWindowAvailable(): Promise<boolean>
}
export class HarmonyFlashAdapter implements FlashSurfacePort {
async showWindow(view: FlashViewModel): Promise<void> {
// 在此处映射到当前 SDK 的真实参数与调用;示例省略具体签名
await this.invokePlatform('show', view)
}
}
若设备或版本不支持,能力探测返回否,由协调器选择应用内降级,不要通过捕获所有异常来猜测能力是否存在。
11. 测试重点是状态组合,不是截图数量
纯策略函数至少覆盖:前台展开、前台收起、后台运行、暂停、完成、可恢复失败、不可恢复失败。协调器测试使用假的端口,验证同一版本不会重复调用、旧事件不会覆盖新状态、平台失败会进入降级。适配层再用少量设备测试确认真实 API 行为。
it('rejects an outdated progress event', async () => {
const current = stateOf({ progress: 80, updatedAt: 200 })
const next = reduce(current, { kind: 'PROGRESS', value: 30, at: 100 })
expect(next.progress).assertEqual(80)
})
真机验收还应包含锁屏/解锁、前后台切换、旋转或窗口变化、连续点击、任务完成瞬间以及进程恢复。测试结论要基于目标设备和当前 SDK,不能把示意代码当成兼容性证明。
12. 落地清单与总结
接入前先确认业务是否真的需要持续状态展示,并核对官方支持范围。实现时建立稳定的领域模型和唯一事件入口,用纯策略决定窗、球或隐藏;用端口隔离平台 SDK,用状态快照完成期望与实际的对账;对高频进度做语义合并,对生命周期和重复操作做幂等;最后准备应用内降级与可观察日志。
工程封装的价值不在于多写几层,而在于让变化停在正确位置:业务变化不触碰平台细节,SDK 变化不重写页面,异常发生后也能知道系统“想显示什么、实际显示了什么”。做到这一点,闪控窗才从一次演示变成可长期维护的产品能力。
本文为 HarmonyOS 7 新能力工程实践系列第 034 篇。示例用于解释架构方法,接入生产项目时请以目标 SDK、设备能力和官方文档为准。
更多推荐




所有评论(0)