HarmonyOS实战(源码教学篇)— 深入浅出鸿蒙特性【跨端迁移-应用接续】

HarmonyOS实战(源码教学篇)--- 深入浅出鸿蒙特性【跨端迁移-应用接续】

  • 深入浅出鸿蒙特性【跨端迁移-应用接续】
    • 前言
    • 案例概述
    • 核心功能实现
      • [1. 应用接续(Application Continuation)原理介绍](#1. 应用接续(Application Continuation)原理介绍)
        • [1.1 启用应用接续能力&数据模型定义](#1.1 启用应用接续能力&数据模型定义)
        • [1.2 应用接续发起端实现](#1.2 应用接续发起端实现)
        • [1.3 应用接续接收端实现](#1.3 应用接续接收端实现)
    • 章节学习整理
      • [💡 实际应用场景](#💡 实际应用场景)
      • [🚀 快速入门建议](#🚀 快速入门建议)
      • [📝 代码要点总结](#📝 代码要点总结)
      • [🌟 写在最后](#🌟 写在最后)

深入浅出鸿蒙特性【跨端迁移-应用接续】

前言

大家好!今天我要给大家分享一个非常酷的HarmonyOS特性------应用接续与跨设备互通。想象一下,你正在手机上编辑一篇图文并茂的游记,突然想在平板上继续编辑,或者想把内容分享给朋友的设备查看。传统做法可能是:保存、发送、打开、重新编辑......但在HarmonyOS的世界里,这一切都可以无缝衔接!

🎯 应用接续的使用场景有哪些?

比如在用户使用过程中,使用情景发生了变化,之前使用的设备不再适合继续当前任务,或者周围有更合适的设备,此时用户可以选择使用新的设备来继续当前的任务。接续完成后,之前设备的应用可退出或保留,用户可以将注意力集中在被拉起的设备上,继续执行任务。

简单说,就是让你的应用能在不同设备间"跳来跳去",就像接力赛一样。比如:

  • 手机上写到一半的文档,点击一下就能在平板上继续写

  • 设备A上的图片,拖拽一下就能传到设备B上使用

  • 多台设备上的应用状态实时同步,始终保持最新

案例概述

接下来我将用一篇系列文章,为大家演示一个基于 HarmonyOS 的内容发布应用如何实现,如何自己动手开发应用接续分布式数据对象分布式文件系统跨设备互通 等核心功能,实现文本和图片数据在多设备间的无缝交互。Demo源码地址

核心功能

  • 📱 应用接续:在不同设备间无缝切换应用
  • 🖼️ 跨设备图片传输:支持拖拽、复制粘贴图片
  • 📝 跨设备文本编辑:支持拖拽、复制粘贴文本
  • 📍 位置信息共享:获取并共享地理位置信息
  • 🔄 分布式数据同步:实时同步应用状态

目录结构

复制代码
entry/src/main/ets/
├── constants/              # 常量定义
│   ├── BreakpointConstants.ets    # 断点常量(响应式布局)
│   └── CommonConstants.ets        # 公共常量
├── entryability/          # 应用入口
│   └── EntryAbility.ets           # 应用生命周期管理
├── model/                 # 数据模型
│   └── ContentInfo.ets            # 内容信息模型
├── pages/                 # 页面
│   └── Home.ets                   # 主页面
├── utils/                 # 工具类
│   ├── BreakpointSystem.ets       # 响应式断点系统
│   ├── FileUtil.ets               # 文件选择工具
│   └── LocationUtil.ets           # 位置服务工具
└── view/                  # 视图组件
    ├── AddPic.ets                 # 图片添加组件
    ├── BottomToolbar.ets          # 底部工具栏
    ├── EditorComponent.ets        # 文本编辑组件
    └── TopArea.ets                # 顶部区域组件

核心功能实现

1. 应用接续(Application Continuation)原理介绍

应用接续允许应用在不同设备间无缝迁移,用户可以在一台设备上开始任务,然后在另一台设备上继续。

在源端,通过UIAbility的onContinue()回调,开发者可以保存待接续的业务数据。

例如,浏览器应用完成应用接续,在源端浏览一个页面,到对端继续浏览。系统将自动保存页面状态,如当前页面的浏览进度;开发者需要通过onContinue接口保存页面url等业务内容。

分布式框架提供跨设备应用界面、页面栈以及业务数据的保存和恢复机制,负责将数据从源端发送到对端。

在对端,同一UIAbility通过onCreate/onNewWant接口恢复业务数据。

1.1 启用应用接续能力&数据模型定义

启用应用接续能力。

在module.json5文件的abilities中,将continuable标签配置为"true",表示该UIAbility可被迁移。配置为false的UIAbility将被系统识别为无法迁移且该配置默认值为false。

复制代码
{
  "module": {
    ...
    "abilities": [
      {
        ...
        "continuable": true,
      }
    ]
  }
}

根据需要配置应用启动模式类型,配置详情请参照UIAbility组件启动模式。

数据模型定义

typescript 复制代码
// entry/src/main/ets/model/ContentInfo.ets
import { commonType } from '@kit.ArkData';

/**
 * 内容信息数据模型
 * 用于在设备间传输应用状态
 */
export class ContentInfo {
  mainTitle: string | undefined;              // 标题
  textContent: string | undefined;            // 正文内容
  imageUriArray: Array<ImageInfo> | undefined; // 图片数组
  isShowLocalInfo: boolean | undefined;       // 是否显示位置信息
  isAddLocalInfo: boolean | undefined;        // 是否添加位置信息
  selectLocalInfo: string | undefined;        // 选中的位置信息
  attachments: commonType.Assets | undefined; // 附件(分布式文件)

  constructor(
    mainTitle: string | undefined,
    textContent: string | undefined,
    imageUriArray: Array<ImageInfo> | undefined,
    isShowLocalInfo: boolean | undefined,
    isAddLocalInfo: boolean | undefined,
    selectLocalInfo: string | undefined,
    attachments: commonType.Assets | undefined
  ) {
    this.mainTitle = mainTitle;
    this.textContent = textContent;
    this.imageUriArray = imageUriArray;
    this.isShowLocalInfo = isShowLocalInfo;
    this.isAddLocalInfo = isAddLocalInfo;
    this.selectLocalInfo = selectLocalInfo;
    this.attachments = attachments;
  }

  /**
   * 将附件数组展平为对象属性
   * 用于分布式数据对象的序列化
   */
  flatAssets(): object {
    let obj: object = this;
    if (!this.attachments) {
      return obj;
    }
    // 将数组元素转换为对象属性:attachments0, attachments1, ...
    for (let i = 0; i < this.attachments.length; i++) {
      obj[`attachments${i}`] = this.attachments[i];
    }
    return obj;
  }
}

/**
 * 图片信息接口
 */
export interface ImageInfo {
  imagePixelMap: PixelMap;  // 图片的像素映射
  imageName: string;         // 图片名称
}
1.2 应用接续发起端实现

在应用迁移启动时,无论是冷启动还是热启动,都会在执行完onCreate()/onNewWant()后,触发onWindowStageRestore()生命周期函数,不执行onWindowStageCreate()生命周期函数。

在开发阶段,如果在onWindowStageCreate()中进行了一些应用启动时必要的初始化,那么迁移后需要在onWindowStageRestore()中执行同样的初始化操作,避免应用异常。

官方的介绍是在源端UIAbility中实现onContinue()接口。

当应用触发迁移时,onContinue()接口在源端被调用,开发者可以在该接口中保存迁移数据,实现应用兼容性检测,决定是否支持此次迁移。

保存迁移数据:开发者可以将要迁移的数据通过键值对的方式保存在wantParam中。

(可选)检测应用兼容性:开发者可以在触发迁移时从onContinue()入参wantParam.version获取到迁移对端应用的版本号,与迁移源端应用版本号做兼容校验。应用在校验版本兼容性失败后,需要提示用户迁移失败的原因。
如果迁移过程中的兼容性问题对于应用迁移体验影响较小或无影响,可以跳过该步骤。

返回迁移结果:开发者可以通过onContinue()回调的返回值决定是否支持此次迁移,接口返回值详见AbilityConstant.OnContinueResult

onContinue()接口传入的wantParam参数中,有部分字段由系统预置,开发者可以使用这些字段用于业务处理。同时,应用在保存自己的wantParam参数时,也应注意不要使用同样的key值,避免被系统覆盖导致数据获取异常。详见下表:

字段 含义
version 对端应用的版本号
targetDevice 对端设备的networkId

下面给出我们的完整实现,细节在注释内补充。

复制代码
设备A(发起端)                    设备B(接收端)
    |                                  |
    | 1. 用户编辑内容                  |
    |    (标题、正文、图片)            |
    |                                  |
    | 2. 点击 Dock 栏应用图标          |
    |    触发 onContinue()             |
    |                                  |
    | 3. 创建分布式数据对象            |
    |    生成 sessionId                |
    |                                  |
    | 4. 保存数据到对端                |
    |    save(targetDevice)            |
    |                                  |
    |--------------------------------->| 5. 接收到接续请求
    |                                  |    触发 onCreate/onNewWant
    |                                  |
    |                                  | 6. 恢复分布式数据对象
    |                                  |    setSessionId(sessionId)
    |                                  |
    |                                  | 7. 监听 'restored' 状态
    |                                  |
    |                                  | 8. 恢复应用状态
    |                                  |    - 标题、正文
    |                                  |    - 图片数组
    |                                  |    - 位置信息
    |                                  |
    |                                  | 9. 复制分布式文件到本地
    |                                  |
    |                                  | 10. 显示完整内容
typescript 复制代码
// entry/src/main/ets/entryability/EntryAbility.ets

/**
 * onContinue 生命周期回调
 * 当用户在对端设备点击应用接续图标时触发
 * @param wantParam 传递给对端设备的参数
 * @returns 接续结果(AGREE 表示同意接续)
 */
async onContinue(wantParam: Record<string, Object | undefined>): Promise<AbilityConstant.OnContinueResult> {
  // 1. 生成分布式数据对象的会话ID
  let sessionId: string = distributedDataObject.genSessionId();
  wantParam.distributedSessionId = sessionId;

  // 2. 准备要传输的图片附件
  let imageUriArray = AppStorage.get<Array<ImageInfo>>('imageUriArray');
  let assets: commonType.Assets = [];
  if (imageUriArray) {
    for (let i = 0; i < imageUriArray.length; i++) {
      let append = imageUriArray[i];
      // 获取分布式文件的资产信息
      let attachment: commonType.Asset = this.getAssetInfo(append);
      assets.push(attachment);
    }
  }

  // 3. 创建内容信息对象,包含所有应用状态
  let contentInfo: ContentInfo = new ContentInfo(
    AppStorage.get('mainTitle'),        // 标题
    AppStorage.get('textContent'),      // 正文
    AppStorage.get('imageUriArray'),    // 图片数组
    AppStorage.get('isShowLocalInfo'),  // 位置信息显示状态
    AppStorage.get('isAddLocalInfo'),   // 位置信息添加状态
    AppStorage.get('selectLocalInfo'),  // 选中的位置
    assets                               // 图片附件
  );

  // 4. 创建分布式数据对象
  let source = contentInfo.flatAssets();
  this.distributedObject = distributedDataObject.create(this.context, source);
  
  // 5. 设置会话ID,建立与对端设备的连接
  this.distributedObject.setSessionId(sessionId);
  
  // 6. 保存数据到对端设备
  await this.distributedObject.save(wantParam.targetDevice as string);
  
  // 7. 返回同意接续
  return AbilityConstant.OnContinueResult.AGREE;
}

/**
 * 获取分布式文件的资产信息
 * @param append 图片信息
 * @returns 资产对象
 */
private getAssetInfo(append: ImageInfo): commonType.Asset {
  // 分布式文件路径
  let filePath = this.context.distributedFilesDir + '/' + append.imageName;
  let attachment: commonType.Asset;
  
  try {
    // 获取文件统计信息
    let stat = fs.statSync(filePath);
    let uri: string = fileUri.getUriFromPath(filePath);
    
    // 构建资产对象
    attachment = {
      name: append.imageName,
      uri: uri,
      path: filePath,
      createTime: stat.ctime.toString(),
      modifyTime: stat.ctime.toString(),
      size: stat.size.toString()
    };
  } catch (err) {
    hilog.error(DOMAIN, TAG, FORMAT, `StatSync failed: ${err.message}`);
  }
  
  return attachment!;
}
1.3 应用接续接收端实现

在目的端设UIAbility中实现onCreate()与onNewWant()接口,恢复迁移数据。

目的端设备上,在onCreate中根据launchReason判断该次启动是否为迁移LaunchReason.CONTINUATION。下面贴出我们的完整实例。

typescript 复制代码
// entry/src/main/ets/entryability/EntryAbility.ets

/**
 * onCreate 生命周期回调
 * 应用启动时调用,检查是否为接续启动
 */
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  hilog.info(DOMAIN, TAG, FORMAT, 'Ability onCreate');
  // 恢复分布式数据对象
  this.restoreDistributedObject(want, launchParam);
}

/**
 * onNewWant 生命周期回调
 * 应用已启动时再次被拉起时调用
 */
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  hilog.info(DOMAIN, TAG, FORMAT, 'Ability onNewWant');
  // 恢复分布式数据对象
  this.restoreDistributedObject(want, launchParam);
}

/**
 * 恢复分布式数据对象
 * 对端设备接收数据的核心逻辑
 */
async restoreDistributedObject(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
  // 1. 检查启动原因是否为应用接续
  if (launchParam.launchReason !== AbilityConstant.LaunchReason.CONTINUATION) {
    return;
  }

  // 2. 创建空的内容信息对象
  let mailInfo: ContentInfo = new ContentInfo(
    undefined, undefined, [], undefined, undefined, undefined, undefined
  );
  this.distributedObject = distributedDataObject.create(this.context, mailInfo);

  // 3. 监听数据恢复状态
  try {
    this.distributedObject.on('status',
      (sessionId: string, networkId: string, status: 'online' | 'offline' | 'restored') => {
        hilog.info(DOMAIN, TAG, FORMAT, `Status changed: ${status}`);
        
        // 当状态为 'restored' 时,表示数据已恢复
        if (status === 'restored') {
          if (!this.distributedObject) {
            return;
          }
          
          // 4. 将恢复的数据存储到 AppStorage
          AppStorage.setOrCreate('mainTitle', this.distributedObject['mainTitle']);
          AppStorage.setOrCreate('textContent', this.distributedObject['textContent']);
          AppStorage.setOrCreate('isShowLocalInfo', this.distributedObject['isShowLocalInfo']);
          AppStorage.setOrCreate('isAddLocalInfo', this.distributedObject['isAddLocalInfo']);
          AppStorage.setOrCreate('selectLocalInfo', this.distributedObject['selectLocalInfo']);
          AppStorage.setOrCreate('attachments', this.distributedObject['attachments']);
          
          // 5. 处理图片附件
          let attachments = this.distributedObject['attachments'] as commonType.Assets;
          for (const attachment of attachments) {
            // 复制分布式文件到本地
            this.fileCopy(attachment);
          }
          
          // 6. 更新图片数组
          AppStorage.setOrCreate<Array<ImageInfo>>('imageUriArray', this.imageUriArray);
        }
      });
  } catch (err) {
    hilog.error(DOMAIN, TAG, FORMAT, `On status failed: ${err.message}`);
  }

  // 7. 设置会话ID,连接到发起端的分布式数据对象
  let sessionId: string = want.parameters?.distributedSessionId as string;
  this.distributedObject.setSessionId(sessionId);

  // 8. 恢复窗口状态
  try {
    this.context.restoreWindowStage(new LocalStorage());
  } catch (err) {
    hilog.error(DOMAIN, TAG, FORMAT, `RestoreWindowStage failed: ${err.message}`);
  }
}

/**
 * 复制分布式文件到本地
 * @param attachment 附件信息
 */
private fileCopy(attachment: commonType.Asset): void {
  // 1. 分布式文件路径
  let filePath: string = this.context.distributedFilesDir + '/' + attachment.name;
  // 2. 本地保存路径
  let savePath: string = this.context.filesDir + '/' + attachment.name;
  
  try {
    // 3. 检查分布式文件是否存在
    if (fs.accessSync(filePath)) {
      // 4. 打开文件
      let saveFile = fs.openSync(savePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
      let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE);
      
      // 5. 读取文件内容
      let buf: ArrayBuffer = new ArrayBuffer(Number(attachment.size));
      let readSize = 0;
      let readLen = fs.readSync(file.fd, buf, { offset: readSize });
      
      // 6. 创建图片源
      let sourceOptions: image.SourceOptions = { sourceDensity: 120 };
      let imageSourceApi: image.ImageSource = image.createImageSource(buf, sourceOptions);
      
      // 7. 添加到图片数组
      this.imageUriArray.push({
        imagePixelMap: imageSourceApi.createPixelMapSync(),
        imageName: attachment.name
      });
      
      // 8. 写入本地文件
      while (readLen > 0) {
        readSize += readLen;
        fs.writeSync(saveFile.fd, buf);
        readLen = fs.readSync(file.fd, buf, { offset: readSize });
      }
      
      // 9. 关闭文件
      fs.closeSync(file);
      fs.closeSync(saveFile);
      
      hilog.info(DOMAIN, TAG, FORMAT, attachment.name + ' synchronized successfully.');
    }
  } catch (error) {
    hilog.error(DOMAIN, TAG, FORMAT, `File copy failed: ${error.message}`);
  }
}

动态配置迁移能力

从API version 10起,提供了支持动态配置迁移能力的功能。即应用可以根据实际使用场景,在需要迁移功能时,设置开启应用迁移能力;在业务不需要迁移时,则可以关闭迁移能力。开发者可以通过调用setMissionContinueState接口对迁移能力进行设置。

章节学习整理

💡 实际应用场景

这个技术可以用在很多地方:

写作应用:手机写提纲 → 平板详细写 → 电脑排版

购物应用:手机选商品 → 平板看详情 → 电脑下单

游戏应用:手机开始玩 → 平板大屏玩 → 电视全家玩

协作应用:多设备实时同步编辑文档

🚀 快速入门建议

如果你也想在自己的应用中实现这个功能:

1.先理解基础:

分布式数据对象:设备间同步数据的桥梁

应用生命周期:onContinue、onCreate、onNewWant是关键

文件系统:分布式文件与本地文件的区别

2.分步实现:

第一步:先实现简单的文本同步

第二步:加入图片等文件传输

第三步:优化用户体验(加载提示、错误处理等)

3.调试技巧:

使用hilog打印详细日志

先用模拟器测试单设备场景,再使用真机验证多设备场景

注意权限配置(文件访问、位置服务等)

📝 代码要点总结

关键点 作用 注意事项
distributedDataObject 设备间数据同步 需要设置SessionID
onContinue 发起接续 返回AGREE表示同意
flatAssets() 序列化附件 数组需要展平
restoreWindowStage 恢复窗口状态 确保UI正确显示

🌟 写在最后

HarmonyOS的跨设备能力真的非常强大,它不仅仅是技术上的创新,更是用户体验的巨大提升。想象一下未来,我们的应用可以真正做到"无处不在、无缝流转",用户不再被设备束缚,这是多么美妙的场景!

如果你对这部分内容感兴趣,我建议:

下载我们的Demo项目亲手试试,评论区可以留言交流。

阅读HarmonyOS官方文档的分布式章节。

加入开发者社区,和大家一起交流。

技术不是冰冷的代码,而是连接人与设备、设备与设备的温暖桥梁。 让我们一起,用代码创造更智能、更便捷的数字生活!下一章节,我将继续为大家介绍并动手实践分布式数据对象实践。

相关推荐
智慧化智能化数字化方案2 小时前
华为项目管理——解读华为客户重大项目管理流程概述【附全文阅读】
华为·华为项目管理·华为产品战略规划·华为客户重大项目管理流程·华为项目管理高级培训·华为研发pmo的能力·战略级项目管理
哈哈你是真的厉害2 小时前
小白基础入门 React Native 鸿蒙跨平台开发:AnimatedSpring 弹簧动画
react native·react.js·harmonyos
哈哈你是真的厉害2 小时前
基础入门 React Native 鸿蒙跨平台开发:颜色选择器工具
react native·react.js·harmonyos
不爱吃糖的程序媛3 小时前
Flutter-OH生态再升级:兼容库数量翻倍,全面支持Flutter 3.27,聚焦开发者体验
华为·harmonyos
摘星编程3 小时前
OpenHarmony环境下React Native:Easing.bounce弹跳效果
react native·华为·harmonyos
猛扇赵四那边好嘴.3 小时前
Flutter 框架跨平台鸿蒙开发 - 书籍借阅管理器应用开发教程
flutter·华为·harmonyos
夜雨声烦丿3 小时前
Flutter 框架跨平台鸿蒙开发 - 日期计算器应用开发教程
flutter·华为·harmonyos
小雨青年4 小时前
鸿蒙 HarmonyOS 6 | 系统能力 (02):文件管理基石 应用沙箱机制与文件 IO 深度解析
华为·harmonyos
AI_零食4 小时前
鸿蒙的flutter框架表达:生命律动系统
学习·flutter·ui·华为·harmonyos·鸿蒙