• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# AppStorage: Storing Application-wide UI State
2<!--Kit: ArkUI-->
3<!--Subsystem: ArkUI-->
4<!--Owner: @zzq212050299-->
5<!--Designer: @s10021109-->
6<!--Tester: @TerryTsao-->
7<!--Adviser: @zhang_yixin13-->
8
9Before reading this document, it is recommend that you review the [State Management Overview](./arkts-state-management-overview.md) to understand the role of AppStorage within the state management framework.
10
11AppStorage is a global UI state storage center bound to the application process. Created by the UI framework at application launch, it stores UI state data in runtime memory to enable application-level global state sharing.
12
13Serving as the application's "central hub," AppStorage acts as the intermediary bridge between persistent data ([PersistentStorage](arkts-persiststorage.md)), environment variables ([Environment](arkts-environment.md)), and UI interactions. Its primary value lies in providing you with cross-ability UI state sharing capabilities.
14
15AppStorage provides APIs for manual create, retrieve, update, delete (CRUD) operations outside custom components. For details, see [AppStorage API Reference](../../reference/apis-arkui/arkui-ts/ts-state-management.md#appstorage). For best practices, see [State Management](https://developer.huawei.com/consumer/en/doc/best-practices/bpta-status-management).
16
17## Overview
18
19AppStorage is a singleton created at application startup, serving as the central repository for application-wide UI state data. This state data is accessible at the application level. AppStorage maintains all properties throughout the application lifecycle.
20
21Properties are accessed through unique string keys. They can be synchronized with UI components and accessed within the application's service logic.
22
23AppStorage enables UI state sharing among multiple [UIAbility](../../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md) instances within the application's [main thread](../../application-models/thread-model-stage.md).
24
25Properties in AppStorage support two-way synchronization and offer extended features, such as data persistence (see [PersistentStorage](arkts-persiststorage.md)). These UI states are implemented through service logic and decoupled from the UI. To use these UI states in the UI, the [@StorageProp](#storageprop) and [@StorageLink](#storagelink) decorators are required.
26
27## \@StorageProp
28
29As mentioned above, to bind AppStorage properties with custom components, the @StorageProp and @StorageLink decorators are required. Component variables decorated with \@StorageProp(key) or \@StorageLink(key) use the key to identify the corresponding property in AppStorage.
30
31During custom component initialization, the variable decorated with \@StorageProp(key) or \@StorageLink(key) is initialized using the value of the property corresponding to the specified key in AppStorage. Due to differences in application logic, it cannot be guaranteed that the corresponding property has been stored in the AppStorage instance before component initialization. Therefore, local initialization of variables decorated with \@StorageProp(key) or \@StorageLink(key) is necessary.
32
33\@StorageProp(key) establishes one-way data synchronization with the property corresponding to **key** in AppStorage:
34
351. When a local modification is made, the change will not be written back to AppStorage.
362. When the property corresponding to **key** in AppStorage is modified, the change will be synchronized to all properties bound to that key in AppStorage, overwriting any local modifications.
37
38> **NOTE**
39>
40> This decorator can be used in atomic services since API version 11.
41
42### Usage Rules
43
44| \@StorageProp Decorator| Description                                                        |
45| ----------------------- | ------------------------------------------------------------ |
46| Parameters             | **key**: constant string, mandatory (the string must be quoted)                 |
47| Allowed variable types     | Object, class, string, number, Boolean, enum, and array of these types.<br>(Applicable to API version 12 or later) **Map**, **Set**, **Date**, **undefined**, and **null**. For details about the scenarios of nested objects, see [Observed Changes and Behavior](#observed-changes-and-behavior).<br>(Applicable to API version 12 or later) Union type of the preceding types, for example, **string \| number, string \| undefined**, or **ClassA \| null**. For details, see [Using Union Types in AppStorage](#using-union-types-in-appstorage).<br>**Notice**<br>The variable type must be specified. Whenever possible, use the same type as that of the corresponding property in AppStorage. Otherwise, implicit type conversion occurs, causing application behavior exceptions.<br>When **undefined** or **null** is used, you are advised to explicitly specify the type to pass the TypeScript type check. For example, **@StorageProp('AA') a: number \| null = null** is supported, but **@StorageProp('AA') a: number = null** is not.<br>**any** is not supported.|
48| Synchronization type               | One-way: from the property in AppStorage to the component variable.<br>The component variable can be changed locally, but an update from AppStorage will overwrite local changes.|
49| Initial value for the decorated variable     | Mandatory. If the property does not exist in AppStorage, it will be created and initialized with this value.|
50
51### Variable Transfer/Access Rules
52
53| Transfer/Access     | Description                                      |
54| ---------- | ---------------------------------------- |
55| Initialization and update from the parent component| Prohibited. Only initialization using the property corresponding to the key in AppStorage is supported. If the corresponding key does not exist, initialization uses the local default value.|
56| Child component initialization    | Supported. Can be used to initialize variables decorated with [\@State](./arkts-state.md), [\@Link](./arkts-link.md), [\@Prop](./arkts-prop.md), or [\@Provide](./arkts-provide-and-consume.md).|
57| Access from outside the component | No                                      |
58
59  **Figure 1** \@StorageProp initialization rule
60
61![en-us_image_0000001552978157](figures/en-us_image_0000001552978157.png)
62
63### Observed Changes and Behavior
64
65**Observed Changes**
66
67- When the decorated variable is of the Boolean, string, or number type, its value change can be observed.
68
69- When the decorated variable is of the class or object type, the complete object reassignment and property-level changes can be observed. For details, see [Using AppStorage from Inside the UI](#using-appstorage-from-inside-the-ui).
70
71- When the decorated object is an array, changes including array item addition, deletion, and updates can be observed.
72
73- When the decorated object is of the Date type, the following changes can be observed: (1) complete **Date** object reassignment; (2) property changes caused by calling **setFullYear**, **setMonth**, **setDate**, **setHours**, **setMinutes**, **setSeconds**, **setMilliseconds**, **setTime**, **setUTCFullYear**, **setUTCMonth**, **setUTCDate**, **setUTCHours**, **setUTCMinutes**, **setUTCSeconds**, or **setUTCMilliseconds**. For details, see [Decorating Variables of the Date Type](#decorating-variables-of-the-date-type).
74
75- When the decorated object is of the **Map** type, the following changes can be observed: (1) complete **Map** object reassignment; (2) changes caused by calling **set**, **clear**, or **delete**. For details, see [Decorating Variables of the Map Type](#decorating-variables-of-the-map-type).
76
77- When the decorated object is of the **Set** type, the following changes can be observed: (1) complete **Set** object reassignment; (2) changes caused by calling **add**, **clear**, or **delete**. For details, see [Decorating Variables of the Set Type](#decorating-variables-of-the-set-type).
78
79**Framework Behavior**
80
811. When a variable decorated with \@StorageProp(key) is modified, the change will not be written back to the corresponding property in AppStorage. The change will trigger a re-render of the custom component and only applies to the private member variable of the current component. Other data bound to the same key will not be synchronized.
82
832. When the property corresponding to the given key in AppStorage changes, all variables decorated with \@StorageProp(key) will be synchronized and updated, and any local changes will be overwritten.
84
85## \@StorageLink
86
87> **NOTE**
88>
89> This decorator can be used in atomic services since API version 11.
90
91\@StorageLink(key) creates a two-way data synchronization between the variable it decorates and the property with the given key in AppStorage.
92
931. Local changes are synchronized to AppStorage.
94
952. Any change in AppStorage is synchronized to the properties bound to the given key in all scenarios, including one-way bound variables (\@StorageProp decorated variables and one-way bound variables created through [@Prop](./arkts-prop.md)), two-way bound variables (\@StorageLink decorated variables and two-way bound variables created through **link**), and other instances (such as PersistentStorage).
96
97### Usage Rules
98
99| \@StorageLink Decorator| Description                                                        |
100| ----------------------- | ------------------------------------------------------------ |
101| Parameters             | **key**: constant string, mandatory (the string must be quoted)                 |
102| Allowed variable types     | Object, class, string, number, Boolean, enum, and array of these types.<br>(Applicable to API version 12 or later) **Map**, **Set**, **Date**, **undefined**, and **null**. For details about the scenarios of nested objects, see [Observed Changes and Behavior](#observed-changes-and-behavior-1).<br>(Applicable to API version 12 or later) Union type of the preceding types, for example, **string \| number, string \| undefined**, or **ClassA \| null**. For details, see [Using Union Types in AppStorage](#using-union-types-in-appstorage).<br>**Notice**<br>The variable type must be specified. Whenever possible, use the same type as that of the corresponding property in AppStorage. Otherwise, implicit type conversion occurs, causing application behavior exceptions.<br>When **undefined** or **null** is used, you are advised to explicitly specify the type to pass the TypeScript type check. For example, **@StorageLink('AA') a: number \| null = null** is supported, but **@StorageLink('AA') a: number = null** is not.<br>**any** is not supported.|
103| Synchronization type               | Two-way: from the property in AppStorage to the custom component variable and vice versa|
104| Initial value for the decorated variable     | Mandatory. If the property does not exist in AppStorage, it will be created and initialized with this value.|
105
106
107### Variable Transfer/Access Rules
108
109| Transfer/Access     | Description                                      |
110| ---------- | ---------------------------------------- |
111| Initialization and update from the parent component| Forbidden.                                     |
112| Child component initialization    | Supported. The \@StorageLink decorated variable can be used to initialize a regular variable or an \@State, \@Link, \@Prop, or \@Provide decorated variable in the child component.|
113| Access from outside the component | No                                      |
114
115  **Figure 2** \@StorageLink initialization rule
116
117![en-us_image_0000001501938718](figures/en-us_image_0000001501938718.png)
118
119### Observed Changes and Behavior
120
121**Observed Changes**
122
123- When the decorated variable is of the Boolean, string, or number type, its value change can be observed.
124
125- When the decorated variable is of the class or object type, the complete object reassignment and property-level changes can be observed. For details, see [Using AppStorage from Inside the UI](#using-appstorage-from-inside-the-ui).
126
127- When the decorated object is an array, changes including array item addition, deletion, and updates can be observed.
128
129- When the decorated object is of the **Date** type, the following changes can be observed: (1) complete **Date** object reassignment; (2) property changes caused by calling **setFullYear**, **setMonth**, **setDate**, **setHours**, **setMinutes**, **setSeconds**, **setMilliseconds**, **setTime**, **setUTCFullYear**, **setUTCMonth**, **setUTCDate**, **setUTCHours**, **setUTCMinutes**, **setUTCSeconds**, or **setUTCMilliseconds**. For details, see [Decorating Variables of the Date Type](#decorating-variables-of-the-date-type).
130
131- When the decorated object is of the **Map** type, the following changes can be observed: (1) complete **Map** object reassignment; (2) changes caused by calling **set**, **clear**, or **delete**. For details, see [Decorating Variables of the Map Type](#decorating-variables-of-the-map-type).
132
133- When the decorated object is of the **Set** type, the following changes can be observed: (1) complete **Set** object reassignment; (2) changes caused by calling **add**, **clear**, or **delete**. For details, see [Decorating Variables of the Set Type](#decorating-variables-of-the-set-type).
134
135**Framework Behavior**
136
1371. Changes to variables decorated with \@StorageLink(key) are automatically written back to the corresponding property in AppStorage.
138
1392. When the value of a key in AppStorage changes, all data bound to that key (including both two-way binding with \@StorageLink and one-way binding with \@StorageProp) will be synchronized.
140
1413. When a variable decorated by \@StorageLink(key) is updated, the change is synchronized back to the corresponding key in AppStorage and triggers re-rendering of the owning custom component.
142
143
144## Constraints
145
1461. The parameter of \@StorageProp and \@StorageLink must be of the string type. Otherwise, an error is reported during compilation.
147
148    ```ts
149    AppStorage.setOrCreate('propA', 47);
150
151    // Incorrect format. An error is reported during compilation.
152    @StorageProp() storageProp: number = 1;
153    @StorageLink() storageLink: number = 2;
154
155    // Correct usage.
156    @StorageProp('propA') storageProp: number = 1;
157    @StorageLink('propA') storageLink: number = 2;
158    ```
159
1602. \@StorageProp and \@StorageLink cannot decorate variables of the function type. Otherwise, the framework throws a runtime error.
161
1623. When using AppStorage together with [PersistentStorage](arkts-persiststorage.md) and [Environment](arkts-environment.md), pay attention to the following:
163
164    1. If a property exists in AppStorage before PersistentStorage.[persistProp](../../reference/apis-arkui/arkui-ts/ts-state-management.md#persistpropdeprecated) is called, the value in AppStorage will overwrite the value in PersistentStorage. As such, when possible, initialize PersistentStorage properties first. For an example with incorrect usage, see [Accessing a Property in AppStorage Before PersistentStorage](arkts-persiststorage.md#accessing-a-property-in-appstorage-before-persistentstorage).
165
166    2. If a property has already been created in AppStorage, subsequent calls to Environment.[envProp](../../reference/apis-arkui/arkui-ts/ts-state-management.md#envprop10) to create a property with the same name will fail. Since AppStorage already contains a property with that name, the Environment variable will not be written to AppStorage. Therefore, avoid using the preset Environment variable names in AppStorage.
167
1684. Changes to variables decorated with state decorators will trigger UI re-rendering. If a variable is modified only for message passing (not for UI updates), using the **emitter** API is recommended. For the example, see [Avoiding @StorageLink for Event Notification](#avoiding-storagelink-for-event-notification).
169
1705. AppStorage is shared within the same process. Since the UIAbility and <!--Del-->[<!--DelEnd-->UIExtensionAbility<!--Del-->](../../application-models/uiextensionability.md)<!--DelEnd--> run in separate processes, the UIExtensionAbility does not share the AppStorage of the main process.
171
172## Use Scenarios
173
174### Using AppStorage and LocalStorage in Application Logic
175
176AppStorage is implemented as a singleton, with all its APIs exposed as static methods. How these APIs work resembles the non-static APIs of LocalStorage.
177
178```ts
179AppStorage.setOrCreate('propA', 47);
180
181let storage: LocalStorage = new LocalStorage();
182storage.setOrCreate('propA',17);
183let propA: number | undefined = AppStorage.get('propA'); // propA in AppStorage == 47, propA in LocalStorage == 17
184let link1: SubscribedAbstractProperty<number> = AppStorage.link('propA'); // link1.get() == 47
185let link2: SubscribedAbstractProperty<number> = AppStorage.link('propA'); // link2.get() == 47
186let prop: SubscribedAbstractProperty<number> = AppStorage.prop('propA'); // prop.get() == 47
187
188link1.set(48); // Two-way synchronization: link1.get() == link2.get() == prop.get() == 48
189prop.set(1); // One-way synchronization: prop.get() == 1; but link1.get() == link2.get() == 48
190link1.set(49); // Two-way synchronization: link1.get() == link2.get() == prop.get() == 49
191
192storage.get<number>('propA') // == 17
193storage.set('propA', 101);
194storage.get<number>('propA') // == 101
195
196AppStorage.get<number>('propA') // == 49
197link1.get() // == 49
198link2.get() // == 49
199prop.get() // == 49
200```
201
202
203### Using AppStorage from Inside the UI
204
205@StorageLink works in conjunction with AppStorage to establish two-way data synchronization using properties stored in AppStorage.
206@StorageProp works in conjunction with AppStorage to establish one-way data synchronization using properties stored in AppStorage.
207
208```ts
209class Data {
210  code: number;
211
212  constructor(code: number) {
213    this.code = code;
214  }
215}
216
217AppStorage.setOrCreate('propA', 47);
218AppStorage.setOrCreate('propB', new Data(50));
219let storage = new LocalStorage();
220storage.setOrCreate('linkA', 48);
221storage.setOrCreate('linkB', new Data(100));
222
223@Entry(storage)
224@Component
225struct Index {
226  @StorageLink('propA') storageLink: number = 1;
227  @StorageProp('propA') storageProp: number = 1;
228  @StorageLink('propB') storageLinkObject: Data = new Data(1);
229  @StorageProp('propB') storagePropObject: Data = new Data(1);
230
231  build() {
232    Column({ space: 20 }) {
233      // StorageLink establishes a two-way synchronization with AppStorage; local changes will be synchronized back to the value of key 'propA' in AppStorage.
234      Text(`storageLink ${this.storageLink}`)
235        .onClick(() => {
236          this.storageLink += 1;
237        })
238
239      // @StorageProp establishes a one-way synchronization with AppStorage; local changes will not be synchronized back to the value of key 'propA' in AppStorage.
240      // However, its value can be updated using the set/setOrCreate APIs of AppStorage.
241      Text(`storageProp ${this.storageProp}`)
242        .onClick(() => {
243          this.storageProp += 1;
244        })
245
246      // Although AppStorage APIs can obtain values, they do not have the capability to refresh the UI (the value change can be seen in logs).
247      // A connection with the custom component can only be established to refresh the UI by relying on @StorageLink/@StorageProp.
248      Text(`change by AppStorage: ${AppStorage.get<number>('propA')}`)
249        .onClick(() => {
250          console.info(`Appstorage.get: ${AppStorage.get<number>('propA')}`);
251          AppStorage.set<number>('propA', 100);
252        })
253
254      Text(`storageLinkObject ${this.storageLinkObject.code}`)
255        .onClick(() => {
256          this.storageLinkObject.code += 1;
257        })
258
259      Text(`storagePropObject ${this.storagePropObject.code}`)
260        .onClick(() => {
261          this.storagePropObject.code += 1;
262        })
263    }
264  }
265}
266```
267
268### Using Union Types in AppStorage
269
270The following example demonstrates how to use union types in LocalStorage. The type of variable **linkA** is **number | null**, and the type of variable **linkB** is **number | undefined**. The **Text** components display **null** and **undefined** upon initialization, numbers when clicked, and **null** and **undefined** when clicked again.
271
272```ts
273@Component
274struct StorageLinkComponent {
275  @StorageLink('linkA') linkA: number | null = null;
276  @StorageLink('linkB') linkB: number | undefined = undefined;
277
278  build() {
279    Column() {
280      Text('@StorageLink initialization, @StorageLink value')
281      Text(this.linkA + '').fontSize(20).onClick(() => {
282        this.linkA ? this.linkA = null : this.linkA = 1;
283      })
284      Text(this.linkB + '').fontSize(20).onClick(() => {
285        this.linkB ? this.linkB = undefined : this.linkB = 1;
286      })
287    }
288    .borderWidth(3).borderColor(Color.Red)
289  }
290}
291
292@Component
293struct StoragePropComponent {
294  @StorageProp('propA') propA: number | null = null;
295  @StorageProp('propB') propB: number | undefined = undefined;
296
297  build() {
298    Column() {
299      Text('@StorageProp initialization, @StorageProp value')
300      Text(this.propA + '').fontSize(20).onClick(() => {
301        this.propA ? this.propA = null : this.propA = 1;
302      })
303      Text(this.propB + '').fontSize(20).onClick(() => {
304        this.propB ? this.propB = undefined : this.propB = 1;
305      })
306    }
307    .borderWidth(3).borderColor(Color.Blue)
308  }
309}
310
311@Entry
312@Component
313struct Index {
314  build() {
315    Row() {
316      Column() {
317        StorageLinkComponent()
318        StoragePropComponent()
319      }
320      .width('100%')
321    }
322    .height('100%')
323  }
324}
325```
326
327### Decorating Variables of the Date Type
328
329> **NOTE**
330>
331> AppStorage supports the Date type since API version 12.
332
333In this example, the **selectedDate** variable decorated with @StorageLink is of the Date type. After the button is clicked, the value of **selectedDate** changes, and the UI is re-rendered.
334
335```ts
336@Entry
337@Component
338struct DateSample {
339  @StorageLink('date') selectedDate: Date = new Date('2021-08-08');
340
341  build() {
342    Column() {
343      Button('set selectedDate to 2023-07-08')
344        .margin(10)
345        .onClick(() => {
346          AppStorage.setOrCreate('date', new Date('2023-07-08'));
347        })
348      Button('increase the year by 1')
349        .margin(10)
350        .onClick(() => {
351          this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1);
352        })
353      Button('increase the month by 1')
354        .margin(10)
355        .onClick(() => {
356          this.selectedDate.setMonth(this.selectedDate.getMonth() + 1);
357        })
358      Button('increase the day by 1')
359        .margin(10)
360        .onClick(() => {
361          this.selectedDate.setDate(this.selectedDate.getDate() + 1);
362        })
363      DatePicker({
364        start: new Date('1970-1-1'),
365        end: new Date('2100-1-1'),
366        selected: $$this.selectedDate
367      })
368    }.width('100%')
369  }
370}
371```
372
373### Decorating Variables of the Map Type
374
375> **NOTE**
376>
377> AppStorage supports the Map type since API version 12.
378
379In this example, the **message** variable decorated with @StorageLink is of the Map\<number, string\> type. After the button is clicked, the value of **message** changes, and the UI is re-rendered.
380
381```ts
382@Entry
383@Component
384struct MapSample {
385  @StorageLink('map') message: Map<number, string> = new Map([[0, 'a'], [1, 'b'], [3, 'c']]);
386
387  build() {
388    Row() {
389      Column() {
390        ForEach(Array.from(this.message.entries()), (item: [number, number]) => {
391          Text(`${item[0]}`).fontSize(30)
392          Text(`${item[1]}`).fontSize(30)
393          Divider()
394        })
395        Button('init map').onClick(() => {
396          this.message = new Map([[0, 'a'], [1, 'b'], [3, 'c']]);
397        })
398        Button('set new one').onClick(() => {
399          this.message.set(4, 'd');
400        })
401        Button('clear').onClick(() => {
402          this.message.clear();
403        })
404        Button('replace the existing one').onClick(() => {
405          this.message.set(0, 'aa');
406        })
407        Button('delete the existing one').onClick(() => {
408          AppStorage.get<Map<number, string>>('map')?.delete(0);
409        })
410      }
411      .width('100%')
412    }
413    .height('100%')
414  }
415}
416```
417
418### Decorating Variables of the Set Type
419
420> **NOTE**
421>
422> AppStorage supports the Set type since API version 12.
423
424In this example, the **memberSet** variable decorated with @StorageLink is of the Set\<number\> type. After the button is clicked, the value of **memberSet** changes, and the UI is re-rendered.
425
426```ts
427@Entry
428@Component
429struct SetSample {
430  @StorageLink('set') memberSet: Set<number> = new Set([0, 1, 2, 3, 4]);
431
432  build() {
433    Row() {
434      Column() {
435        ForEach(Array.from(this.memberSet.entries()), (item: [number, string]) => {
436          Text(`${item[0]}`)
437            .fontSize(30)
438          Divider()
439        })
440        Button('init set')
441          .onClick(() => {
442            this.memberSet = new Set([0, 1, 2, 3, 4]);
443          })
444        Button('set new one')
445          .onClick(() => {
446            AppStorage.get<Set<number>>('set')?.add(5);
447          })
448        Button('clear')
449          .onClick(() => {
450            this.memberSet.clear();
451          })
452        Button('delete the first one')
453          .onClick(() => {
454            this.memberSet.delete(0);
455          })
456      }
457      .width('100%')
458    }
459    .height('100%')
460  }
461}
462```
463
464## AppStorage Usage Recommendations
465
466### Avoiding @StorageLink for Event Notification
467
468For performance considerations, using the two-way synchronization mechanism between @StorageLink and AppStorage for event notification is not recommended: (1) Variables in AppStorage may be bound to components across multiple pages, while event notifications typically do not need to be broadcast to all these components. (2) When @StorageLink decorated variables are used in UI, changes trigger component re-renders, causing performance overhead even when no visual updates are required.
469
470In the following example, any click event in the **TapImage** component will trigger a change of the **tapIndex** property. As @StorageLink establishes a two-way data synchronization with AppStorage, the local change is synchronized to AppStorage. As a result, all custom components bound to the **tapIndex** property in AppStorage are notified of the change. After @Watch observes the change to **tapIndex**, the state variable **tapColor** is updated, triggering UI re-rendering. (Because **tapIndex** is not directly bound to the UI, its change does not directly trigger UI re-rendering.)
471
472When using this mechanism for event notification, make sure the property in AppStorage is not directly bound to the UI and control the complexity of the [@Watch](./arkts-watch.md) function:If the @Watch function execution time is too long, it will impact UI re-rendering efficiency.
473
474```ts
475// xxx.ets
476class ViewData {
477  title: string;
478  uri: Resource;
479  color: Color = Color.Black;
480
481  constructor(title: string, uri: Resource) {
482    this.title = title;
483    this.uri = uri;
484  }
485}
486
487@Entry
488@Component
489struct Gallery {
490  // 'app.media.icon' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
491  dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))];
492  scroller: Scroller = new Scroller();
493
494  build() {
495    Column() {
496      Grid(this.scroller) {
497        ForEach(this.dataList, (item: ViewData, index?: number) => {
498          GridItem() {
499            TapImage({
500              uri: item.uri,
501              index: index
502            })
503          }.aspectRatio(1)
504
505        }, (item: ViewData, index?: number) => {
506          return JSON.stringify(item) + index;
507        })
508      }.columnsTemplate('1fr 1fr')
509    }
510
511  }
512}
513
514@Component
515export struct TapImage {
516  @StorageLink('tapIndex') @Watch('onTapIndexChange') tapIndex: number = -1;
517  @State tapColor: Color = Color.Black;
518  private index: number = 0;
519  private uri: Resource = {
520    id: 0,
521    type: 0,
522    moduleName: '',
523    bundleName: ''
524  };
525
526  // Check whether the component is selected.
527  onTapIndexChange() {
528    if (this.tapIndex >= 0 && this.index === this.tapIndex) {
529      console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, red`);
530      this.tapColor = Color.Red;
531    } else {
532      console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, black`);
533      this.tapColor = Color.Black;
534    }
535  }
536
537  build() {
538    Column() {
539      Image(this.uri)
540        .objectFit(ImageFit.Cover)
541        .onClick(() => {
542          this.tapIndex = this.index;
543        })
544        .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
545    }
546
547  }
548}
549```
550
551Compared with the use of @StorageLink, the use of **emit** allows you to subscribe to events and receive event callbacks, thereby reducing overhead and improving code readability.
552
553> **NOTE**
554>
555> The **emit** API is not available in DevEco Studio Previewer.
556
557
558```ts
559// xxx.ets
560import { emitter } from '@kit.BasicServicesKit';
561
562let nextId: number = 0;
563
564class ViewData {
565  title: string;
566  uri: Resource;
567  color: Color = Color.Black;
568  id: number;
569
570  constructor(title: string, uri: Resource) {
571    this.title = title;
572    this.uri = uri;
573    this.id = nextId++;
574  }
575}
576
577@Entry
578@Component
579struct Gallery {
580  // 'app.media.icon' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
581  dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))];
582  scroller: Scroller = new Scroller();
583  private preIndex: number = -1;
584
585  build() {
586    Column() {
587      Grid(this.scroller) {
588        ForEach(this.dataList, (item: ViewData) => {
589          GridItem() {
590            TapImage({
591              uri: item.uri,
592              index: item.id
593            })
594          }.aspectRatio(1)
595          .onClick(() => {
596            if (this.preIndex === item.id) {
597              return;
598            }
599            let innerEvent: emitter.InnerEvent = { eventId: item.id };
600            // Selected: from black to red
601            let eventData: emitter.EventData = {
602              data: {
603                'colorTag': 1
604              }
605            };
606            emitter.emit(innerEvent, eventData);
607
608            if (this.preIndex != -1) {
609              console.info(`preIndex: ${this.preIndex}, index: ${item.id}, black`);
610              let innerEvent: emitter.InnerEvent = { eventId: this.preIndex };
611              // Deselected: from red to black
612              let eventData: emitter.EventData = {
613                data: {
614                  'colorTag': 0
615                }
616              };
617              emitter.emit(innerEvent, eventData);
618            }
619            this.preIndex = item.id;
620          })
621        }, (item: ViewData) => JSON.stringify(item))
622      }.columnsTemplate('1fr 1fr')
623    }
624
625  }
626}
627
628@Component
629export struct TapImage {
630  @State tapColor: Color = Color.Black;
631  private index: number = 0;
632  private uri: Resource = {
633    id: 0,
634    type: 0,
635    moduleName: '',
636    bundleName: ''
637  };
638
639  onTapIndexChange(colorTag: emitter.EventData) {
640    if (colorTag.data != null) {
641      this.tapColor = colorTag.data.colorTag ? Color.Red : Color.Black
642    }
643  }
644
645  aboutToAppear() {
646    // Define the event ID.
647    let innerEvent: emitter.InnerEvent = { eventId: this.index };
648    emitter.on(innerEvent, data => {
649      this.onTapIndexChange(data);
650    });
651  }
652
653  build() {
654    Column() {
655      Image(this.uri)
656        .objectFit(ImageFit.Cover)
657        .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
658    }
659  }
660}
661```
662
663The preceding notification logic is simple. It can be simplified into a ternary expression as follows:
664
665```ts
666// xxx.ets
667class ViewData {
668  title: string;
669  uri: Resource;
670  color: Color = Color.Black;
671
672  constructor(title: string, uri: Resource) {
673    this.title = title;
674    this.uri = uri;
675  }
676}
677
678@Entry
679@Component
680struct Gallery {
681  // 'app.media.icon' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
682  dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))];
683  scroller: Scroller = new Scroller();
684
685  build() {
686    Column() {
687      Grid(this.scroller) {
688        ForEach(this.dataList, (item: ViewData, index?: number) => {
689          GridItem() {
690            TapImage({
691              uri: item.uri,
692              index: index
693            })
694          }.aspectRatio(1)
695
696        }, (item: ViewData, index?: number) => {
697          return JSON.stringify(item) + index;
698        })
699      }.columnsTemplate('1fr 1fr')
700    }
701
702  }
703}
704
705@Component
706export struct TapImage {
707  @StorageLink('tapIndex') tapIndex: number = -1;
708  private index: number = 0;
709  private uri: Resource = {
710    id: 0,
711    type: 0,
712    moduleName: '',
713    bundleName: ''
714  };
715
716  build() {
717    Column() {
718      Image(this.uri)
719        .objectFit(ImageFit.Cover)
720        .onClick(() => {
721          this.tapIndex = this.index;
722        })
723        .border({
724          width: 5,
725          style: BorderStyle.Dotted,
726          color: (this.tapIndex >= 0 && this.index === this.tapIndex) ? Color.Red : Color.Black
727        })
728    }
729  }
730}
731```
732
733
734### Notes on Update Rules When @StorageProp Is Used with AppStorage APIs
735
736When a key's value is updated using the **setOrCreate** or** set** API, if the new value is identical to the existing one, **setOrCreate** will not trigger updates for \@StorageLink or \@StorageProp. In addition, since \@StorageProp keeps its own local data copy, modifying this local value directly will not synchronize the change back to AppStorage. This can lead to a common misunderstanding: You may assume you have updated the value via AppStorage, only to find that the @StorageProp value remains unchanged in practice.
737Example:
738
739```ts
740AppStorage.setOrCreate('propA', false);
741
742@Entry
743@Component
744struct Index {
745  @StorageProp('propA') @Watch('onChange') propA: boolean = false;
746
747  onChange() {
748    console.info(`propA change`);
749  }
750
751  aboutToAppear(): void {
752    this.propA = true;
753  }
754
755  build() {
756    Column() {
757      Text(`${this.propA}`)
758      Button('change')
759        .onClick(() => {
760          AppStorage.setOrCreate('propA', false);
761          console.info(`propA: ${this.propA}`);
762        })
763    }
764  }
765}
766```
767
768In the preceding example, the value of **PropA** has been changed to **true** locally before the click event, while the value stored in AppStorage remains **false**. When the click event attempts to update the value of **PropA** to **false** through the **setOrCreate** API, no synchronization is triggered because the value in AppStorage is already **false** (matching the new value). As a result, the value of the @StorageProp decorated variable remains **true**.
769
770To achieve synchronization, there are two approaches:
7711. Change \@StorageProp to \@StorageLink.
7722. Use **AppStorage.setOrCreate('propA', true)** to change the local value.
773