一个批量文件整理任务正在执行。用户在任务中心看到进度为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或重置任务状态,用户看到的进度可能回退到零。仓库需要根据服务事件和缓存记录进行初始化决策,首屏只负责显示。

DevEco Studio中的共享快照发布与页面订阅

四、两个页面使用同一个键,展示密度不同

球态摘要和任务中心可以分别实现为组件,它们共同读取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绑定在一起,而是先确定任务事实的唯一拥有者,再让多个界面订阅同一个、带版本的状态投影。这样球态、展开窗口与任务中心即使同时出现,也能稳定展示任务的当前阶段。下一篇将围绕前后台切换,把订阅生命周期、任务恢复和状态重新同步串成完整链路。

参考:华为开发者—AppStorage共享范围、ArkUI状态更新与嵌套观测、窗口生命周期。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐