一、迁移按钮背后有两条异步链路

“跨端迁移”看起来是一个按钮,实际包含设备发现和状态接续两条链路。第一条决定当前有哪些可信设备可选,并监听设备上线、离线;第二条把当前创作状态封装进接续参数,在目标设备拉起应用后恢复。只实现 startAbility() 会得到一个偶尔能拉起、却无法解释失败和恢复范围的入口。

生成页顶部保留迁移入口,是因为此时纹样、载体、预览模式和 prompt 都可能已经形成。迁移按钮不能直接假设权限、设备或网络可用,而应先展示设备列表,再让用户确认目标。

生成页中的跨端迁移入口

阶段 可观察状态 主要失败 页面动作
初始 未检查设备 能力或权限不可用 显示说明,不发起迁移
发现中 加载设备列表 无可信设备 提供刷新与配对指引
可选择 设备名称和类型 设备离线 刷新列表
发起中 目标设备已锁定 启动失败 显示错误码,可重试
已发起 等待目标拉起 目标未响应 保留本机状态
已恢复 目标页面定位完成 参数缺失或非法 回退到安全步骤

二、设备管理器要单例化并显式解绑

设备发现服务不应在每次重组时创建新实例。一个进程内复用 DeviceManager,订阅一次设备状态事件,并在页面退出时解绑。创建失败返回空列表,让 UI 进入可解释空态,而不是向上抛出不可恢复异常。

let manager: distributedDeviceManager.DeviceManager | null = null
let listenerBound: boolean = false

function ensureManager(): distributedDeviceManager.DeviceManager | null {
  if (manager !== null) {
    return manager
  }
  try {
    manager = distributedDeviceManager.createDeviceManager(
      'com.example.myapplication'
    )
    return manager
  } catch {
    return null
  }
}

export function stopDeviceListener(): void {
  if (!listenerBound || manager === null) return
  try {
    manager.off('deviceStateChange')
  } finally {
    listenerBound = false
  }
}

单例只解决重复创建,不代表可以永久持有页面回调。监听器闭包若引用已销毁组件,会造成重复刷新和内存泄漏,因此页面生命周期仍要负责订阅和取消。

三、设备列表只暴露页面真正需要的字段

平台设备对象可能包含较多底层字段,页面只需要稳定 ID、可读名称和设备类型。服务层完成映射,异常时返回空数组;UI 不直接依赖平台对象结构。

export interface DiscoveredDevice {
  deviceId: string
  deviceName: string
  deviceType: string
}

export function getTrustedDevicesSync(): DiscoveredDevice[] {
  const dm = ensureManager()
  if (dm === null) return []
  try {
    return dm.getAvailableDeviceListSync().map((info) => ({
      deviceId: info.deviceId,
      deviceName: info.deviceName || '未命名设备',
      deviceType: info.deviceType
    }))
  } catch {
    return []
  }
}

export function listenDeviceStateChange(onChanged: () => void): void {
  const dm = ensureManager()
  if (dm === null || listenerBound) return
  dm.on('deviceStateChange', () => onChanged())
  listenerBound = true
}

空列表至少有三种含义:没有配对设备、能力暂不可用、读取失败。页面可以统一展示“暂无可用设备”,同时提供刷新和系统配对指引;若业务需要更精确诊断,服务返回带状态码的结果对象,而不是继续用异常控制界面。

四、快照传业务状态,不传组件细节

接续快照应覆盖用户当前任务所需的最小字段:页签、纹样 ID、载体 ID、步骤、三维开关、导出格式、prompt 和图片 URL。滚动位置、动画进度、弹窗开关和加载计时器属于设备局部表现,不适合迁移。

export interface ContinuationSnapshot {
  tab: number
  patternId: number
  productId: string
  step: number
  use3D: boolean
  focus: string
  exportFormat: string
  customPrompt: string
  aiImageUrl: string
}

export function setContinuationSnapshot(
  current: ContinuationSnapshot,
  partial: Partial<ContinuationSnapshot>
): ContinuationSnapshot {
  return {
    tab: partial.tab ?? current.tab,
    patternId: partial.patternId ?? current.patternId,
    productId: partial.productId ?? current.productId,
    step: partial.step ?? current.step,
    use3D: partial.use3D ?? current.use3D,
    focus: partial.focus ?? current.focus,
    exportFormat: partial.exportFormat ?? current.exportFormat,
    customPrompt: clampText(partial.customPrompt, current.customPrompt, 800),
    aiImageUrl: clampText(partial.aiImageUrl, current.aiImageUrl, 1200)
  }
}

稳定 ID 比中文名称更适合跨端传递。目标设备恢复时还要再次查询目录:ID 不存在就回到选择页,不能静默替换成默认纹样或默认载体。

五、onContinue 负责写入一致快照

发起迁移前,页面每次关键状态变化都同步快照;系统回调到 onContinue 时只读取一次完整快照,并把字段写入同一个参数对象。不要在回调中重新访问页面组件或异步查询,因为系统期待快速返回明确结果。

onContinue(wantParam: Record<string, Object>):
  AbilityConstant.OnContinueResult {
  try {
    const snapshot = getContinuationSnapshot()
    const parameters: Record<string, Object> = {
      auto: true,
      tab: snapshot.tab,
      patternId: snapshot.patternId,
      productId: snapshot.productId,
      use3D: snapshot.use3D,
      step: snapshot.step,
      focus: snapshot.focus,
      exportFormat: snapshot.exportFormat,
      customPrompt: snapshot.customPrompt,
      aiImageUrl: snapshot.aiImageUrl
    }
    wantParam['parameters'] = parameters
    wantParam['wantType'] = 'wenyuan_continuation'
    return AbilityConstant.OnContinueResult.AGREE
  } catch {
    return AbilityConstant.OnContinueResult.REJECT
  }
}

捕获异常后继续返回 AGREE 会让目标设备收到不完整参数,问题更难定位。只有快照序列化成功才同意接续;失败时保留本机状态并向用户显示可重试提示。

六、目标设备恢复必须经过校验

目标端可能从 onCreateonNewWant 接收接续参数,两条入口要汇入同一个解析函数。解析先确认类型,再限制数值范围和文本长度,最后按依赖顺序恢复:先目录 ID,再步骤与 UI 状态,最后恢复可选的图片 URL。

function restoreContinuation(raw: Record<string, Object>): RestoreResult {
  const patternId = toSafeInt(raw['patternId'], 0)
  const productId = toSafeText(raw['productId'], 64)
  const step = clamp(toSafeInt(raw['step'], 0), 0, 2)

  if (getPatternById(patternId) === undefined) {
    return { ok: false, reason: '纹样已不存在', fallbackStep: 0 }
  }
  if (getProductById(productId) === undefined) {
    return { ok: false, reason: '载体已不存在', fallbackStep: 1 }
  }
  return {
    ok: true,
    patternId,
    productId,
    step,
    use3D: raw['use3D'] === true,
    customPrompt: toSafeText(raw['customPrompt'], 800),
    aiImageUrl: toSafeText(raw['aiImageUrl'], 1200)
  }
}

AI 图片 URL 即使成功迁移,也必须重新验证可访问性;三维开关即使为真,也要重新检查目标设备能力。接续传递的是意图和上下文,不是源设备的资源句柄。

七、按钮提示要对应真实结果

调用 startAbility() 成功只表示启动请求已提交,不等于目标设备已经完成恢复。提示文案应写“已向目标设备发起请求”,而不是“迁移成功”。错误则保留平台错误码和可读信息,方便区分设备离线、权限缺失与目标应用不可用。

export async function triggerContinuation(context: UIAbilityContext,
  device: DiscoveredDevice): Promise<ContinuationMessage> {
  try {
    context.startAbility({
      deviceId: device.deviceId,
      bundleName: 'com.example.myapplication',
      abilityName: 'EntryAbility',
      parameters: { auto: true }
    })
    return {
      level: 'info',
      text: `已向 ${device.deviceName} 发起启动请求`
    }
  } catch (error) {
    const failure = error as BusinessError
    return {
      level: 'error',
      text: `启动失败:${failure.code} ${failure.message}`
    }
  }
}

发起期间锁定目标设备并禁用重复点击;收到结果后恢复按钮。本机工作流始终保留,用户可以在迁移失败后继续编辑或改选其他设备。

八、异常与验收矩阵

场景 预期列表/按钮 快照要求 目标端结果
无可信设备 空态、可刷新 不生成迁移请求 不拉起
设备上线 列表自动刷新 保持当前创作 可选择
设备点击后离线 显示错误、按钮恢复 本机快照不丢 不误报成功
快照写入失败 拒绝接续 本机继续可用 不收残缺参数
纹样 ID 非法 已发起 参数可解析 回退纹样选择
载体 ID 非法 已发起 参数可解析 回退载体选择
目标不支持 3D 已发起 use3D 保留为意图 使用二维并提示
AI URL 已过期 已发起 URL 长度受限 清空图片并可重生成
重复点击 首次请求进行中 只读一份快照 不重复拉起

验证时需要两台满足接续条件的设备;只有一台设备时仍可完整验收空态、刷新、按钮禁用、快照序列化和错误提示,但不能把这些结果写成跨端成功。

九、总结

跨端迁移入口的完整性来自清晰的职责划分:设备服务负责发现和监听,页面负责选择目标与提示状态,快照只保存稳定业务字段,onContinue 快速写入一致参数,目标端统一解析并做能力复检。任何失败都保留本机创作上下文,按钮文案只描述已经发生的事实,不把“已发起”误报为“已完成”。

应用接续的生命周期与配置可参考应用接续开发指导

Logo

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

更多推荐