一、背景说明

HarmonyOS 的"接续"(Continuation)能力,解决的是同一账号下多设备之间的任务无缝迁移问题:在手机上看到一半的文章、编辑到一半的表单,可以在平板或鸿蒙电脑上从同一步骤继续。近期 HarmonyOS 7.0.0.111SP10 版本向 nova 系列下放了多人互助通信共享等分布式组网能力,跨设备协同的网络底座进一步成熟,而应用侧承接这一切的核心接口就是 UIAbility 的 onContinue 生命周期。本文实现一个完整的"阅读进度跨设备接续"示例:手机上退出页面,平板上拉起同一个应用时自动恢复到该页面并还原滚动位置。

二、技术原理

应用接续的数据流分三步:

  1. 发起端(源设备):系统流转入口触发接续时,应用所在 UIAbility 的 onContinue(wantParam: Record<string, Object>) 回调被调用,应用把需要迁移的状态(页面路径、表单内容、滚动位置等)以键值对形式写入 wantParam。数据量上限约 1MB,超出会接续失败。
  2. 系统传输:wantParam 经系统分布式软总线加密传输到目标设备,应用无需关心网络细节。
  3. 接收端(目标设备):
    • 目标设备上应用未启动:onCreate 中通过 launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION 判断本次启动来自接续,从 want.parameters 取出数据恢复状态;
    • 应用已在运行:走 onNewWant,同样按 launchReason 判断。

要让应用参与接续,还需满足两个配置:module.json5 中该 Ability 声明 "continuable": true;源端调用 startAbility 前目标设备与应用需登录同一华为账号、开启多设备协同开关。手动测试时可以在代码里调用 this.context.startAbility() 并通过 startAbilityParameter 携带参数模拟流转验证数据链路。

三、实现步骤

工程结构:

entry/src/main/ets/
  entryability/EntryAbility.ets
  pages/Index.ets
1. 配置 module.json5

在 module.json5 的 abilities 中声明 continuable,并添加分布式数据所需权限:

{
  "module": {
    "requestPermissions": [
      { "name": "ohos.permission.DISTRIBUTED_DATASYNC" },
      { "name": "ohos.permission.KEEP_BACKGROUND_RUNNING" }
    ],
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "exported": true,
        "continuable": true,
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "skills": [
          { "entities": ["entity.system.home"], "actions": ["action.system.home"] }
        ]
      }
    ]
  }
}

DISTRIBUTED_DATASYNC 是用户授权类权限,首次流转前需在界面里调用 requestPermissionsFromUser 申请。

2. EntryAbility:保存与恢复接续数据
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { readContinuationData, saveContinuationData } from '../common/ContinuationStore';

const TAG = 'ContinuationDemo';

export default class EntryAbility extends UIAbility {
  // 目标设备:应用冷启动,启动原因 = 接续
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
      const page = want.parameters?.['page'] as string ?? 'Index';
      const position = Number(want.parameters?.['position'] ?? 0);
      saveContinuationData(page, position);
      hilog.info(0, TAG, 'onCreate by continuation: %{public}s %{public}d', page, position);
    }
  }

  // 目标设备:应用热启动,接续走这里
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
      const page = want.parameters?.['page'] as string ?? 'Index';
      const position = Number(want.parameters?.['position'] ?? 0);
      saveContinuationData(page, position);
      hilog.info(0, TAG, 'onNewWant by continuation: %{public}s %{public}d', page, position);
    }
  }

  // 源设备:系统触发流转时保存状态,wantParam 会整体搬到对端
  onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
    const page = readContinuationData().page;
    const position = readContinuationData().position;
    wantParam['page'] = page;
    wantParam['position'] = position;
    hilog.info(0, TAG, 'onContinue: %{public}s %{public}d', page, position);
    return AbilityConstant.OnContinueResult.AGREE;
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0, TAG, 'loadContent failed: %{public}s', JSON.stringify(err));
        return;
      }
    });
  }
}

关键点:onContinue 的返回值用 AbilityConstant.OnContinueResult.AGREE 表示同意接续;返回 REJECT 或 MISMATCH(版本不匹配)则系统取消流转。launchReason 判断是接续与普通启动共用一个 Want 的唯一区分手段。

3. 全局状态存储 ContinuationStore

接续数据从 Ability 传到页面需要一个应用级桥,这里用 AppStorage:

// common/ContinuationStore.ets
export interface ContinuationData {
  page: string;
  position: number;
}

export function saveContinuationData(page: string, position: number): void {
  AppStorage.setOrCreate('continuePage', page);
  AppStorage.setOrCreate('continuePosition', position);
}

export function readContinuationData(): ContinuationData {
  return {
    page: AppStorage.get<string>('continuePage') ?? 'Index',
    position: AppStorage.get<number>('continuePosition') ?? 0
  };
}
4. 页面:还原进度并回写
// pages/Index.ets
import { readContinuationData } from '../common/ContinuationStore';

@Entry
@Component
struct Index {
  @StorageLink('continuePosition') position: number = 0;
  private scroller: Scroller = new Scroller();

  build() {
    Column() {
      Text('阅读进度接续示例')
        .fontSize(22)
        .margin(12)
      List({ scroller: this.scroller, initialIndex: this.position }) {
        ForEach(Array.from({ length: 50 }, (v, i) => i), (item: number) => {
          ListItem() {
            Text(`章节 ${item + 1}`)
              .fontSize(18)
              .padding(16)
          }
        }, (item: number) => item.toString())
      }
      .layoutWeight(1)
      .onScrollIndex((first: number) => {
        // 随滚动持续更新进度,onContinue 时取到的就是最新值
        this.position = first;
      })
    }
    .width('100%')
    .height('100%')
  }
}

List 的 initialIndex 在组件创建时生效,正好承接冷启动接续场景;热启动接续时改为调用 scroller.scrollToIndex(this.position) 即可。

四、应用场景示例

除页面进度外,onContinue 常用于迁移表单与播放状态。例如视频应用接续时把播放进度和暂停状态搬过去:

onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
  wantParam['videoUrl'] = this.currentVideo.url;
  wantParam['playTime'] = Math.floor(this.player.currentTime); // 秒
  wantParam['isPlaying'] = this.player.state === 'playing';
  return AbilityConstant.OnContinueResult.AGREE;
}

接收端在 onCreate/onNewWant 中读出后,创建播放器并 seek 到对应时间。

五、技术要点
  • 数据上限:wantParam 整体不超过 1MB,大对象(如列表全量数据)只传 ID 或索引,数据本体走分布式数据对象或网络接口拉取。
  • 权限申请:发起流转前申请 DISTRIBUTED_DATASYNC,被拒绝时用 requestPermissionsFromUser 的返回结果引导用户去设置开启:
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';

const permissions: Array<Permissions> = ['ohos.permission.DISTRIBUTED_DATASYNC'];
const atManager = abilityAccessCtrl.createAtManager();
const result = await atManager.requestPermissionsFromUser(this.context, permissions);
if (result.authResults[0] !== 0) {
  // 提示用户到设置中开启多设备协同授权
}
  • 版本兼容:源端与目标端应用版本不一致时返回 MISMATCH;发布新版本后应保证 onContinue 写入的键对旧版本可缺省解析,避免接续后页面崩溃。
  • 错误处理:onContinue 内不要做耗时操作,序列化控制在毫秒级;对可能失败的数据组装包 try/catch,失败时降级为只传页面路径:
try {
  wantParam['detail'] = JSON.stringify(this.detailState);
} catch (e) {
  wantParam['page'] = 'Index';
}
六、扩展建议

需要实时双向同步(而不是一次性迁移)时,用分布式数据对象 distributedDataObject:两端创建同一 sessionId 的对象后,任一端修改属性会自动同步到另一端,适合协同编辑、多端购物车等场景:

import { distributedDataObject } from '@kit.ArkData';

let obj: distributedDataObject.ObjectType =
  distributedDataObject.create(getContext(), { cart: [] as object[] });
obj.setSessionId('cart-session'); // 两端 set 相同 sessionId 即自动组网同步

进一步可结合 onContinue(迁移导航栈)与分布式数据对象(同步业务数据),实现完整的跨设备工作流。

结语

应用接续的落地成本很低:一个 continuable 配置、一个 onContinue 回调、一处 launchReason 判断,合计不足百行代码,就能让任务在手机、平板、鸿蒙电脑之间无缝续接。随着互助通信共享等组网能力在更多机型下放,这条能力的实用价值会持续放大,建议交互密集型应用尽早接入。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐