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 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 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