1# 分布式文件场景
2
3## 场景说明
4
5两台设备组网的分布式场景是工作中常常需要的。常见的如代码的同步编辑、文档的同步修改等。这样的分布式场景有助于加快工作效率,减少工作中的冗余,本例将为大家介绍如何实现上述功能。
6
7## 效果呈现
8
9本例效果如下:
10
11| 设置分布式权限                         | 进行分布式连接                           | 连接后状态显示                          |
12| -------------------------------------- | ---------------------------------------- | --------------------------------------- |
13| ![](figures/disributed_permission.png) | ![](figures/disributed_note_connect.png) | ![](figures/disributed_note-before.png) |
14
15| 点击添加进入编辑界面                  | 保存后本机显示                         | 另外一台机器分布式应用显示             |
16| ------------------------------------- | -------------------------------------- | -------------------------------------- |
17| ![](figures/disributed_note-edit.png) | ![](figures/disributed_note-after.png) | ![](figures/disributed_note-after.png) |
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
45model.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