1# 分布式数据对象跨设备数据同步 2 3 4## 场景介绍 5 6传统方式下,设备之间的数据同步,需要开发者完成消息处理逻辑,包括:建立通信链接、消息收发处理、错误重试、数据冲突解决等操作,工作量非常大。而且设备越多,调试复杂度也将同步增加。 7 8其实设备之间的状态、消息发送进度、发送的数据等都是“变量”。如果这些变量支持“全局”访问,那么开发者跨设备访问这些变量就能像操作本地变量一样,从而能够自动高效、便捷地实现数据多端同步。 9 10分布式数据对象即实现了对“变量”的“全局”访问。向应用开发者提供内存对象的创建、查询、删除、修改、订阅等基本数据对象的管理能力,同时具备分布式能力。为开发者在分布式应用场景下提供简单易用的JS接口,轻松实现多设备间同应用的数据协同,同时设备间可以监听对象的状态和数据变更。满足超级终端场景下,相同应用多设备间的数据对象协同需求。与传统方式相比,分布式数据对象大大减少了开发者的工作量。 11 12目前<!--RP2-->分布式数据对象只能在[跨端迁移](../application-models/hop-cross-device-migration.md)和[通过跨设备Call调用实现的多端协同](../application-models/hop-multi-device-collaboration.md#通过跨设备call调用实现多端协同)场景中使用。<!--RP2End--> 13 14## 基本概念 15 16- **分布式内存数据库**:分布式内存数据库将数据缓存在内存中,以便应用获得更快的数据存取速度,不会将数据进行持久化。若数据库关闭,则数据不会保留。 17 18- **分布式数据对象**:分布式数据对象是一个JS对象型的封装。每一个分布式数据对象实例会创建一个内存数据库中的数据表,每个应用程序创建的内存数据库相互隔离,对分布式数据对象的“读取”或“赋值”会自动映射到对应数据库的get/put操作。 19 20 分布式数据对象的生命周期包括以下状态: 21 22 - 未初始化:未实例化,或已被销毁。 23 - 本地数据对象:已创建对应的数据表,但是还无法进行数据同步。 24 - 分布式数据对象:已创建对应的数据表,设备在线且组网内设置同样sessionId的对象数>=2,可以跨设备同步数据。若设备掉线或将sessionId置为空,分布式数据对象退化为本地数据对象。 25 26 27## 运作机制 28 29**图1** 分布式数据对象运作机制 30 31 32 33分布式数据对象生长在分布式内存数据库之上,在分布式内存数据库上进行了JS对象型的封装,能像操作本地变量一样操作分布式数据对象,数据的跨设备同步由系统自动完成。 34 35 36### JS对象型存储与封装机制 37 38- 为每个分布式数据对象实例创建一个内存数据库,通过SessionId标识,每个应用程序创建的内存数据库相互隔离。 39 40- 在分布式数据对象实例化的时候,(递归)遍历对象所有属性,使用“Object.defineProperty”定义所有属性的set和get方法,set和get中分别对应数据库一条记录的put和get操作,Key对应属性名,Value对应属性值。 41 42- 在开发者对分布式数据对象进行“读取”或者“赋值”的时候,都会自动调用到set和get方法,映射到对应数据库的操作。 43 44**表1** 分布式数据对象和分布式数据库的对应关系 45 46| 分布式对象实例 | 对象实例 | 属性名称 | 属性值 | 47| -------- | -------- | -------- | -------- | 48| 分布式内存数据库 | 一个数据库(sessionID标识) | 一条数据库记录的key | 一条数据库记录的value | 49 50 51### 跨设备同步和数据变更通知机制 52 53分布式数据对象,最重要的功能就是对象之间的数据同步。可信组网内的设备可以在本地创建分布式数据对象,并设置sessionID。不同设备上的分布式数据对象,通过设置相同的sessionID,建立对象之间的同步关系。 54 55如下图所示,设备A和设备B上的“分布式数据对象1”,其sessionID均为session1,这两个对象建立了session1的同步关系。 56 57 **图2** 对象的同步关系 58 59 60 61一个同步关系中,一个设备只能有一个对象加入。比如上图中,设备A的“分布式数据对象1”已经加入了session1的同步关系,所以设备A的“分布式数据对象2”就加入失败了。 62 63建立同步关系后,每个Session有一份共享对象数据。加入了同一个Session的对象,支持以下操作: 64 65(1)读取/修改Session中的数据。 66 67(2)监听数据变更,感知其他设备对共享对象数据的修改。 68 69(3)监听状态变更,感知其他设备的加入和退出。 70 71分布式数据对象加入session时,如果它的数据与session中的数据不同,则它会更新session中的数据。如果希望分布式数据对象加入sessionId时不更新session中的数据,并且得到session中的数据,需要将对象的属性的值设置为undefined(资产类型的属性则是将它的各个属性值设置为空字符串)。 72 73### 同步的最小单位 74 75关于分布式数据对象的数据同步,值得注意的是,同步的最小单位是“属性”。比如,下图中对象1包含三个属性:name、age和parents。当其中一个属性变更时,则数据同步时只需同步此变更的属性。 76 77对象属性支持基本类型(数字类型、布尔类型、字符串类型)以及复杂类型(数组、基本类型嵌套)。针对复杂类型的数据修改,目前仅支持对根属性的修改,暂不支持对下级属性的修改。 78 79```ts 80dataObject['parents'] = {mom: "amy"}; // 支持的修改 81dataObject['parents']['mon'] = "amy"; // 不支持的修改 82``` 83 84**图3** 数据同步视图 85 86 87 88 89 90### 对象持久化缓存机制 91 92分布式对象主要运行在应用程序的进程空间。当调用分布式对象持久化接口时,通过分布式数据库对对象进行持久化和同步,进程退出后数据也不会丢失。 93 94该场景是分布式对象的扩展场景,主要用于以下情况: 95 96- 在设备上创建持久化对象后APP退出,重新打开APP,创建持久化对象,加入同一个Session,数据可以恢复到APP退出前的数据。 97 98- 在设备A上创建持久化对象并同步后持久化到设备B后,A设备的APP退出,设备B打开APP,创建持久化对象,加入同一个Session,数据可以恢复到A设备退出前的数据。 99 100### 资产同步机制 101 102在分布式对象中,可以使用[资产类型](../reference/apis-arkdata/js-apis-data-commonType.md#asset)来描述本地实体资产文件,分布式对象跨设备同步时,该文件会和数据一起同步到其他设备上。当前只支持资产类型,不支持[资产类型数组](../reference/apis-arkdata/js-apis-data-commonType.md#assets)。如需同步多个资产,可将每个资产作为分布式对象的一个根属性实现。 103 104## 约束限制 105 106- 目前<!--RP2-->分布式数据对象只能在[跨端迁移](../application-models/hop-cross-device-migration.md)和[通过跨设备Call调用实现的多端协同](../application-models/hop-multi-device-collaboration.md#通过跨设备call调用实现多端协同)场景中使用。<!--RP2End--> 107 108- 不同设备间只有相同bundleName的应用才能直接同步。 109 110- 分布式数据对象的数据同步发生在同一个应用程序下,且同sessionID之间。 111 112- 不建议创建过多的分布式数据对象,每个分布式数据对象将占用100-150KB内存。 113 114- 每个分布式数据对象大小不超过500KB。 115 116- 设备A修改1KB数据,设备B收到变更通知,50ms内完成。 117 118- 单个应用程序最多只能创建16个分布式数据对象实例。 119 120- 考虑到性能和用户体验,最多不超过3个设备进行数据协同。 121 122- 如对复杂类型的数据进行修改,仅支持修改根属性,暂不支持下级属性修改。[资产同步机制](#资产同步机制)中,资产类型的数据支持下一级属性修改。 123 124- 支持JS接口间的互通,与其他语言不互通。 125 126## 接口说明 127 128以下是分布式对象跨设备数据同步功能的相关接口,大部分为异步接口。异步接口均有callback和Promise两种返回形式,下表均以callback形式为例,更多接口及使用方式请见[分布式数据对象](../reference/apis-arkdata/js-apis-data-distributedobject.md)。 129 130 131 132| 接口名称 | 描述 | 133| -------- | -------- | 134| create(context: Context, source: object): DataObject | 创建并得到一个分布式数据对象实例。 | 135| genSessionId(): string | 创建一个sessionId,可作为分布式数据对象的sessionId。 | 136| setSessionId(sessionId: string, callback: AsyncCallback<void>): void | 设置同步的sessionId,当可信组网中有多个设备时,多个设备间的对象如果设置为同一个sessionId,就能自动同步。 | 137| setSessionId(callback: AsyncCallback<void>): void | 退出所有已加入的session。 | 138| on(type: 'change', callback: (sessionId: string, fields: Array<string>) => void): void | 监听分布式数据对象的数据变更。 | 139| off(type: 'change', callback?: (sessionId: string, fields: Array<string>) => void): void | 取消监听分布式数据对象的数据变更。 | 140| on(type: 'status', callback: (sessionId: string, networkId: string, status: 'online' \| 'offline' ) => void): void | 监听分布式数据对象的上下线。 | 141| off(type: 'status', callback?: (sessionId: string, networkId: string, status: 'online' \|'offline' ) => void): void | 取消监听分布式数据对象的上下线。 | 142| save(deviceId: string, callback: AsyncCallback<SaveSuccessResponse>): void | 保存分布式数据对象。 | 143| revokeSave(callback: AsyncCallback<RevokeSaveSuccessResponse>): void | 撤回保存的分布式数据对象。 | 144| bindAssetStore(assetKey: string, bindInfo: BindInfo, callback: AsyncCallback<void>): void | 绑定融合资产。 | 145 146 147## 开发步骤 148 149### 在跨端迁移中使用分布式数据对象迁移数据 150 1511. 迁移发起端在onContinue接口中创建分布式数据对象并保存数据到接收端: 152 153 1.1 调用create接口创建并得到一个分布式数据对象实例。 154 155 1.2 调用genSessionId接口创建一个sessionId,调用setSessionId接口设置同步的sessionId,并将这个sessionId放入wantParam。 156 157 1.3 从wantParam获取接收端设备networkId,使用这个networkId调用save接口保存数据到接收端。 158 1592. 接收端在onCreate和onNewWant接口中创建分布式数据对象并注册恢复状态监听: 160 161 2.1 调用create接口创建并得到一个分布式数据对象实例。 162 163 2.2 注册恢复状态监听。收到状态为'restored'的回调通知时,表示接收端分布式数据对象已恢复发起端保存过来的数据。 164 165 2.3 从want.parameters中获取发起端放入的sessionId,调用setSessionId接口设置同步的sessionId。 166 167> **说明:** 168> 169> - 跨端迁移时,在迁移发起端调用setsessionId接口设置同步的sessionId后,必须再调用save接口保存数据到接收端。 170> 171<!--RP1--> 172> - 跨端迁移需要配置`continuable`标签,详见[跨端迁移开发步骤](../application-models/hop-cross-device-migration.md#开发步骤)。<!--RP1End--> 173> 174> - wantParam中的"sessionId"字段可能被其他服务占用,建议自定义一个key存取sessionId。 175> 176> - 可以使用资产类型记录资产附件(文件、图片、视频等类型文件)的相关信息,迁移资产类型数据时,对应的资产附件会一起迁移到对端。 177> 178> - 接收端需要将业务数据的初始值设置为undefined,才能恢复发起端保存的数据,否则接收端的数据会覆盖同步到发起端。如果是资产数据,需要将资产数据的各个属性设置为空字符串而不是将整个资产数据设置为undefined。 179> 180> - 暂不支持资产类型数组,如果要迁移多个文件,在业务数据中定义多条资产数据来记录。 181> 182> - 目前仅支持迁移分布式文件目录下的文件,非分布式文件目录下的文件可以复制或移动到分布式文件目录下再进行迁移。文件的操作和URI的获取详见[文件管理](../reference/apis-core-file-kit/js-apis-file-fs.md)和[文件URI](../reference/apis-core-file-kit/js-apis-file-fileuri.md)。 183 184```ts 185import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; 186import { hilog } from '@kit.PerformanceAnalysisKit'; 187import { window } from '@kit.ArkUI'; 188import { commonType, distributedDataObject } from '@kit.ArkData'; 189import { fileIo, fileUri } from '@kit.CoreFileKit'; 190import { BusinessError } from '@kit.BasicServicesKit'; 191 192// 业务数据定义 193class Data { 194 title: string | undefined; 195 text: string | undefined; 196 attachment: commonType.Asset; // 可以使用资产类型记录分布式目录下的文件,迁移资产数据时,对应的文件会一起迁移到接收端。(不迁移文件时不需要此字段,下方代码中的createAttachment、createEmptyAttachment方法也都不需要。) 197 // attachment2: commonType.Asset; // 暂不支持资产类型数组,如果要迁移多个文件,在业务数据中定义多条资产数据来记录 198 199 constructor(title: string | undefined, text: string | undefined, attachment: commonType.Asset) { 200 this.title = title; 201 this.text = text; 202 this.attachment = attachment; 203 } 204} 205 206const TAG = '[DistributedDataObject]'; 207let dataObject: distributedDataObject.DataObject; 208 209export default class EntryAbility extends UIAbility { 210 // 1. 迁移发起端在onContinue接口中创建分布式数据对象并保存数据到接收端 211 onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult | Promise<AbilityConstant.OnContinueResult> { 212 // 1.1 调用create接口创建并得到一个分布式数据对象实例 213 let attachment = this.createAttachment(); 214 let data = new Data('The title', 'The text', attachment); 215 dataObject = distributedDataObject.create(this.context, data); 216 217 // 1.2 调用genSessionId接口创建一个sessionId,调用setSessionId接口设置同步的sessionId,并将这个sessionId放入wantParam 218 let sessionId = distributedDataObject.genSessionId(); 219 console.log(TAG + `gen sessionId: ${sessionId}`); 220 dataObject.setSessionId(sessionId); 221 wantParam.distributedSessionId = sessionId; 222 223 // 1.3 从wantParam获取接收端设备networkId,使用这个networkId调用save接口保存数据到接收端 224 let deviceId = wantParam.targetDevice as string; 225 console.log(TAG + `get deviceId: ${deviceId}`); 226 dataObject.save(deviceId); 227 return AbilityConstant.OnContinueResult.AGREE; 228 } 229 230 // 2. 接收端在onCreate和onNewWant接口中创建分布式数据对象并加入组网进行数据恢复 231 onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { 232 if (launchParam.launchReason == AbilityConstant.LaunchReason.CONTINUATION) { 233 if (want.parameters && want.parameters.distributedSessionId) { 234 this.restoreDistributedDataObject(want); 235 } 236 } 237 } 238 239 // 2. 接收端在onCreate和onNewWant接口中创建分布式数据对象并加入组网进行数据恢复 240 onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void { 241 if (launchParam.launchReason == AbilityConstant.LaunchReason.CONTINUATION) { 242 if (want.parameters && want.parameters.distributedSessionId) { 243 this.restoreDistributedDataObject(want); 244 } 245 } 246 } 247 248 restoreDistributedDataObject(want: Want) { 249 if (!want.parameters || !want.parameters.distributedSessionId) { 250 console.error(TAG + 'missing sessionId'); 251 return; 252 } 253 254 // 2.1 调用create接口创建并得到一个分布式数据对象实例 255 let attachment = this.createEmptyAttachment(); // 接收端需要将资产数据的各个属性设置为空字符串,才能恢复发起端保存的资产数据 256 let data = new Data(undefined, undefined, attachment); 257 dataObject = distributedDataObject.create(this.context, data); 258 259 // 2.2 注册恢复状态监听。收到状态为'restored'的回调通知时,表示接收端分布式数据对象已恢复发起端保存过来的数据(有资产数据时,对应的文件也迁移过来了) 260 dataObject.on('status', (sessionId: string, networkId: string, status: string) => { 261 if (status == 'restored') { // 收到'restored'的状态通知表示已恢复发起端保存的数据 262 console.log(TAG + `title: ${dataObject['title']}, text: ${dataObject['text']}`); 263 } 264 }); 265 266 // 2.3 从want.parameters中获取发起端放入的sessionId,调用setSessionId接口设置同步的sessionId 267 let sessionId = want.parameters.distributedSessionId as string; 268 console.log(TAG + `get sessionId: ${sessionId}`); 269 dataObject.setSessionId(sessionId); 270 } 271 272 // 在分布式文件目录下创建一个文件并使用资产类型记录(也可以记录分布式文件目录下已有文件,非分布式文件目录下的文件可以复制或移动到分布式文件目录下再进行记录) 273 createAttachment() { 274 let attachment = this.createEmptyAttachment(); 275 try { 276 let distributedDir: string = this.context.distributedFilesDir; // 分布式文件目录 277 let fileName: string = 'text_attachment.txt'; // 文件名 278 let filePath: string = distributedDir + '/' + fileName; // 文件路径 279 let file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); 280 fileIo.writeSync(file.fd, 'The text in attachment'); 281 fileIo.closeSync(file.fd); 282 let uri: string = fileUri.getUriFromPath(filePath); // 获取文件URI 283 let stat = fileIo.statSync(filePath); // 获取文件详细属性信息 284 285 // 写入资产数据 286 attachment = { 287 name: fileName, 288 uri: uri, 289 path: filePath, 290 createTime: stat.ctime.toString(), 291 modifyTime: stat.mtime.toString(), 292 size: stat.size.toString() 293 } 294 } catch (e) { 295 let err = e as BusinessError; 296 console.error(TAG + `file error, error code: ${err.code}, error message: ${err.message}`); 297 } 298 return attachment; 299 } 300 301 createEmptyAttachment() { 302 let attachment: commonType.Asset = { 303 name: '', 304 uri: '', 305 path: '', 306 createTime: '', 307 modifyTime: '', 308 size: '' 309 } 310 return attachment; 311 } 312} 313``` 314 315### 在多端协同中使用分布式数据对象 316 3171. 调用端调用startAbilityByCall接口拉起对端Ability: 318 319 1.1 调用genSessionId接口创建一个sessionId,通过分布式设备管理接口获取对端设备networkId。 320 321 1.2 组装want,并将sessionId放入want。 322 323 1.3 调用startAbilityByCall接口拉起对端Ability。 324 3252. 调用端拉起对端Ability后创建分布式数据对象并加入组网: 326 327 2.1 创建分布式数据对象实例。 328 329 2.2 注册数据变更监听。 330 331 2.3 设置同步sessionId加入组网。 332 3333. 被调用端被拉起后创建和恢复分布式数据对象: 334 335 3.1 创建分布式数据对象实例。 336 337 3.2 注册数据变更监听。 338 339 3.3 从want中获取源端放入的sessionId,使用这个sessionId加入组网。 340 341> **说明:** 342> 343> - 暂时只支持<!--RP3-->在[跨设备Call调用实现的多端协同](../application-models/hop-multi-device-collaboration.md#通过跨设备call调用实现多端协同)中使用分布式数据对象进行数据同步。<!--RP3End--> 344> 345> - 跨设备Call调用实现的多端协同开发<!--RP4-->需要申请`ohos.permission.DISTRIBUTED_DATASYNC`权限和配置单实例启动标签,详见跨设备Call调用实现的多端协同的[开发步骤](../application-models/hop-multi-device-collaboration.md#通过跨设备call调用实现多端协同)。<!--RP4End--> 346> 347> - wantParam中的"sessionId"字段可能被其他服务占用,建议自定义一个key存取sessionId。 348> 349> - 使用分布式设备管理获取对端设备networkId详见[设备信息查询开发指导](../distributedservice/devicemanager-guidelines.md#设备信息查询开发指导)。 350 351 示例代码如下: 352 353```ts 354import { AbilityConstant, Caller, common, UIAbility, Want } from '@kit.AbilityKit'; 355import { hilog } from '@kit.PerformanceAnalysisKit'; 356import { window } from '@kit.ArkUI'; 357import { distributedDataObject } from '@kit.ArkData'; 358import { distributedDeviceManager } from '@kit.DistributedServiceKit'; 359import { BusinessError } from '@kit.BasicServicesKit'; 360 361// 业务数据定义 362class Data { 363 title: string | undefined; 364 text: string | undefined; 365 366 constructor(title: string | undefined, text: string | undefined) { 367 this.title = title; 368 this.text = text; 369 } 370} 371 372const TAG = '[DistributedDataObject]'; 373 374let sessionId: string; 375let caller: Caller; 376let dataObject: distributedDataObject.DataObject; 377 378export default class EntryAbility extends UIAbility { 379 // 1. 调用端调用startAbilityByCall接口拉起对端Ability 380 callRemote() { 381 if (caller) { 382 console.error(TAG + 'call remote already'); 383 return; 384 } 385 386 // 1.1 调用genSessionId接口创建一个sessionId,通过分布式设备管理接口获取对端设备networkId 387 sessionId = distributedDataObject.genSessionId(); 388 console.log(TAG + `gen sessionId: ${sessionId}`); 389 let deviceId = getRemoteDeviceId(); 390 if (deviceId == "") { 391 console.warn(TAG + 'no remote device'); 392 return; 393 } 394 console.log(TAG + `get remote deviceId: ${deviceId}`); 395 396 // 1.2 组装want,并将sessionId放入want 397 let want: Want = { 398 bundleName: 'com.example.collaboration', 399 abilityName: 'EntryAbility', 400 deviceId: deviceId, 401 parameters: { 402 'ohos.aafwk.param.callAbilityToForeground': true, // 前台启动,非必须 403 'distributedSessionId': sessionId 404 } 405 } 406 try { 407 // 1.3 调用startAbilityByCall接口拉起对端Ability 408 this.context.startAbilityByCall(want).then((res) => { 409 if (!res) { 410 console.error(TAG + 'startAbilityByCall failed'); 411 } 412 caller = res; 413 }) 414 } catch (e) { 415 let err = e as BusinessError; 416 console.error(TAG + `get remote deviceId error, error code: ${err.code}, error message: ${err.message}`); 417 } 418 } 419 420 // 2. 拉起对端Ability后创建分布式数据对象 421 createDataObject() { 422 if (!caller) { 423 console.error(TAG + 'call remote first'); 424 return; 425 } 426 if (dataObject) { 427 console.error(TAG + 'create dataObject already'); 428 return; 429 } 430 431 // 2.1 创建分布式数据对象实例 432 let data = new Data('The title', 'The text'); 433 dataObject = distributedDataObject.create(this.context, data); 434 435 // 2.2 注册数据变更监听 436 dataObject.on('change', (sessionId: string, fields: Array<string>) => { 437 fields.forEach((field) => { 438 console.log(TAG + `${field}: ${dataObject[field]}`); 439 }); 440 }); 441 // 2.3 设置同步sessionId加入组网 442 dataObject.setSessionId(sessionId); 443 } 444 445 // 3. 被调用端被拉起后创建和恢复分布式数据对象 446 onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { 447 if (want.parameters && want.parameters.distributedSessionId) { 448 // 3.1 创建分布式数据对象实例 449 let data = new Data(undefined, undefined); 450 dataObject = distributedDataObject.create(this.context, data); 451 452 // 3.2 注册数据变更监听 453 dataObject.on('change', (sessionId: string, fields: Array<string>) => { 454 fields.forEach((field) => { 455 console.log(TAG + `${field}: ${dataObject[field]}`); 456 }); 457 }); 458 // 3.3 从want中获取源端放入的sessionId,使用这个sessionId加入组网 459 let sessionId = want.parameters.distributedSessionId as string; 460 console.log(TAG + `onCreate get sessionId: ${sessionId}`); 461 dataObject.setSessionId(sessionId); 462 } 463 } 464} 465 466// 获取可信组网中的设备 467function getRemoteDeviceId() { 468 let deviceId = ""; 469 try { 470 let deviceManager = distributedDeviceManager.createDeviceManager('com.example.collaboration'); 471 let devices = deviceManager.getAvailableDeviceListSync(); 472 if (devices[0] && devices[0].networkId) { 473 deviceId = devices[0].networkId; 474 } 475 } catch (e) { 476 let err = e as BusinessError; 477 console.error(TAG + `get remote deviceId error, error code: ${err.code}, error message: ${err.message}`); 478 } 479 return deviceId; 480} 481``` 482 483## 相关实例 484 485针对分布式数据对象开发,有以下相关实例可供参考: 486 487- [设备管理(ArkTS)(Full SDK)(API10)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/SystemFeature/DistributedAppDev/DistributedAuthentication) 488 489- [分布式备忘录(ArkTS)(Full SDK)(API10)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/SuperFeature/DistributedAppDev/DistributedNote)