• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# AppStorage:应用全局的UI状态存储
2<!--Kit: ArkUI-->
3<!--Subsystem: ArkUI-->
4<!--Owner: @zzq212050299-->
5<!--Designer: @s10021109-->
6<!--Tester: @TerryTsao-->
7<!--Adviser: @zhang_yixin13-->
8
9在阅读本文档前,建议提前阅读:[状态管理概述](./arkts-state-management-overview.md),从而对状态管理框架中AppStorage的定位有一个宏观了解。
10
11AppStorage是与应用进程绑定的全局UI状态存储中心,由UI框架在应用启动时创建,将UI状态数据存储于运行内存,实现应用级全局状态共享。
12
13作为应用的“中枢”,AppStorage是[持久化数据PersistentStorage](arkts-persiststorage.md)和[环境变量Environment](arkts-environment.md)与UI交互的中转桥梁。其核心价值在于为开发者提供跨ability的大范围UI状态数据共享能力。
14
15AppStorage提供了API接口,允许开发者在自定义组件外手动触发AppStorage对应属性的增、删、改、查操作。建议配合[AppStorage API文档](../../reference/apis-arkui/arkui-ts/ts-state-management.md#appstorage)阅读。最佳实践请参考[状态管理最佳实践](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-status-management)16
17## 概述
18
19AppStorage是在应用启动时创建的单例,用于提供应用状态数据的中心存储。这些状态数据在应用级别可访问。AppStorage在应用运行过程中保留其属性。
20
21AppStorage中保存的属性通过唯一的字符串类型属性名(key)访问,该属性可以和UI组件同步,且可以在应用业务逻辑中被访问。
22
23AppStorage支持应用的[主线程](../../application-models/thread-model-stage.md)内多个[UIAbility](../../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md)实例间的UI状态数据共享。
24
25AppStorage中的属性通过唯一的字符串类型key值访问,支持与UI组件同步,并可在应用业务逻辑中被访问。其支持应用的[主线程](../../application-models/thread-model-stage.md)内多个[UIAbility](../../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md)实例间的UI状态数据共享。
26
27AppStorage中的属性可以被双向同步,并具有不同的功能,比如数据持久化(详见[PersistentStorage](arkts-persiststorage.md))。这些UI状态是通过业务逻辑实现,与UI解耦,如果希望这些UI状态在UI中使用,需要用到[@StorageProp](#storageprop)和[@StorageLink](#storagelink)。
28
29## \@StorageProp
30
31\@StorageProp与AppStorage中对应的属性建立单向数据同步。
32
33> **说明:**
34>
35> 从API version 11开始,该装饰器支持在原子化服务中使用。
36
37### 装饰器使用规则说明
38
39| \@StorageProp变量装饰器 | 说明                                                         |
40| ----------------------- | ------------------------------------------------------------ |
41| 装饰器参数              | 常量字符串,必填(字符串需要有引号)。<br/>**说明:**<br/>使用null和undefined作为key时,会隐式转换为对应的字符串,不建议该用法。                |
42| 允许装饰的变量类型      | Object、class、string、number、boolean、enum类型,以及这些类型的数组。<br/>>API Version 12及以上,支持Map、Set、Date、undefined和null类型,嵌套类型的场景请参考[观察变化和行为表现](#观察变化和行为表现)。<br/>同时,API Version 12及以上还支持上述支持类型的联合类型,比如string \| number, string \| undefined 或者 ClassA \| null,示例见[AppStorage支持联合类型](#appstorage支持联合类型)。 <br/>**说明:**<br/>变量类型必须被指定,建议和AppStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。|
43| 不允许装饰的变量类型                | 不支持装饰Function类型。 |
44| 同步类型                | 单向同步:从AppStorage的对应属性到组件的状态变量。<br/>组件本地的修改是允许的,但是AppStorage中给定的属性一旦发生变化,将覆盖本地的修改。 |
45| 被装饰变量的初始值      | 必须本地初始化,如果AppStorage实例中不存在属性,则用该初始值初始化该属性,并存入AppStorage中。 |
46
47### 变量的传递/访问规则说明
48
49| 传递/访问      | 说明                                       |
50| ---------- | ---------------------------------------- |
51| 从父节点初始化和更新 | 禁止从父节点初始化和更新@StorageProp。仅支持使用AppStorage中对应key的属性进行初始化,如果不存在对应key,则使用本地默认值进行初始化。 |
52| 初始化子节点     | 支持,可用于初始化[\@State](./arkts-state.md)、[\@Link](./arkts-link.md)、[\@Prop](./arkts-prop.md)、[\@Provide](./arkts-provide-and-consume.md)。 |
53| 是否支持组件外访问  | 否。                                       |
54
55  **图1** \@StorageProp初始化规则图示  
56
57![zh-cn_image_0000001552978157](figures/zh-cn_image_0000001552978157.png)
58
59### 观察变化和行为表现
60
61**观察变化**
62
63- 当装饰的类型为boolean、string、number时,可以观察到数值的变化。
64
65- 当装饰的数据类型为class或者Object时,可以观察到对象整体赋值和属性变化(详见[从ui内部使用appstorage](#从ui内部使用appstorage))。
66
67- 当装饰的对象是数组时,可以观察到数组添加、删除、更新数组单元的变化。
68
69- 当装饰的对象是Date时,可以观察到Date整体的赋值,以及通过调用Date的接口`setFullYear`、`setMonth`、`setDate`、`setHours`、`setMinutes`、`setSeconds`、`setMilliseconds`、`setTime`、`setUTCFullYear`、`setUTCMonth`、`setUTCDate`、`setUTCHours`、`setUTCMinutes`、`setUTCSeconds`、`setUTCMilliseconds`更新Date的属性。详见[装饰Date类型变量](#装饰date类型变量)。
70
71- 当装饰的变量是Map时,可以观察到Map整体的赋值,以及通过调用Map的接口`set`、`clear`、`delete`更新Map的值。详见[装饰Map类型变量](#装饰map类型变量)。
72
73- 当装饰的变量是Set时,可以观察到Set整体的赋值,以及通过调用Set的接口`add`、`clear`、`delete`更新Set的值。详见[装饰Set类型变量](#装饰set类型变量)。
74
75**框架行为**
76
771. \@StorageProp(key)装饰的数值发生变化,不会同步写回AppStorage对应的属性;变化会触发自定义组件重新渲染,并且该变动仅作用于当前组件的私有成员变量,其他绑定该key的数据不会同步改变。
78
792. 当AppStorage中对应key的属性发生改变时,所有\@StorageProp(key)装饰的变量都会同步更新,本地的修改将被覆盖。
80
81## \@StorageLink
82
83\@StorageLink与AppStorage中对应的属性建立双向数据同步。
84
85> **说明:**
86>
87> 从API version 11开始,该装饰器支持在原子化服务中使用。
88
89### 装饰器使用规则说明
90
91| \@StorageLink变量装饰器 | 说明                                                         |
92| ----------------------- | ------------------------------------------------------------ |
93| 装饰器参数              | key:常量字符串,必填(字符串需要有引号)。<br/>**注意:**<br/>使用null和undefined作为key时,会隐式转换为对应的字符串,不建议该用法。                  |
94| 允许装饰的变量类型      | Object、class、string、number、boolean、enum类型,以及这些类型的数组。<br/>API Version 12及以上支持Map、Set、Date、undefined和null类型。嵌套类型的场景请参考[观察变化和行为表现](#观察变化和行为表现-1)。<br/>>API Version 12及以上还支持上述支持类型的联合类型,比如string \| number, string \| undefined 或者 ClassA \| null,示例见[AppStorage支持联合类型](#appstorage支持联合类型)。 <br/>**注意:**<br/>变量类型必须被指定,建议和AppStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。 |
95| 不允许装饰的变量类型                | 不支持装饰Function类型。 |
96| 同步类型                | 双向同步:从AppStorage的对应属性到自定义组件,从自定义组件到AppStorage对应属性。 |
97| 被装饰变量的初始值      | 必须本地初始化,如果AppStorage实例中不存在属性,则用该初始值初始化该属性,并存入AppStorage中。 |
98
99
100### 变量的传递/访问规则说明
101
102| 传递/访问      | 说明                                       |
103| ---------- | ---------------------------------------- |
104| 从父节点初始化和更新 | 禁止。                                      |
105| 初始化子节点     | 支持,可用于初始化常规变量、\@State、\@Link、\@Prop、\@Provide。 |
106| 是否支持组件外访问  | 否。                                       |
107
108  **图2** \@StorageLink初始化规则图示  
109
110![zh-cn_image_0000001501938718](figures/zh-cn_image_0000001501938718.png)
111
112### 观察变化和行为表现
113
114**观察变化**
115
116- 装饰的数据类型为boolean、string、number时,可以观察到数值变化。
117
118- 装饰的数据类型为class或Object时,可以观察到对象整体赋值和属性变化。(详见[从ui内部使用appstorage](#从ui内部使用appstorage))。
119
120- 当装饰的对象是数组时,可以观察到数组添加、删除、更新数组单元的变化。
121
122- 当装饰的对象是Date时,可以观察到Date整体的赋值,以及通过调用Date的接口`setFullYear`, `setMonth`, `setDate`, `setHours`, `setMinutes`, `setSeconds`, `setMilliseconds`, `setTime`, `setUTCFullYear`, `setUTCMonth`, `setUTCDate`, `setUTCHours`, `setUTCMinutes`, `setUTCSeconds`, `setUTCMilliseconds` 更新Date的属性。详见[装饰Date类型变量](#装饰date类型变量)。
123
124- 当装饰的变量是Map时,可以观察到Map整体的赋值,以及通过调用Map的接口`set`、`clear`、`delete`更新Map的值。详见[装饰Map类型变量](#装饰map类型变量)。
125
126- 当装饰的变量是Set时,可以观察到Set的整体赋值,以及通过调用Set的接口`add`、`clear`、`delete`更新Set的值。详见[装饰Set类型变量](#装饰set类型变量)。
127
128**框架行为**
129
1301. 当\@StorageLink(key)装饰的数值发生变化时,修改将被同步回AppStorage对应key的属性中。
131
1322. AppStorage中key对应的数据一旦改变,其绑定的所有的数据(包括双向\@StorageLink和单向\@StorageProp)都将被同步修改。
133
1343. `@StorageLink(key)`装饰的数据是状态变量,其变化不仅会同步到AppStorage,还会触发自定义组件的重新渲染。
135
136
137## 限制条件
138
1391. \@StorageProp/\@StorageLink的参数必须为string类型,否则编译期会报错。
140
141    ```ts
142    AppStorage.setOrCreate('propA', 47);
143
144    // 错误写法,编译报错
145    @StorageProp() storageProp: number = 1;
146    @StorageLink() storageLink: number = 2;
147
148    // 正确写法
149    @StorageProp('propA') storageProp: number = 1;
150    @StorageLink('propA') storageLink: number = 2;
151    ```
152
1532. \@StorageProp与\@StorageLink不支持装饰Function类型的变量,框架会抛出运行时错误。
154
1553. AppStorage与[PersistentStorage](arkts-persiststorage.md)以及[Environment](arkts-environment.md)配合使用时,需要注意以下几点:
156
157    1. 在AppStorage中创建属性后,调用PersistentStorage.[persistProp](../../reference/apis-arkui/arkui-ts/ts-state-management.md#persistpropdeprecated)接口时,会使用AppStorage中已存在的值,并覆盖PersistentStorage中的同名属性。因此,建议使用相反的调用顺序。反例可见[在PersistentStorage之前访问AppStorage中的属性](arkts-persiststorage.md#在persistentstorage之前访问appstorage中的属性)。
158
159    2. 如果在AppStorage中已创建属性,再调用Environment.[envProp](../../reference/apis-arkui/arkui-ts/ts-state-management.md#envprop10)创建同名属性,会调用失败。因为AppStorage已有同名属性,Environment环境变量不会再写入AppStorage中,所以建议不要在AppStorage中使用Environment预置环境变量名。
160
1614. 状态装饰器装饰的变量,改变会引起UI的渲染更新。如果改变的变量仅用于消息传递,不用于UI更新,推荐使用emitter方式。具体示例可见[不建议借助@StorageLink的双向同步机制实现事件通知](#不建议借助storagelink的双向同步机制实现事件通知)。
162
1635. AppStorage同一进程内共享,UIAbility和<!--Del-->[<!--DelEnd-->UIExtensionAbility<!--Del-->](../../application-models/uiextensionability.md)<!--DelEnd-->是两个进程,所以在<!--Del-->[<!--DelEnd-->UIExtensionAbility<!--Del-->](../../application-models/uiextensionability.md)<!--DelEnd-->中不共享主进程的AppStorage。
164
165## 使用场景
166
167### 从应用逻辑使用AppStorage和LocalStorage
168
169AppStorage是单例,其所有API均为静态方法,使用方法类似于LocalStorage中对应的非静态方法。
170
171```ts
172AppStorage.setOrCreate('propA', 47);
173
174let storage: LocalStorage = new LocalStorage();
175storage.setOrCreate('propA',17);
176let propA: number | undefined = AppStorage.get('propA'); // propA in AppStorage == 47, propA in LocalStorage == 17
177let link1: SubscribedAbstractProperty<number> = AppStorage.link('propA'); // link1.get() == 47
178let link2: SubscribedAbstractProperty<number> = AppStorage.link('propA'); // link2.get() == 47
179let prop: SubscribedAbstractProperty<number> = AppStorage.prop('propA'); // prop.get() == 47
180
181link1.set(48); // 双向同步: link1.get() == link2.get() == prop.get() == 48
182prop.set(1); // 单向同步: prop.get() == 1; 但 link1.get() == link2.get() == 48
183link1.set(49); // 双向同步: link1.get() == link2.get() == prop.get() == 49
184
185storage.get<number>('propA') // == 17
186storage.set('propA', 101);
187storage.get<number>('propA') // == 101
188
189AppStorage.get<number>('propA') // == 49
190link1.get() // == 49
191link2.get() // == 49
192prop.get() // == 49
193```
194
195
196### 从UI内部使用AppStorage
197
198@StorageLink与AppStorage配合使用,通过AppStorage中的属性创建双向数据同步。
199@StorageProp与AppStorage配合使用,通过AppStorage中的属性创建单向数据同步。
200
201```ts
202class Data {
203  code: number;
204
205  constructor(code: number) {
206    this.code = code;
207  }
208}
209
210AppStorage.setOrCreate('propA', 47);
211AppStorage.setOrCreate('propB', new Data(50));
212let storage = new LocalStorage();
213storage.setOrCreate('linkA', 48);
214storage.setOrCreate('linkB', new Data(100));
215
216@Entry(storage)
217@Component
218struct Index {
219  @StorageLink('propA') storageLink: number = 1;
220  @StorageProp('propA') storageProp: number = 1;
221  @StorageLink('propB') storageLinkObject: Data = new Data(1);
222  @StorageProp('propB') storagePropObject: Data = new Data(1);
223
224  build() {
225    Column({ space: 20 }) {
226      // @StorageLink与AppStorage建立双向联系,更改数据会同步回AppStorage中key为'propA'的值
227      Text(`storageLink ${this.storageLink}`)
228        .onClick(() => {
229          this.storageLink += 1;
230        })
231
232      // @StorageProp与AppStorage建立单向联系,更改数据不会同步AppStoragekey为'propA'的值
233      // 但能被AppStorage的set/setorCreate更新值
234      Text(`storageProp ${this.storageProp}`)
235        .onClick(() => {
236          this.storageProp += 1;
237        })
238
239      // AppStorage的API虽然能获取值,但是不具有刷新UI的能力,日志能看到数值更改
240      // 依赖@StorageLink/@StorageProp才能建立起与自定义组件的联系,刷新UI
241      Text(`change by AppStorage: ${AppStorage.get<number>('propA')}`)
242        .onClick(() => {
243          console.info(`Appstorage.get: ${AppStorage.get<number>('propA')}`);
244          AppStorage.set<number>('propA', 100);
245        })
246
247      Text(`storageLinkObject ${this.storageLinkObject.code}`)
248        .onClick(() => {
249          this.storageLinkObject.code += 1;
250        })
251
252      Text(`storagePropObject ${this.storagePropObject.code}`)
253        .onClick(() => {
254          this.storagePropObject.code += 1;
255        })
256    }
257  }
258}
259```
260
261### AppStorage支持联合类型
262
263在下面的示例中,变量linkA的类型为number | null,变量linkB的类型为number | undefined。Text组件初始化分别显示为null和undefined,点击切换为数字,再次点击切换回null和undefined。
264
265```ts
266@Component
267struct StorageLinkComponent {
268  @StorageLink('linkA') linkA: number | null = null;
269  @StorageLink('linkB') linkB: number | undefined = undefined;
270
271  build() {
272    Column() {
273      Text('@StorageLink接口初始化,@StorageLink取值')
274      Text(`${this.LinkA}`).fontSize(20).onClick(() => {
275        this.linkA ? this.linkA = null : this.linkA = 1;
276      })
277      Text(`${this.LinkB}`).fontSize(20).onClick(() => {
278        this.linkB ? this.linkB = undefined : this.linkB = 1;
279      })
280    }
281    .borderWidth(3).borderColor(Color.Red)
282  }
283}
284
285@Component
286struct StoragePropComponent {
287  @StorageProp('propA') propA: number | null = null;
288  @StorageProp('propB') propB: number | undefined = undefined;
289
290  build() {
291    Column() {
292      Text('@StorageProp接口初始化,@StorageProp取值')
293      Text(`${this.propA}`).fontSize(20).onClick(() => {
294        this.propA ? this.propA = null : this.propA = 1;
295      })
296      Text(`${this.propB}`).fontSize(20).onClick(() => {
297        this.propB ? this.propB = undefined : this.propB = 1;
298      })
299    }
300    .borderWidth(3).borderColor(Color.Blue)
301  }
302}
303
304@Entry
305@Component
306struct Index {
307  build() {
308    Row() {
309      Column() {
310        StorageLinkComponent()
311        StoragePropComponent()
312      }
313      .width('100%')
314    }
315    .height('100%')
316  }
317}
318```
319
320### 装饰Date类型变量
321
322> **说明:**
323>
324> 从API version 12开始,AppStorage支持Date类型。
325
326以下示例中,@StorageLink装饰的selectedDate类型为Date。点击Button改变selectedDate的值,视图会随之刷新。
327
328```ts
329@Entry
330@Component
331struct DateSample {
332  @StorageLink('date') selectedDate: Date = new Date('2021-08-08');
333
334  build() {
335    Column() {
336      Button('set selectedDate to 2023-07-08')
337        .margin(10)
338        .onClick(() => {
339          AppStorage.setOrCreate('date', new Date('2023-07-08'));
340        })
341      Button('increase the year by 1')
342        .margin(10)
343        .onClick(() => {
344          this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1);
345        })
346      Button('increase the month by 1')
347        .margin(10)
348        .onClick(() => {
349          this.selectedDate.setMonth(this.selectedDate.getMonth() + 1);
350        })
351      Button('increase the day by 1')
352        .margin(10)
353        .onClick(() => {
354          this.selectedDate.setDate(this.selectedDate.getDate() + 1);
355        })
356      DatePicker({
357        start: new Date('1970-1-1'),
358        end: new Date('2100-1-1'),
359        selected: $$this.selectedDate
360      })
361    }.width('100%')
362  }
363}
364```
365
366### 装饰Map类型变量
367
368> **说明:**
369>
370> 从API version 12开始,AppStorage支持Map类型。
371
372在下面的示例中,@StorageLink装饰的message类型为Map\<number, string\>,点击Button改变message的值,视图会随之刷新。
373
374```ts
375@Entry
376@Component
377struct MapSample {
378  @StorageLink('map') message: Map<number, string> = new Map([[0, 'a'], [1, 'b'], [3, 'c']]);
379
380  build() {
381    Row() {
382      Column() {
383        ForEach(Array.from(this.message.entries()), (item: [number, string]) => {
384          Text(`${item[0]}`).fontSize(30)
385          Text(`${item[1]}`).fontSize(30)
386          Divider()
387        })
388        Button('init map').onClick(() => {
389          this.message = new Map([[0, 'a'], [1, 'b'], [3, 'c']]);
390        })
391        Button('set new one').onClick(() => {
392          this.message.set(4, 'd');
393        })
394        Button('clear').onClick(() => {
395          this.message.clear();
396        })
397        Button('replace the existing one').onClick(() => {
398          this.message.set(0, 'aa');
399        })
400        Button('delete the existing one').onClick(() => {
401          AppStorage.get<Map<number, string>>('map')?.delete(0);
402        })
403      }
404      .width('100%')
405    }
406    .height('100%')
407  }
408}
409```
410
411### 装饰Set类型变量
412
413> **说明:**
414>
415> 从API version 12开始,AppStorage支持Set类型。
416
417在下面的示例中,@StorageLink装饰的memberSet类型为Set\<number\>,点击Button改变memberSet的值,视图会随之刷新。
418
419```ts
420@Entry
421@Component
422struct SetSample {
423  @StorageLink('set') memberSet: Set<number> = new Set([0, 1, 2, 3, 4]);
424
425  build() {
426    Row() {
427      Column() {
428        ForEach(Array.from(this.memberSet.entries()), (item: [number, number]) => {
429          Text(`${item[0]}`)
430            .fontSize(30)
431          Divider()
432        })
433        Button('init set')
434          .onClick(() => {
435            this.memberSet = new Set([0, 1, 2, 3, 4]);
436          })
437        Button('set new one')
438          .onClick(() => {
439            AppStorage.get<Set<number>>('set')?.add(5);
440          })
441        Button('clear')
442          .onClick(() => {
443            this.memberSet.clear();
444          })
445        Button('delete the first one')
446          .onClick(() => {
447            this.memberSet.delete(0);
448          })
449      }
450      .width('100%')
451    }
452    .height('100%')
453  }
454}
455```
456
457## AppStorage使用建议
458
459### 不建议借助@StorageLink的双向同步机制实现事件通知
460
461不建议使用@StorageLink和AppStorage的双向同步机制来实现事件通知。AppStorage中的变量可能绑定在多个页面的组件中,但事件通知不一定需要通知到所有这些组件。此外,当这些@StorageLink装饰的变量在UI中使用时,会触发UI刷新,造成不必要的性能影响。
462
463示例代码中,`TapImage`中的点击事件会触发`AppStorage`中`tapIndex`对应属性的改变。由于`@StorageLink`是双向同步的,修改会同步回`AppStorage`中,因此所有绑定`AppStorage`的`tapIndex`自定义组件都能感知到`tapIndex`的变化。使用`@Watch`监听到`tapIndex`的变化后,修改状态变量`tapColor`,从而触发UI刷新(此处`tapIndex`未直接绑定在UI上,因此`tapIndex`的变化不会直接触发UI刷新)。
464
465使用该机制实现事件通知时,应确保AppStorage中的变量不直接被绑定到UI上,同时控制[@Watch](./arkts-watch.md)函数的复杂度。如果@Watch函数执行时间过长,会影响UI刷新效率。
466
467```ts
468// xxx.ets
469class ViewData {
470  title: string;
471  uri: Resource;
472  color: Color = Color.Black;
473
474  constructor(title: string, uri: Resource) {
475    this.title = title;
476    this.uri = uri;
477  }
478}
479
480@Entry
481@Component
482struct Gallery {
483  // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
484  dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))];
485  scroller: Scroller = new Scroller();
486
487  build() {
488    Column() {
489      Grid(this.scroller) {
490        ForEach(this.dataList, (item: ViewData, index?: number) => {
491          GridItem() {
492            TapImage({
493              uri: item.uri,
494              index: index
495            })
496          }.aspectRatio(1)
497
498        }, (item: ViewData, index?: number) => {
499          return JSON.stringify(item) + index;
500        })
501      }.columnsTemplate('1fr 1fr')
502    }
503
504  }
505}
506
507@Component
508export struct TapImage {
509  @StorageLink('tapIndex') @Watch('onTapIndexChange') tapIndex: number = -1;
510  @State tapColor: Color = Color.Black;
511  private index: number = 0;
512  private uri: Resource = {
513    id: 0,
514    type: 0,
515    moduleName: '',
516    bundleName: ''
517  };
518
519  // 判断是否被选中
520  onTapIndexChange() {
521    if (this.tapIndex >= 0 && this.index === this.tapIndex) {
522      console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, red`);
523      this.tapColor = Color.Red;
524    } else {
525      console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, black`);
526      this.tapColor = Color.Black;
527    }
528  }
529
530  build() {
531    Column() {
532      Image(this.uri)
533        .objectFit(ImageFit.Cover)
534        .onClick(() => {
535          this.tapIndex = this.index;
536        })
537        .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
538    }
539
540  }
541}
542```
543
544相比借助@StorageLink的双向同步机制实现事件通知,开发者可以使用emit订阅某个事件并接收事件回调的方式来减少开销,增强代码的可读性。
545
546> **说明:**
547>
548> emit接口不支持在Previewer预览器中使用。
549
550
551```ts
552// xxx.ets
553import { emitter } from '@kit.BasicServicesKit';
554
555let nextId: number = 0;
556
557class ViewData {
558  title: string;
559  uri: Resource;
560  color: Color = Color.Black;
561  id: number;
562
563  constructor(title: string, uri: Resource) {
564    this.title = title;
565    this.uri = uri;
566    this.id = nextId++;
567  }
568}
569
570@Entry
571@Component
572struct Gallery {
573  // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
574  dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))];
575  scroller: Scroller = new Scroller();
576  private preIndex: number = -1;
577
578  build() {
579    Column() {
580      Grid(this.scroller) {
581        ForEach(this.dataList, (item: ViewData) => {
582          GridItem() {
583            TapImage({
584              uri: item.uri,
585              index: item.id
586            })
587          }.aspectRatio(1)
588          .onClick(() => {
589            if (this.preIndex === item.id) {
590              return;
591            }
592            let innerEvent: emitter.InnerEvent = { eventId: item.id };
593            // 选中态:黑变红
594            let eventData: emitter.EventData = {
595              data: {
596                'colorTag': 1
597              }
598            };
599            emitter.emit(innerEvent, eventData);
600
601            if (this.preIndex != -1) {
602              console.info(`preIndex: ${this.preIndex}, index: ${item.id}, black`);
603              let innerEvent: emitter.InnerEvent = { eventId: this.preIndex };
604              // 取消选中态:红变黑
605              let eventData: emitter.EventData = {
606                data: {
607                  'colorTag': 0
608                }
609              };
610              emitter.emit(innerEvent, eventData);
611            }
612            this.preIndex = item.id;
613          })
614        }, (item: ViewData) => JSON.stringify(item))
615      }.columnsTemplate('1fr 1fr')
616    }
617
618  }
619}
620
621@Component
622export struct TapImage {
623  @State tapColor: Color = Color.Black;
624  private index: number = 0;
625  private uri: Resource = {
626    id: 0,
627    type: 0,
628    moduleName: '',
629    bundleName: ''
630  };
631
632  onTapIndexChange(colorTag: emitter.EventData) {
633    if (colorTag.data != null) {
634      this.tapColor = colorTag.data.colorTag ? Color.Red : Color.Black
635    }
636  }
637
638  aboutToAppear() {
639    //定义事件ID
640    let innerEvent: emitter.InnerEvent = { eventId: this.index };
641    emitter.on(innerEvent, data => {
642      this.onTapIndexChange(data);
643    });
644  }
645
646  build() {
647    Column() {
648      Image(this.uri)
649        .objectFit(ImageFit.Cover)
650        .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
651    }
652  }
653}
654```
655
656以上通知事件逻辑简单,也可以简化成三元表达式。
657
658```ts
659// xxx.ets
660class ViewData {
661  title: string;
662  uri: Resource;
663  color: Color = Color.Black;
664
665  constructor(title: string, uri: Resource) {
666    this.title = title;
667    this.uri = uri;
668  }
669}
670
671@Entry
672@Component
673struct Gallery {
674  // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
675  dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))];
676  scroller: Scroller = new Scroller();
677
678  build() {
679    Column() {
680      Grid(this.scroller) {
681        ForEach(this.dataList, (item: ViewData, index?: number) => {
682          GridItem() {
683            TapImage({
684              uri: item.uri,
685              index: index
686            })
687          }.aspectRatio(1)
688
689        }, (item: ViewData, index?: number) => {
690          return JSON.stringify(item) + index;
691        })
692      }.columnsTemplate('1fr 1fr')
693    }
694
695  }
696}
697
698@Component
699export struct TapImage {
700  @StorageLink('tapIndex') tapIndex: number = -1;
701  private index: number = 0;
702  private uri: Resource = {
703    id: 0,
704    type: 0,
705    moduleName: '',
706    bundleName: ''
707  };
708
709  build() {
710    Column() {
711      Image(this.uri)
712        .objectFit(ImageFit.Cover)
713        .onClick(() => {
714          this.tapIndex = this.index;
715        })
716        .border({
717          width: 5,
718          style: BorderStyle.Dotted,
719          color: (this.tapIndex >= 0 && this.index === this.tapIndex) ? Color.Red : Color.Black
720        })
721    }
722  }
723}
724```
725
726
727### \@StorageProp和AppStorage接口配合使用时,需要注意更新规则
728
729使用setOrCreate/set接口更新key的值时,如果值相同,setOrCreate不会通知\@StorageLink/\@StorageProp更新,但因为\@StorageProp本身有数据副本,更改值不会同步给AppStorage,这会导致开发者误认己通过AppStorage改了值,但实际上未通知\@StorageProp更新值的情况。
730示例如下。
731
732```ts
733AppStorage.setOrCreate('propA', false);
734
735@Entry
736@Component
737struct Index {
738  @StorageProp('propA') @Watch('onChange') propA: boolean = false;
739
740  onChange() {
741    console.info(`propA change`);
742  }
743
744  aboutToAppear(): void {
745    this.propA = true;
746  }
747
748  build() {
749    Column() {
750      Text(`${this.propA}`)
751      Button('change')
752        .onClick(() => {
753          AppStorage.setOrCreate('propA', false);
754          console.info(`propA: ${this.propA}`);
755        })
756    }
757  }
758}
759```
760
761上述示例,在点击事件之前,propA的值已经在本地被更改为true,而AppStorage中存的值仍为false。当点击事件通过setOrCreate接口尝试更新propA的值为false时,由于AppStorage中的值为false,两者相等,不会触发更新同步,因此@StorageProp的值仍为true。
762
763实现二者同步有以下两种方式:
7641. 将\@StorageProp更改为\@StorageLink。
7652. 本地更改值的方式变为使用AppStorage.setOrCreate('propA', true)的方式。