【鸿蒙开发实战】HarmonyOS 跨设备应用接续教程:图片浏览无缝迁移
HarmonyOS 跨设备应用接续教程:图片浏览无缝迁移
一、概述
应用接续(App Continuation)是 HarmonyOS 分布式能力的标志性功能,允许用户在一个设备上使用应用时,无缝地将整个应用状态迁移到另一台设备上继续操作。与简单的数据同步不同,应用接续迁移的是完整的运行时上下文——当前页面、浏览位置、数据列表,用户在另一台设备上看到的是"同一个应用在同一瞬间的延续"。
本教程以「云星图」项目的图片浏览场景为例,讲解:
- module.json5 接续配置 – 声明 continueType 与 continuable
- 源端数据序列化 – onContinue 回调中打包当前页面状态
- 目标端冷启动恢复 – onCreate 中解析接续数据 + 恢复窗口
- 目标端热启动恢复 – onNewWant 中处理热启动接续
- UI 层状态恢复 – 导航栈跳转 + 页面数据还原
- 实时状态保持 – 页面切换时实时更新接续数据源
整体接续流程如下:
二、前置准备
2.1 设备与账号要求
应用接续依赖 HarmonyOS 分布式能力,需要满足以下条件:
| 条件 | 说明 |
|---|---|
| 设备要求 | 两台均支持分布式能力的 HarmonyOS 设备(手机/平板/PC) |
| 账号登录 | 两台设备需登录同一华为账号 |
| 网络连接 | 两台设备需联网(Wi-Fi 或移动网络) |
| 蓝牙开启 | 设备间通过蓝牙发现,需保持蓝牙开启 |
2.2 相关文档
2.3 依赖导入
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
2.4 核心概念
应用接续涉及三个核心环节,理解它们的关系是掌握接续开发的关键:
| 环节 | 触发方 | 核心回调/方法 | 作用 |
|---|---|---|---|
| 序列化 | 源设备 | onContinue(wantParam) |
打包当前页面状态,返回 AGREE/REJECT |
| 分发 | 系统 | – | 跨设备拉起目标应用,携带 wantParam |
| 恢复 | 目标设备 | onCreate / onNewWant |
检测 CONTINUATION 启动原因,解析参数 |
| UI 还原 | 目标设备 | 页面 aboutToAppear |
读取 AppStorage 中的接续数据 |
三、module.json5 配置
3.1 声明接续能力
在 module.json5 的 Ability 配置中添加两个关键字段:
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
// 声明接续类型(快速接续)
"continueType": ["EntryAbility_ContinueQuickStart"],
// 启用应用接续能力
"continuable": true
// ... 其他配置
}
]
}
}
配置说明:
| 字段 | 作用 | 取值 |
|---|---|---|
continueType |
声明接续类型标识,系统据此匹配可接续的设备 | 字符串数组,自定义命名 |
continuable |
是否允许该 Ability 被接续 | true / false |
注意:
continueType的值是自定义的标识符,两台设备上安装的相同应用会自动匹配。如果应用有多个 Ability 需要接续,每个 Ability 可以设置不同的 continueType。
四、定义接续数据结构
4.1 接续参数接口
接续传输的数据通过 wantParam 携带,需要定义清晰的数据结构:
interface ContinuationParams {
imgUrls?: string; // 图片 URL 列表的 JSON 字符串
currentIndex?: number; // 当前浏览的图片索引
}
为什么 imgUrls 用 string 而非 string[]? wantParam 的值类型有限制,复杂数据结构需要先 JSON.stringify 序列化为字符串,目标端再 JSON.parse 还原。
4.2 AppStorage 状态键
在 StateKeys 中定义接续相关的全局状态键,分为两组:
export class StateKeys {
// ─── 应用接续:传输通道(源端 → 目标端)───
/** 接续的图片 URL 列表 */
static readonly CONTINUATION_IMG_URLS: string = 'continuationImgUrls';
/** 接续的当前图片索引 */
static readonly CONTINUATION_INDEX: string = 'continuationIndex';
/** 接续目标页面 */
static readonly CONTINUATION_TARGET_PAGE: string = 'continuationTargetPage';
// ─── 应用接续:实时状态源(供 onContinue 读取)───
/** 当前 PicView 的图片 URL 列表(用于接续序列化) */
static readonly CURRENT_PIC_VIEW_URLS: string = 'currentPicViewUrls';
/** 当前 PicView 的图片索引(用于接续序列化) */
static readonly CURRENT_PIC_VIEW_INDEX: string = 'currentPicViewIndex';
}
两组状态键的分工:
五、源端:onContinue 序列化状态
5.1 实现 onContinue 回调
当用户触发接续时,系统回调源设备的 onContinue,在此方法中读取当前页面状态并打包:
export default class EntryAbility extends UIAbility {
// 应用接续:源设备序列化数据
onContinue(wantParam: ContinuationParams): AbilityConstant.OnContinueResult {
// 从 AppStorage 读取 PicView 实时维护的状态
const imgUrls = AppStorage.get<string[]>(StateKeys.CURRENT_PIC_VIEW_URLS) ?? [];
const currentIndex = AppStorage.get<number>(StateKeys.CURRENT_PIC_VIEW_INDEX) ?? 0;
hilog.info(DOMAIN, 'EntryAbility',
`onContinue 数据准备: imgUrls长度=${imgUrls.length}, currentIndex=${currentIndex}`);
// 无有效数据时拒绝接续
if (imgUrls.length === 0) {
return AbilityConstant.OnContinueResult.MISMATCH;
}
// 序列化数据写入 wantParam
wantParam.imgUrls = JSON.stringify(imgUrls);
wantParam.currentIndex = currentIndex;
return AbilityConstant.OnContinueResult.AGREE;
}
}
返回值说明:
| 返回值 | 含义 | 使用场景 |
|---|---|---|
AGREE |
同意接续,系统开始迁移 | 有有效数据可迁移时 |
MISMATCH |
数据不匹配,拒绝接续 | 当前状态不适合接续(如无数据) |
DATA_READY |
数据已就绪 | 配合异步数据准备时使用 |
关键点:
onContinue是同步回调,不能做异步操作。如果需要异步准备数据,应在页面运行期间就实时维护好状态(见第六节),而不是等到onContinue时才去异步获取。
5.2 实时维护接续数据源
onContinue 依赖的 CURRENT_PIC_VIEW_URLS 和 CURRENT_PIC_VIEW_INDEX 不是凭空产生的,而是 PicView 页面在运行期间实时写入 AppStorage 的:
// PicView.ets
@Entry
@Component
export struct PicView {
@State imgUrls: string[] = [];
@StorageLink(StateKeys.CURRENT_IMAGE_INDEX) currentIndex: number = 0;
async aboutToAppear(): Promise<void> {
// ... 接续数据恢复逻辑(见第七节)...
// 实时保存当前状态供 onContinue 使用
AppStorage.setOrCreate(StateKeys.CURRENT_PIC_VIEW_URLS, this.imgUrls);
AppStorage.setOrCreate(StateKeys.CURRENT_PIC_VIEW_INDEX, this.currentIndex);
// ... 其他初始化 ...
}
}
在 Swiper 组件的 onChange 回调中,每次切换图片都更新索引:
Swiper(this.controller) {
ForEach(this.imgUrls, (imgUrl: string, index: number) => {
// ... 图片内容 ...
})
}
.index(this.currentIndex)
.onChange((index: number) => {
this.currentIndex = index;
// 实时更新接续状态
AppStorage.setOrCreate(StateKeys.CURRENT_PIC_VIEW_INDEX, index);
// ... 其他逻辑 ...
})
为什么要实时维护?
最佳实践: 在页面
aboutToAppear时写入完整状态(URL 列表 + 索引),在交互回调中增量更新变化的字段(索引)。这样无论用户何时触发接续,onContinue都能拿到最新状态。
六、目标端:冷启动恢复
6.1 onCreate 中处理接续
目标设备收到接续请求后,如果应用未在运行(冷启动),系统会通过 onCreate 拉起应用,launchParam.launchReason 会标记为 CONTINUATION:
export default class EntryAbility extends UIAbility {
async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
// ... 其他初始化(StorageUtil、网络监听、云存储等)...
// 应用接续相关
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
const params = want.parameters as ContinuationParams;
if (params) {
const imgUrlsJson: string = params.imgUrls ?? '';
const currentIndex: number = params.currentIndex ?? 0;
if (imgUrlsJson) {
try {
// 反序列化图片 URL 列表
const imgUrls: string[] = JSON.parse(imgUrlsJson);
// 写入 AppStorage,供 UI 层读取
AppStorage.setOrCreate<string[]>(StateKeys.CONTINUATION_IMG_URLS, imgUrls);
AppStorage.setOrCreate<number>(StateKeys.CONTINUATION_INDEX, currentIndex);
// 标记接续目标页面,供导航层读取
AppStorage.setOrCreate<string>(StateKeys.CONTINUATION_TARGET_PAGE, 'PicView');
} catch (e) {
hilog.error(DOMAIN, 'EntryAbility', `解析接续数据失败:${JSON.stringify(e)}`);
}
}
}
// 恢复窗口阶段
this.context.restoreWindowStage(this.storage);
}
// ... 其他初始化(实况窗、AppLinking 等)...
}
}
冷启动接续恢复流程:
关键设计点:
launchReason判断:必须先判断launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION,否则会把正常启动的 Want 误当接续数据处理。restoreWindowStage:接续冷启动时,必须调用此方法恢复窗口阶段,否则 UI 不会正常加载。传入的LocalStorage对象需要与onWindowStageCreate中使用的一致。- 写入三个 AppStorage 键:
CONTINUATION_IMG_URLS和CONTINUATION_INDEX供 PicView 读取恢复数据,CONTINUATION_TARGET_PAGE供 TabsView 读取决定导航跳转。
七、目标端:热启动恢复
7.1 onNewWant 中处理接续
如果目标设备上应用已在运行(热启动),系统通过 onNewWant 传递接续数据,而不走 onCreate:
export default class EntryAbility extends UIAbility {
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 处理接续启动(热启动场景)
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
this.handleContinuationData(want);
}
// ... 处理系统分享、服务卡片、AppLinking 等其他入口 ...
}
// 封装接续数据处理(onCreate 和 onNewWant 共用)
private handleContinuationData(want: Want): void {
const params = want.parameters as ContinuationParams;
if (params) {
const imgUrlsJson: string = params.imgUrls ?? '';
const currentIndex: number = params.currentIndex ?? 0;
if (imgUrlsJson) {
try {
const imgUrls: string[] = JSON.parse(imgUrlsJson);
AppStorage.setOrCreate<string[]>(StateKeys.CONTINUATION_IMG_URLS, imgUrls);
AppStorage.setOrCreate<number>(StateKeys.CONTINUATION_INDEX, currentIndex);
AppStorage.setOrCreate<string>(StateKeys.CONTINUATION_TARGET_PAGE, 'PicView');
hilog.info(DOMAIN, 'EntryAbility',
`接续数据已恢复:${imgUrls.length} 张图片,索引 ${currentIndex}`);
} catch (e) {
hilog.error(DOMAIN, 'EntryAbility', `解析接续数据失败:${JSON.stringify(e)}`);
}
}
}
this.context.restoreWindowStage(this.storage);
}
}
为什么提取公共方法? 冷启动(onCreate)和热启动(onNewWant)的接续数据处理逻辑完全一致,提取为 handleContinuationData 避免重复代码。冷启动中也可以调用此方法:
async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
// ...
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
this.handleContinuationData(want); // 复用公共方法
}
// ...
}
冷启动与热启动的对比:
八、UI 层:导航跳转与状态恢复
8.1 路由注册
PicView 页面通过 Navigation 路由表注册,在 resources/base/profile/router_map.json 中声明:
{
"routerMap": [
{
"name": "PicView",
"pageSourceFile": "src/main/ets/pages/PicView.ets",
"buildFunction": "PicViewBuilder",
"data": {
"description": "this is PicView"
}
}
]
}
在 module.json5 中引用路由表:
{
"module": {
"name": "Home",
"type": "har",
"routerMap": "$profile:router_map"
}
}
PicView 中的 @Builder 函数需与 buildFunction 对应:
// PicView.ets
@Builder
export function PicViewBuilder() {
PicView();
}
@Entry
@Component
export struct PicView {
// ...
build() {
NavDestination() {
// 页面内容
}
.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack;
})
}
}
8.2 TabsView:检测接续标记并跳转
TabsView 是应用的主界面,持有 Navigation 导航栈。在 aboutToAppear 中检测接续标记,自动跳转到 PicView:
// TabsView.ets
@Component
export struct TabsView {
// 订阅接续目标页面标记
@StorageLink(StateKeys.CONTINUATION_TARGET_PAGE)
continuationTargetPage: string | undefined = undefined;
// 导航栈(全局共享)
pathStack: NavPathStack = AppStorageV2.connect(
NavPathStack, StateKeys.NAV_STACK, () => new NavPathStack()
)!;
aboutToAppear(): void {
// ... 其他初始化 ...
// 处理应用接续
this.handleContinuationNavigation();
}
private handleContinuationNavigation(): void {
// 检测接续目标页面标记
if (this.continuationTargetPage === 'PicView') {
// 清除标记,避免重复跳转
AppStorage.setOrCreate(StateKeys.CONTINUATION_TARGET_PAGE, '');
// 通过导航栈跳转到图片详情页
this.pathStack.pushPathByName('PicView', null);
console.log('接续跳转:进入图片详情页');
}
}
}
导航跳转流程:
为什么要清除标记?
@StorageLink是响应式的,如果不清除标记,后续任何原因导致 TabsView 重新执行aboutToAppear(如页面切换回来)都会再次触发跳转,形成死循环。清除标记确保接续跳转只执行一次。
8.3 PicView:恢复图片浏览状态
PicView 在 aboutToAppear 中检测接续数据,优先使用接续传入的数据恢复页面状态:
// PicView.ets
@Entry
@Component
export struct PicView {
@State imgUrls: string[] = [];
@StorageLink(StateKeys.CURRENT_IMAGE_INDEX) currentIndex: number = 0;
@StorageLink(StateKeys.COS_IMAGE_URLS_FOR_VIEW) allImgUrls: string[] = [];
async aboutToAppear(): Promise<void> {
// 1. 检测接续数据
const continuationUrls = AppStorage.get<string[]>(StateKeys.CONTINUATION_IMG_URLS);
const continuationIndex = AppStorage.get<number>(StateKeys.CONTINUATION_INDEX);
if (continuationUrls && continuationUrls.length > 0) {
// 2. 使用接续传入的数据恢复
this.imgUrls = continuationUrls;
this.currentIndex = continuationIndex ?? 0;
// 3. 清除接续标记,避免下次误用
AppStorage.setOrCreate(StateKeys.CONTINUATION_IMG_URLS, []);
AppStorage.setOrCreate(StateKeys.CONTINUATION_INDEX, 0);
hilog.info(0x0000, 'PicView',
`接续恢复:${this.imgUrls.length}张图片,索引${this.currentIndex}`);
} else {
// 4. 非接续场景:从收藏或全部图片进入
let tempUrls: string[] = AppStorage.get(StateKeys.TEMP_FAVORITE_URLS) || [];
if (tempUrls && tempUrls.length > 0) {
this.imgUrls = tempUrls;
} else {
this.imgUrls = this.allImgUrls;
}
}
// 5. 实时保存当前状态供下一次 onContinue 使用
AppStorage.setOrCreate(StateKeys.CURRENT_PIC_VIEW_URLS, this.imgUrls);
AppStorage.setOrCreate(StateKeys.CURRENT_PIC_VIEW_INDEX, this.currentIndex);
// ... 其他初始化 ...
}
}
PicView 的数据恢复决策:
关键点: 恢复数据后必须立即清除接续标记(
CONTINUATION_IMG_URLS和CONTINUATION_INDEX),否则用户返回后再次进入 PicView 时会误用旧的接续数据。同时要写入实时状态源(CURRENT_PIC_VIEW_URLS和CURRENT_PIC_VIEW_INDEX),使目标设备也能再次发起接续。
8.4 Swiper 绑定与实时更新
PicView 使用 Swiper 展示图片列表,通过 index 绑定当前索引:
Swiper(this.controller) {
ForEach(this.imgUrls, (imgUrl: string, index: number) => {
ImagePreview({ config: {} }) {
Image(imgUrl)
.width("100%")
.height("100%")
.objectFit(ImageFit.Contain)
}
}, (imgUrl: string) => imgUrl)
}
.width('100%')
.height('100%')
.loop(false)
.index(this.currentIndex) // 绑定当前索引
.indicator(false)
.onChange((index: number) => {
this.currentIndex = index;
// 实时更新接续状态
AppStorage.setOrCreate(StateKeys.CURRENT_PIC_VIEW_INDEX, index);
// ... 其他逻辑 ...
})
为什么
loop(false)? 接续场景下图片列表是确定的,禁用循环避免索引错乱。onChange中实时更新CURRENT_PIC_VIEW_INDEX确保用户滑动后的位置能被后续接续正确恢复。
九、完整接续时序
将所有环节串联起来,完整的接续时序如下:
十、常见问题与最佳实践
10.1 常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| 接续后页面停在首页,没进 PicView | TabsView 没检测 CONTINUATION_TARGET_PAGE 或标记已被清除 |
确保在 aboutToAppear 中调用 handleContinuationNavigation,且在 push 前才清除标记 |
| PicView 恢复了旧数据 | CONTINUATION_IMG_URLS 未在恢复后清除 |
恢复后立即 setOrCreate(CONTINUATION_IMG_URLS, []) |
| 接续后图片索引不对 | Swiper 的 index 未正确绑定 currentIndex |
确保 .index(this.currentIndex) 且 onChange 中更新 this.currentIndex |
onContinue 返回 MISMATCH |
CURRENT_PIC_VIEW_URLS 为空(用户不在 PicView 时触发接续) |
在 onContinue 中做空值判断,或为非 PicView 页面提供默认接续数据 |
| 热启动接续不生效 | 只在 onCreate 处理了接续,未处理 onNewWant |
onNewWant 中也检测 launchReason === CONTINUATION |
| 目标端接续后无法再次发起接续 | 恢复后未写入 CURRENT_PIC_VIEW_URLS |
PicView 恢复数据后立即写入实时状态源 |
restoreWindowStage 后 UI 白屏 |
传入的 LocalStorage 与 onWindowStageCreate 不一致 |
确保 onCreate 和 onNewWant 使用同一个 storage 实例 |
| 接续数据太大导致传输失败 | wantParam 携带的数据量过大 |
只传必要数据(如 ID),目标端通过 ID 重新拉取;避免传大量 URL |
10.2 最佳实践
-
实时维护状态源:不要等
onContinue时才去获取数据。在页面aboutToAppear写入完整状态,在交互回调中增量更新。这样onContinue只需同步读取即可。 -
冷热启动都处理:
onCreate处理冷启动接续,onNewWant处理热启动接续,两者逻辑一致,建议提取公共方法。 -
清除标记防重复:接续标记(
CONTINUATION_TARGET_PAGE、CONTINUATION_IMG_URLS)使用后必须立即清除,避免页面重建时重复触发。 -
恢复后写入状态源:目标端恢复数据后,要同步写入
CURRENT_PIC_VIEW_URLS和CURRENT_PIC_VIEW_INDEX,使目标端也能发起新的接续,形成闭环。 -
onContinue只做同步操作:onContinue是同步回调,不能做网络请求或文件 IO。需要异步准备的数据应在运行期间提前维护好。 -
wantParam中的复杂数据要序列化:数组、对象等复杂数据结构需JSON.stringify为字符串传输,目标端JSON.parse还原。 -
restoreWindowStage必须调用:接续冷启动时,onCreate中需要调用restoreWindowStage恢复窗口阶段,否则 UI 不会加载。 -
导航栈跳转用
pushPathByName:接续跳转应通过 Navigation 路由栈的pushPathByName实现,而不是router.pushUrl,以保持与正常导航的一致性。
十一、API 速查
11.1 EntryAbility 接续相关方法
| 方法 | 触发时机 | 作用 |
|---|---|---|
onContinue(wantParam) |
源端用户触发接续时 | 序列化当前状态到 wantParam,返回 AGREE/MISMATCH |
onCreate(want, launchParam) |
目标端冷启动 | 检测 CONTINUATION 启动原因,解析接续数据 |
onNewWant(want, launchParam) |
目标端热启动 | 检测 CONTINUATION 启动原因,解析接续数据 |
restoreWindowStage(storage) |
接续恢复时 | 恢复窗口阶段,触发 UI 加载 |
11.2 AbilityConstant 关键枚举
| 枚举 | 值 | 说明 |
|---|---|---|
LaunchReason.CONTINUATION |
– | 启动原因为应用接续 |
OnContinueResult.AGREE |
0 | 同意接续 |
OnContinueResult.REJECT |
1 | 拒绝接续 |
OnContinueResult.MISMATCH |
2 | 数据不匹配 |
OnContinueResult.DATA_READY |
3 | 数据已就绪 |
11.3 AppStorage 状态键一览
| 状态键 | 方向 | 作用 |
|---|---|---|
CURRENT_PIC_VIEW_URLS |
运行时 → onContinue | PicView 实时维护的图片列表 |
CURRENT_PIC_VIEW_INDEX |
运行时 → onContinue | PicView 实时维护的当前索引 |
CONTINUATION_IMG_URLS |
onContinue → 目标端 PicView | 接续传输的图片列表 |
CONTINUATION_INDEX |
onContinue → 目标端 PicView | 接续传输的当前索引 |
CONTINUATION_TARGET_PAGE |
onContinue → 目标端 TabsView | 接续目标页面标识 |
十二、总结
应用接续是 HarmonyOS 分布式能力的核心体验,本教程以图片浏览无缝迁移为例,讲解了完整实现:
- 配置阶段:
module.json5中声明continueType和continuable: true - 数据准备:定义
ContinuationParams接口 + 5 个 AppStorage 状态键,分为实时状态源和接续传输两组 - 源端序列化:
onContinue中同步读取 AppStorage,JSON.stringify序列化后写入wantParam,返回AGREE - 实时维护:PicView 的
aboutToAppear写入完整状态,Swiper.onChange增量更新索引 - 目标端恢复:
onCreate(冷启动)和onNewWant(热启动)检测CONTINUATION启动原因,解析参数写入 AppStorage,调用restoreWindowStage - UI 还原:TabsView 检测
CONTINUATION_TARGET_PAGE跳转导航栈,PicView 读取接续数据恢复imgUrls和currentIndex,清除标记并写入实时状态源
开发建议:
onContinue只做同步读取,运行期间实时维护状态源- 冷启动和热启动都要处理,提取公共方法避免重复
- 接续标记用完即清,防止重复触发
- 恢复后写入实时状态源,使目标端也能再次发起接续
- 复杂数据
JSON.stringify序列化传输,目标端JSON.parse还原
更多推荐



所有评论(0)