1# 分布式文件场景 2 3## 场景说明 4 5两台设备组网的分布式场景是工作中常常需要的。常见的如代码的同步编辑、文档的同步修改等。这样的分布式场景有助于加快工作效率,减少工作中的冗余,本例将为大家介绍如何实现上述功能。 6 7## 效果呈现 8 9本例效果如下: 10 11| 设置分布式权限 | 进行分布式连接 | 连接后状态显示 | 12| -------------------------------------- | ---------------------------------------- | --------------------------------------- | 13|  |  |  | 14 15| 点击添加进入编辑界面 | 保存后本机显示 | 另外一台机器分布式应用显示 | 16| ------------------------------------- | -------------------------------------- | -------------------------------------- | 17|  |  |  | 18 19## 运行环境 20 21本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发。 22 23- IDE:DevEco Studio 4.0.0.201 Beta1 24- SDK:Ohos_sdk_public 4.0.7.5 (API Version 10 Beta1) 25 26## 实现思路 27 28在分布式文件场景中,分布式设备管理包含了分布式设备搜索、分布式设备列表弹窗、远端设备拉起三部分。 29首先在分布式组网内搜索设备,然后把设备展示到分布式设备列表弹窗中,最后根据用户的选择拉起远端设备。 30 31- 分布式设备搜索:通过SUBSCRIBE_ID搜索分布式组网内的设备。 32 33- 分布式设备列表弹窗:使用@CustomDialog装饰器来装饰分布式设备列表弹窗。 34 35- 远端设备拉起:通过startAbility(deviceId)方法拉起远端设备的包。 36 37- 分布式数据管理:(1)管理分布式数据库:创建一个distributedObject分布式数据对象实例,用于管理分布式数据对象。 38 39 (2)订阅分布式数据变化:通过this.distributedObject.on('status', this.statusCallback)监听分布式数据对象的变更。 40 41## 开发步骤 42 431. 申请所需权限 44 45 在model.json5中添加以下配置: 46 47 ```json 48 "requestPermissions": [ 49 { 50 "name": "ohos.permission.DISTRIBUTED_DATASYNC"//允许不同设备间的数据交换 51 }, 52 { 53 "name": "ohos.permission.ACCESS_SERVICE_DM"//允许系统应用获取分布式设备的认证组网能力 54 } 55 ] 56 ``` 57 582. 构建UI框架 59 60 index页面: 61 62 TitleBar组件呈现标题栏。使用List组件呈现文件列表,ListItem由一个呈现文件类型标志的Image组件,一个呈现文件标题的Text组件,一个呈现文件内容的Text组件组成。 63 64 ```typescript 65 build() { 66 Column() { 67 TitleBar({ rightBtn: $r('app.media.trans'), onRightBtnClicked: this.showDialog }) 68 //自/common/TitleBar.ets中引入标题栏相关。点击标题栏中的右侧按钮会调用showDialog()函数连接组网设备 69 Row() { 70 Text($r('app.string.state')) 71 .fontSize(30) 72 Image(this.isOnline ? $r('app.media.green') : $r('app.media.red'))//两台设备组网成功后状态显示为绿色、否则为红色 73 .size({ width: 30, height: 30 }) 74 .objectFit(ImageFit.Contain) 75 } 76 .width('100%') 77 .padding(16) 78 //通过数据懒加载的方式从数据源中每次迭代一个文件进行展示,可用列表被放置在滚动容器中,被划出可视区域外的资源会被回收 79 List({ space: 10 }) { 80 LazyForEach(this.noteDataSource, (item: Note, index) => { 81 ListItem() { 82 NoteItem({ note: item, index: index })//NoteItem引入自common/NoteItem.ets,负责主页文件信息的呈现 83 .id(`${item.title}`) 84 } 85 }, item => JSON.stringify(item)) 86 } 87 .width('95%') 88 .margin(10) 89 .layoutWeight(1) 90 91 Row() { 92 Column() { 93 Image($r('app.media.clear'))//清除按钮 94 .size({ width: 40, height: 40 }) 95 Text($r('app.string.clear')) 96 .fontColor(Color.Red) 97 .fontSize(20) 98 }.layoutWeight(1) 99 .id('clearNote') 100 .onClick(() => { 101 //点击清除按钮清除所有文件 102 Logger.info(TAG, 'clear notes') 103 this.noteDataSource['dataArray'] = [] 104 this.noteDataSource.notifyDataReload() 105 this.globalObject.clear() 106 AppStorage.SetOrCreate('sessionId', this.sessionId) 107 }) 108 109 Column() { 110 Image($r('app.media.add'))//添加按钮 111 .size({ width: 40, height: 40 }) 112 Text($r('app.string.add')) 113 .fontColor(Color.Black) 114 .fontSize(20) 115 }.layoutWeight(1) 116 .id('addNote') 117 .onClick(() => { 118 //点击添加按钮跳转到编辑页面 119 router.push({ 120 url: 'pages/Edit', 121 params: { 122 note: new Note('', '', -1), 123 isAdd: true 124 } 125 }) 126 }) 127 } 128 .width('100%') 129 .padding(10) 130 .backgroundColor('#F0F0F0') 131 } 132 .width('100%') 133 .height('100%') 134 .backgroundColor('#F5F5F5') 135 } 136 } 137 ... 138 //common/NoteItem.ets 139 import router from '@ohos.router' 140 import { MARKS } from '../model/Const' 141 import Note from '../model/Note' 142 143 @Component 144 export default struct NoteItem { 145 @State note: Note | undefined = undefined 146 private index: number = 0 147 148 build() { 149 Row() { 150 Image(this.note.mark >= 0 ? MARKS[this.note.mark] : $r('app.media.note'))//文件标志图片 151 .size({ width: 30, height: 30 }) 152 .objectFit(ImageFit.Contain) 153 Column() { 154 Text(this.note.title)//文件标题 155 .fontColor(Color.Black) 156 .fontSize(30) 157 .maxLines(1) 158 .textOverflow({ overflow: TextOverflow.Ellipsis }) 159 Text(this.note.content)//文件内容 160 .fontColor(Color.Gray) 161 .margin({ top: 10 }) 162 .fontSize(25) 163 .maxLines(1)//在列表中最多展示一行 164 .textOverflow({ overflow: TextOverflow.Ellipsis }) 165 } 166 .alignItems(HorizontalAlign.Start) 167 .margin({ left: 20 }) 168 } 169 .padding(16) 170 .width('100%') 171 .borderRadius(16) 172 .backgroundColor(Color.White) 173 .onClick(() => { 174 //点击文件进入此文件编辑页面 175 router.push({ 176 url: 'pages/Edit', 177 params: { 178 index: this.index, 179 note: this.note, 180 isAdd: false 181 } 182 }) 183 }) 184 } 185 } 186 ``` 187 188 Edit页面: 189 190 使用TextInput组件呈现文件标题输入框,使用TextArea组件呈现文件内容的输入区域,使用Button组件呈现保存按钮并绑定点击事件以新建或更新文件内容。 191 192 ```typescript 193 build() { 194 Column() { 195 TitleBar({ title: this.note.title === '' ? $r('app.string.add_note') : this.note.title }) 196 Column() { 197 Row() { 198 Image(this.note.mark >= 0 ? MARKS[this.note.mark] : $r('app.media.mark')) 199 .width(30) 200 .aspectRatio(1) 201 .margin({ left: 16, top: 16 }) 202 .objectFit(ImageFit.Contain) 203 .alignSelf(ItemAlign.Start) 204 Select([{ value: ' ', icon: MARKS[0] }, 205 { value: ' ', icon: MARKS[1] }, 206 { value: ' ', icon: MARKS[2] }, 207 { value: ' ', icon: MARKS[3] }, 208 { value: ' ', icon: MARKS[4] }]) 209 .selected(this.note.mark) 210 .margin({ top: 5 }) 211 .onSelect((index: number) => { 212 this.note.mark = index 213 }) 214 } 215 .width('100%') 216 217 TextInput({ placeholder: 'input the title', text: this.note.title })//文件标题输入框 218 .id('titleInput') 219 .placeholderColor(Color.Gray) 220 .fontSize(30) 221 .margin({ left: 15, right: 15, top: 15 }) 222 .height(60) 223 .backgroundColor(Color.White) 224 .onChange((value: string) => { 225 this.note.title = value 226 }) 227 TextArea({ placeholder: 'input the content', text: this.note.content })//文件内容输入区域 228 .id('contentInput') 229 .placeholderColor(Color.Gray) 230 .backgroundColor(Color.White) 231 .fontSize(30) 232 .height('35%') 233 .margin({ left: 16, right: 16, top: 16 }) 234 .textAlign(TextAlign.Start) 235 .onChange((value: string) => { 236 this.note.content = value 237 }) 238 239 Button() { 240 //保存按钮 241 Text($r('app.string.save')) 242 .fontColor(Color.White) 243 .fontSize(17) 244 } 245 .id('saveNote') 246 .backgroundColor('#0D9FFB') 247 .height(50) 248 .width(200) 249 .margin({ top: 20 }) 250 .onClick(() => { 251 //点击按钮时调用model/DistributedObjectModel.ts定义的类globalObject中的方法 252 if (!this.isAdd) { 253 let index = router.getParams()['index'] 254 this.globalObject.update(index, this.note.title, this.note.content, this.note.mark)//编辑时更新内容 255 } else { 256 this.globalObject.add(this.note.title, this.note.content, this.note.mark)//新建时添加内容 257 } 258 router.back()//返回主页 259 }) 260 } 261 } 262 .width('100%') 263 .height('100%') 264 .backgroundColor('#F5F5F5') 265 } 266 } 267 ``` 268 2693. 将两台设备组网 270 271 使用自RemoteDeviceModel.ts中引入的类RemoteDeviceModel以扫描获得附近可以连接的设备。 272 273 ```typescript 274 showDialog = () => { 275 //RemoteDeviceModel引入自model/RemoteDeviceModel.ts 276 RemoteDeviceModel.registerDeviceListCallback(() => { 277 //得到附近可信的设备列表 278 Logger.info(TAG, 'registerDeviceListCallback, callback entered') 279 this.devices = [] 280 this.devices = RemoteDeviceModel.discoverDevices.length > 0 ? RemoteDeviceModel.discoverDevices : RemoteDeviceModel.devices 281 if (this.dialogController) { 282 this.dialogController.close() 283 this.dialogController = undefined 284 } 285 this.dialogController = new CustomDialogController({ 286 builder: DeviceDialog({ 287 devices: this.devices, 288 onSelectedIndexChange: this.onSelectedDevice 289 }), 290 autoCancel: true 291 }) 292 this.dialogController.open() 293 }) 294 } 295 ... 296 //model/RemoteDeviceModel.ts 297 import deviceManager from '@ohos.distributedHardware.deviceManager' 298 registerDeviceListCallback(stateChangeCallback: () => void) { 299 if (typeof (this.deviceManager) !== 'undefined') { 300 this.registerDeviceListCallbackImplement(stateChangeCallback) 301 return 302 } 303 Logger.info(TAG, 'deviceManager.createDeviceManager begin') 304 try { 305 deviceManager.createDeviceManager(BUNDLE, (error, value) => { 306 if (error) { 307 Logger.error(TAG, 'createDeviceManager failed.') 308 return 309 } 310 this.deviceManager = value 311 this.registerDeviceListCallbackImplement(stateChangeCallback) 312 Logger.info(TAG, `createDeviceManager callback returned,value=${value}`) 313 }) 314 } catch (error) { 315 Logger.error(TAG, `createDeviceManager throw error, code=${error.code} message=${error.message}`) 316 } 317 318 Logger.info(TAG, 'deviceManager.createDeviceManager end') 319 } 320 registerDeviceListCallbackImplement(stateChangeCallback: () => void) { 321 Logger.info(TAG, 'registerDeviceListCallback') 322 this.stateChangeCallback = stateChangeCallback 323 if (this.deviceManager === undefined) { 324 Logger.error(TAG, 'deviceManager has not initialized') 325 this.stateChangeCallback() 326 return 327 } 328 Logger.info(TAG, 'getTrustedDeviceListSync begin') 329 try { 330 let list = this.deviceManager.getTrustedDeviceListSync()//同步获取所有可信设备列表 331 Logger.info(TAG, `getTrustedDeviceListSync end, devices=${JSON.stringify(list)}`) 332 if (typeof (list) !== 'undefined' && typeof (list.length) !== 'undefined') { 333 this.devices = list 334 } 335 } catch (error) { 336 Logger.error(TAG, `getLocalDeviceInfoSync throw error, code=${error.code} message=${error.message}`) 337 } 338 this.stateChangeCallback() 339 Logger.info(TAG, 'callback finished') 340 try { 341 this.deviceManager.on('deviceStateChange', (data) => { 342 if (data === null) { 343 return 344 } 345 Logger.info(TAG, `deviceStateChange data = ${JSON.stringify(data)}`) 346 switch (data.action) { 347 case deviceManager.DeviceStateChangeAction.READY://即设备处于可用状态,表示设备间信息已在分布式数据中同步完成, 可以运行分布式业务 348 this.discoverDevices = [] 349 this.devices.push(data.device) 350 this.stateChangeCallback() 351 try { 352 let list = this.deviceManager.getTrustedDeviceListSync() 353 if (typeof (list) !== 'undefined' && typeof (list.length) !== 'undefined') { 354 this.devices = list 355 } 356 } catch (error) { 357 Logger.error(TAG, `getTrustedDeviceListSync throw error, code=${error.code} message=${error.message}`) 358 } 359 this.stateChangeCallback() 360 break 361 default: 362 break 363 } 364 }) 365 this.deviceManager.on('deviceFound', (data) => { 366 if (data === null) { 367 return 368 } 369 Logger.info(TAG, `deviceFound data=${JSON.stringify(data)}`) 370 this.onDeviceFound(data) 371 }) 372 this.deviceManager.on('discoverFail', (data) => { 373 Logger.info(TAG, `discoverFail data=${JSON.stringify(data)}`) 374 }) 375 this.deviceManager.on('serviceDie', () => { 376 Logger.info(TAG, 'serviceDie') 377 }) 378 } catch (error) { 379 Logger.error(TAG, `on throw error, code=${error.code} message=${error.message}`) 380 } 381 this.startDeviceDiscovery() 382 } 383 startDeviceDiscovery() { 384 SUBSCRIBE_ID = Math.floor(65536 * Math.random()) 385 var info = { 386 subscribeId: SUBSCRIBE_ID, 387 mode: 0xAA, 388 medium: 2, 389 freq: 2,//高频率 390 isSameAccount: false, 391 isWakeRemote: true, 392 capability: 0 393 } 394 Logger.info(TAG, `startDeviceDiscovery${SUBSCRIBE_ID}`) 395 try { 396 this.deviceManager.startDeviceDiscovery(info)//开始发现周边设备 397 } catch (error) { 398 Logger.error(TAG, `startDeviceDiscovery throw error, code=${error.code} message=${error.message}`) 399 } 400 401 } 402 ``` 403 4044. 实现同步编辑 405 406 通过AppStorage设置持久性数据,然后实现IDataSource接口,通过注册数据监听接口监听数据的变化。 407 408 ```typescript 409 class BasicDataSource implements IDataSource { 410 private listeners: DataChangeListener[] = [] 411 412 public totalCount(): number { 413 return 0 414 } 415 416 public getData(index: number): any { 417 return undefined 418 } 419 420 registerDataChangeListener(listener: DataChangeListener): void { 421 if (this.listeners.indexOf(listener) < 0) { 422 console.info('add listener') 423 this.listeners.push(listener) 424 } 425 } 426 427 unregisterDataChangeListener(listener: DataChangeListener): void { 428 const pos = this.listeners.indexOf(listener); 429 if (pos >= 0) { 430 console.info('remove listener') 431 this.listeners.splice(pos, 1) 432 } 433 } 434 //数据准备好了 435 notifyDataReload(): void { 436 this.listeners.forEach(listener => { 437 listener.onDataReloaded() 438 }) 439 } 440 ... 441 } 442 443 onPageShow() { 444 //每当完成编辑或者新建文件,就会回到主页,此时就会执行onPageShow() 445 //noteDataSource获取globalObject保存的分布式的持久性数据,并进行Reload操作传递。 446 this.noteDataSource['dataArray'] = this.globalObject.distributedObject.documents 447 this.noteDataSource.notifyDataReload() 448 Logger.info(TAG, `this.sessionId = ${this.sessionId}`) 449 Logger.info(TAG, `globalSessionId = ${this.globalSessionId}`) 450 if (this.sessionId !== this.globalSessionId) { 451 this.sessionId = this.globalSessionId 452 this.share() 453 } 454 } 455 share() { 456 //多个设备间的对象如果设置为同一个sessionId的笔记数据自动同步 457 Logger.info(TAG, `sessionId = ${this.sessionId}`) 458 this.globalObject.setChangeCallback(() => { 459 this.noteDataSource['dataArray'] = this.globalObject.distributedObject.documents 460 this.noteDataSource.notifyDataReload() 461 }) 462 this.globalObject.setStatusCallback((session, networkId, status) => { 463 Logger.info(TAG, `StatusCallback,${status}`) 464 if (status === 'online') { 465 this.isOnline = true 466 } else { 467 this.isOnline = false 468 } 469 }) 470 this.globalObject.distributedObject.setSessionId(this.sessionId) 471 AppStorage.SetOrCreate('objectModel', this.globalObject) 472 } 473 ``` 474 475## 全部代码 476 477本例完整代码sample示例链接:[分布式对象](https://gitee.com/openharmony/applications_app_samples/tree/master/code/SuperFeature/DistributedAppDev/DistributedNote) 478 479## 参考 480 481- [权限列表](../application-dev/security/AccessToken/permissions-for-all.md) 482- [分布式数据对象](../application-dev/reference/apis-arkdata/js-apis-data-distributedobject.md) 483