纹渊 HarmonyOS 7 工程实战(15):跨端迁移按钮的权限请求与状态提示
一、迁移按钮背后有两条异步链路
“跨端迁移”看起来是一个按钮,实际包含设备发现和状态接续两条链路。第一条决定当前有哪些可信设备可选,并监听设备上线、离线;第二条把当前创作状态封装进接续参数,在目标设备拉起应用后恢复。只实现 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 会让目标设备收到不完整参数,问题更难定位。只有快照序列化成功才同意接续;失败时保留本机状态并向用户显示可重试提示。
六、目标设备恢复必须经过校验
目标端可能从 onCreate 或 onNewWant 接收接续参数,两条入口要汇入同一个解析函数。解析先确认类型,再限制数值范围和文本长度,最后按依赖顺序恢复:先目录 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 快速写入一致参数,目标端统一解析并做能力复检。任何失败都保留本机创作上下文,按钮文案只描述已经发生的事实,不把“已发起”误报为“已完成”。
应用接续的生命周期与配置可参考应用接续开发指导。
更多推荐



所有评论(0)