HarmonyOS 7闪控窗与闪控球连载05(共08篇):跨页面状态同步与版本收敛【共创稿事节】
一个批量文件整理任务正在执行。用户在任务中心看到进度为64%,切回主页面的球态摘要却停留在52%;从紧凑入口展开轻量窗口后,暂停按钮又显示为“继续”。三处界面读取了不同时间点的数据,导致用户无法判断任务究竟处于什么阶段。这种问题的根源往往不在控件,而在于多个页面各自维护任务副本。
前面的文章已建立轻任务队列、球窗视觉密度切换和触控命令网关。本篇进一步处理跨页面状态:同一进程内的任务中心、应用首页及轻量任务表面应共享一个状态发布源;不同视图可以选择不同字段展示,却不应独立修改任务完成率。闪控窗、闪控球涉及的系统接入需结合目标产品提供的开发能力进行配置,本篇重点给出可以复用的ArkUI业务状态层。

一、同步的是业务事实,不是每个控件
任务队列里的一个任务包含执行阶段、进度、总量、错误消息和事件版本。球态只需要任务名称、进度及一个状态颜色;展开窗口需要暂停、继续与取消;任务中心则展示队列中的全部任务。视图字段不同,但都应该从统一快照派生。
| 数据 | 拥有者 | 更新来源 | 呈现位置 |
|---|---|---|---|
| 处理阶段 | 任务仓库 | 执行服务的阶段事件 | 球态、展开窗口、任务中心 |
| 已处理数量 | 任务仓库 | 递增版本的进度事件 | 进度环、进度条、任务列表 |
| 操作提交中 | 命令控制器 | 用户指令提交与返回 | 交互按钮 |
| 页面是否展开 | 表面控制器 | 用户展示意图 | 当前表面 |
| 当前选中任务 | 页面状态 | 用户在列表中的选择 | 任务中心与详情 |
最容易混淆的是“已按下暂停”与“任务已经暂停”。按钮点击只生成命令,进程中的执行服务确认状态转移后才能发布新的PAUSED快照。显示层可以临时展示“指令处理中”,但不得直接将进度或执行阶段设为预期结果。这样即使用户连续切换页面,最终显示仍以执行事件为准。
二、给每次状态更新一个明确的版本
普通订阅流可能受到网络重试、异步回调顺序或页面重建影响,旧事件在新事件之后到达并不罕见。只比对时间戳可能遇到时钟精度和同一时刻多次事件的问题;业务侧为每个任务生成单调递增的revision更可靠。
export enum TaskPhase {
QUEUED = 'queued',
RUNNING = 'running',
PAUSED = 'paused',
DONE = 'done',
FAILED = 'failed'
}
export class TaskSummary {
constructor(
public id: string,
public label: string,
public phase: TaskPhase,
public completed: number,
public total: number,
public revision: number
) {}
}
export class TaskEvent {
constructor(
public id: string,
public phase: TaskPhase,
public completed: number,
public total: number,
public revision: number
) {}
}
export function percentOf(task: TaskSummary): number {
if (task.total <= 0) {
return 0
}
const safe = Math.max(0, Math.min(task.total, task.completed))
return Math.floor(safe * 100 / task.total)
}
TaskSummary是轻量投影,不承担执行。即使未来由多个服务共同处理一个任务,视图仍只接收这个统一数据结构。对已完成的任务,要定义服务端是否允许后续追加阶段事件;如果阶段已经终结,仓库应拒绝违反状态机的跳转,而不只是依赖进度百分比。
三、一个仓库发布状态,页面只订阅投影
在同一应用主线程中,AppStorage适合共享UI状态;使用@StorageLink能够让多个ArkUI页面读取同一存储键的变化。这里约定键名task.focus存储当前关注任务的最新快照,由仓库统一写入。每次事件都重新生成对象,而不是在旧对象上直接修改深层字段,避免V1状态观察粒度造成刷新遗漏。
export class FocusTaskRepository {
private latest: TaskSummary = new TaskSummary(
'file-pack', '素材归档', TaskPhase.QUEUED, 0, 24, 0)
initialize(): void {
AppStorage.setOrCreate('task.focus', this.latest)
}
accept(event: TaskEvent): boolean {
if (event.id !== this.latest.id ||
event.revision <= this.latest.revision ||
event.total < 0 || event.completed < 0 ||
event.completed > event.total) {
return false
}
const next = new TaskSummary(this.latest.id, this.latest.label,
event.phase, event.completed, event.total, event.revision)
this.latest = next
AppStorage.set('task.focus', next)
return true
}
getCurrent(): TaskSummary {
return this.latest
}
}
仓库应在页面订阅之前初始化一次,通常由应用入口的业务初始化过程调用,而不是每个页面在aboutToAppear()中各自写入默认值。若页面每出现一次就调用setOrCreate或重置任务状态,用户看到的进度可能回退到零。仓库需要根据服务事件和缓存记录进行初始化决策,首屏只负责显示。

四、两个页面使用同一个键,展示密度不同
球态摘要和任务中心可以分别实现为组件,它们共同读取task.focus。视图可以改变排版、字体大小、进度条类型,不能绕过仓库直接写completed。下列组件保持同一字段协议,布局代码可放在不同页面中使用。
@Component
export struct TaskBallSummary {
@StorageLink('task.focus') task: TaskSummary =
new TaskSummary('', '', TaskPhase.QUEUED, 0, 1, 0)
build() {
Column({ space: 6 }) {
Text(`${percentOf(this.task)}%`)
.fontSize(25).fontWeight(FontWeight.Bold)
Text(this.task.label).fontSize(12).maxLines(1)
Text(this.task.phase).fontSize(11)
}
.padding(12).borderRadius(28)
.backgroundColor('#EAF2FF')
}
}
@Component
export struct TaskCenterCard {
@StorageLink('task.focus') task: TaskSummary =
new TaskSummary('', '', TaskPhase.QUEUED, 0, 1, 0)
build() {
Column({ space: 10 }) {
Text(this.task.label).fontSize(19)
Progress({ value: percentOf(this.task), total: 100,
type: ProgressType.Linear })
Text(`${this.task.completed}/${this.task.total} 项`)
.fontSize(13)
Text(`状态:${this.task.phase}`)
.fontSize(12)
}.padding(18).borderRadius(16).backgroundColor(Color.White)
}
}
此处仅展示进程内跨页面的共享状态。任务执行服务若运行在其他线程、进程或设备上,应通过明确的消息通道把结果回送到主线程,再由仓库写入AppStorage。UI状态容器本身不提供跨设备的数据复制、事件持久化或消息可靠投递,这几类需求需要分别设计。
五、事件抖动与页面切换竞态
当事件频率较高时,应先在仓库做有效性检查,再决定是否通知视图。尤其在页面从紧凑入口切换到任务中心的瞬间,可能同时出现一次旧页面销毁、一次新页面订阅以及一批进度消息。如果两处都注册执行服务监听,会造成重复回调、界面刷新和资源泄漏。
推荐让执行服务只有一个应用级监听入口;仓库负责收敛事件;页面仅依赖AppStorage的状态订阅。在用户选择另一项任务时,应该同时切换当前任务标识和它的版本基线,避免新任务收到旧任务的事件。对于暂停、取消等命令,继续沿用此前的幂等控制器:按钮发出意图,仓库根据服务确认更新阶段。
可以在仓库入口加入事件日志,字段包括任务ID、上一个版本、接收版本、是否接受及拒绝原因。日志只保留必要的状态元数据,不应复制任务附带的私有文件内容。页面侧若出现进度不一致,先对比两处读取的revision,再排查控件渲染与订阅生命周期。
export function shouldAccept(current: TaskSummary,
incoming: TaskEvent): boolean {
if (incoming.id !== current.id) return false
if (incoming.revision <= current.revision) return false
if (incoming.completed < 0 || incoming.total < 0) return false
if (incoming.completed > incoming.total) return false
if (current.phase === TaskPhase.DONE &&
incoming.phase !== TaskPhase.DONE) return false
return true
}
// 接收事件后的顺序:校验 -> 构造新快照 -> 发布 -> 页面重绘
业务中还要考虑同一版本重复发送、服务重连后的版本重置、任务队列被替换等情况。不能只把数值从大到小排序;版本号必须与任务ID形成明确作用域。若服务重启后版本从1重新开始,仓库要先完成新的任务会话识别,再重新建立版本基线。
六、用一致的操作路径检查不同表面

| 操作链路 | 期望结果 | 检查重点 |
|---|---|---|
| 球态进入任务中心 | 两处进度与阶段一致 | 是否使用相同存储键 |
| 暂停命令返回较慢 | 按钮显示忙碌,阶段仍以服务为准 | 命令与业务事件是否分离 |
| 旧进度在新进度后到达 | 旧版本被忽略 | revision比较是否按任务进行 |
| 页面快速退出又进入 | 新页面立即获得最近快照 | 是否依赖短生命周期副本 |
| 新任务替换关注任务 | 前一任务的事件无法覆盖新任务 | ID与版本基线是否一起切换 |
| 任务中心与展开窗口同时可见 | 两个界面显示相同完成率 | 共享状态是否整体替换 |
对进度更新频率还应作节流:若业务每处理几百字节就发一次消息,UI没有必要逐条绘制。可以在仓库中按阶段变化优先、百分比变化次之的策略合并展示事件,同时保留执行侧的真实计数。节流只影响页面刷新频率,不应降低业务状态记录的准确性。
七、工程收束与资料
可靠的跨页面同步不是把每个页面的@State绑定在一起,而是先确定任务事实的唯一拥有者,再让多个界面订阅同一个、带版本的状态投影。这样球态、展开窗口与任务中心即使同时出现,也能稳定展示任务的当前阶段。下一篇将围绕前后台切换,把订阅生命周期、任务恢复和状态重新同步串成完整链路。
更多推荐

所有评论(0)