1# AppStorage: Application-wide UI State Storage 2 3 4AppStorage provides central storage for application UI state attributes. It is bound to the application process and is created by the UI framework at application startup. 5 6 7Unlike LocalStorage, which is usually used for page-level state sharing, AppStorage enables application-wide UI state sharing. AppStorage is equivalent to the hub of the entire application. [PersistentStorage](arkts-persiststorage.md) and [Environment](arkts-environment.md) data is passed first to AppStorage and then from AppStorage to the UI component. 8 9 10This topic describes the AppStorage use scenarios and related decorators: \@StorageProp and \@StorageLink. 11 12 13## Overview 14 15AppStorage is a singleton object that is created at application startup. Its purpose is to provide central storage for application UI state attributes. AppStorage retains all those attributes and their values as long as the application remains running. Each attribute is accessed using a unique key, which is a string. 16 17UI components synchronize application state attributes with AppStorage. AppStorage can be accessed during implementation of application service logic as well. 18 19AppStorage supports state sharing among multiple UIAbility instances in the [main thread](../application-models/thread-model-stage.md) of an application. 20 21Selected state attributes of AppStorage can be synchronized with different data sources or data sinks. Those data sources and sinks can be on a local or remote device, and have different capabilities, such as data persistence (see [PersistentStorage](arkts-persiststorage.md)). These data sources and sinks are implemented in the service logic, and separated from the UI. Link to [@StorageProp](#storageprop) and [@StorageLink](#storagelink) those AppStorage attributes whose values should be kept until application re-start. 22 23 24## \@StorageProp 25 26As mentioned above, if you want to establish a binding between AppStorage and a custom component, you'll need the \@StorageProp or \@StorageLink decorator. Use \@StorageProp(key) or \@StorageLink(key) to decorate variables in the component, where **key** identifies an attribute in AppStorage. 27 28When a custom component is initialized, the attribute value corresponding to the key in AppStorage is used to initialize the \@StorageProp(key) or \@StorageLink(key) decorated variable. Whether the attribute with the given key exists in AppStorage depends on the application logic. This means that it may be missing from AppStorage. In light of this, local initialization is mandatory for the \@StorageProp(key) or \@StorageLink(key) decorated variable. 29 30By decorating a variable with \@StorageProp(key), a one-way data synchronization is established from the attribute with the given key in AppStorage to the variable. A local change can be made, but it will not be synchronized to AppStorage. An update to the attribute with the given key in AppStorage will overwrite local changes. 31 32 33### Rules of Use 34 35| \@StorageProp Decorator| Description | 36| ----------------------- | ------------------------------------------------------------ | 37| Decorator parameters | **key**: constant string, mandatory (the string must be quoted) | 38| Allowed variable types | Object, class, string, number, Boolean, enum, and array of these types. For details about the scenarios of nested objects, see [Observed Changes and Behavior](#observed-changes-and-behavior).<br>The type must be specified. Whenever possible, use the same type as that of the corresponding attribute in AppStorage. Otherwise, implicit type conversion occurs, which may cause application behavior exceptions. **any** is not supported. The **undefined** and **null** values are not allowed.| 39| Synchronization type | One-way: from the attribute in AppStorage to the component variable.<br>The component variable can be changed locally, but an update from AppStorage will overwrite local changes.| 40| Initial value for the decorated variable | Mandatory. It is used as the default value for initialization if the corresponding attribute does not exist in AppStorage.| 41 42 43### Variable Transfer/Access Rules 44 45| Transfer/Access | Description | 46| ---------- | ---------------------------------------- | 47| Initialization and update from the parent component| Forbidden.| 48| Child component initialization | Supported. The \@StorageProp decorated variable can be used to initialize an \@State, \@Link, \@Prop, or \@Provide decorated variable in the child component.| 49| Access from outside the component | Not supported. | 50 51 52 **Figure 1** \@StorageProp initialization rule 53 54 55 56 57 58### Observed Changes and Behavior 59 60**Observed Changes** 61 62 63- When the decorated variable is of the Boolean, string, or number type, its value change can be observed. 64 65- When the decorated variable is of the class or object type, its value change as well as value changes of all its attributes (the attributes that **Object.keys(observedObject)** returns) can be observed. 66 67- When the decorated variable is of the array type, the addition, deletion, and updates of array items can be observed. 68 69 70**Framework Behavior** 71 72 73- When the value change of the \@StorageProp(key) decorated variable is observed, the change is not synchronized to the attribute with the given key in AppStorage. 74 75- The value change of the \@StorageProp(key) decorated variable only applies to the private member variables of the current component, but not other variables bound to the key. 76 77- When the data decorated by \@StorageProp(key) is a state variable, the change of the data is not synchronized to AppStorage, but the owning custom component is re-rendered. 78 79- When the attribute with the given key in AppStorage is updated, the change is synchronized to all the \@StorageProp(key) decorated data, and the local changes of the data are overwritten. 80 81 82## \@StorageLink 83 84\@StorageLink(key) creates a two-way data synchronization between the variable it decorates and the attribute with the given key in AppStorage. 85 861. Local changes are synchronized to AppStorage. 87 882. Any change in AppStorage is synchronized to the attribute with the given key in all scenarios, including one-way bound variables (\@StorageProp decorated variables and one-way bound variables created through \@Prop), two-way bound variables (\@StorageLink decorated variables and two-way bound variables created through \@Link), and other instances (such as PersistentStorage). 89 90 91### Rules of Use 92 93| \@StorageLink Decorator| Description | 94| ------------------ | ---------------------------------------- | 95| Decorator parameters | **key**: constant string, mandatory (the string must be quoted) | 96| Allowed variable types | Object, class, string, number, Boolean, enum, and array of these types. For details about the scenarios of nested objects, see [Observed Changes and Behavior](#observed-changes-and-behavior-1).<br>The type must be specified. Whenever possible, use the same type as that of the corresponding attribute in AppStorage. Otherwise, implicit type conversion occurs, which may cause application behavior exceptions. **any** is not supported. The **undefined** and **null** values are not allowed.| 97| Synchronization type | Two-way: from the attribute in AppStorage to the custom component variable and vice versa| 98| Initial value for the decorated variable | Mandatory. It is used as the default value for initialization if the corresponding attribute does not exist in AppStorage.| 99 100 101### Variable Transfer/Access Rules 102 103| Transfer/Access | Description | 104| ---------- | ---------------------------------------- | 105| Initialization and update from the parent component| Forbidden. | 106| 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.| 107| Access from outside the component | Not supported. | 108 109 110 **Figure 2** \@StorageLink initialization rule 111 112 113 114 115 116### Observed Changes and Behavior 117 118**Observed Changes** 119 120 121- When the decorated variable is of the Boolean, string, or number type, its value change can be observed. 122 123- When the decorated variable is of the class or object type, its value change as well as value changes of all its attributes (the attributes that **Object.keys(observedObject)** returns) can be observed. 124 125- When the decorated variable is of the array type, the addition, deletion, and updates of array items can be observed. 126 127 128**Framework Behavior** 129 130 1311. When the value change of the \@StorageLink(key) decorated variable is observed, the change is synchronized to the attribute with the given key in AppStorage. 132 1332. Once the attribute with the given key in AppStorage is updated, all the data (including \@StorageLink and \@StorageProp decorated variables) bound to the key is changed synchronously. 134 1353. When the data decorated by \@StorageLink(key) is a state variable, its change is synchronized to AppStorage, and the owning custom component is re-rendered. 136 137 138## Use Scenarios 139 140 141### Example of Using AppStorage and LocalStorage in Application Logic 142 143Since AppStorage is a singleton, its APIs are all static. How these APIs work resembles the non-static APIs of LocalStorage. 144 145 146```ts 147AppStorage.setOrCreate('PropA', 47); 148 149let storage: LocalStorage = new LocalStorage(); 150storage.setOrCreate('PropA',17); 151let propA: number | undefined = AppStorage.get('PropA') // propA in AppStorage == 47, propA in LocalStorage == 17 152let link1: SubscribedAbstractProperty<number> = AppStorage.link('PropA'); // link1.get() == 47 153let link2: SubscribedAbstractProperty<number> = AppStorage.link('PropA'); // link2.get() == 47 154let prop: SubscribedAbstractProperty<number> = AppStorage.prop('PropA'); // prop.get() == 47 155 156link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48 157prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48 158link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49 159 160storage.get<number>('PropA') // == 17 161storage.set('PropA', 101); 162storage.get<number>('PropA') // == 101 163 164AppStorage.get<number>('PropA') // == 49 165link1.get() // == 49 166link2.get() // == 49 167prop.get() // == 49 168``` 169 170 171### Example of Using AppStorage and LocalStorage Inside the UI 172 173\@StorageLink works together with AppStorage in the same way as \@LocalStorageLink works together with LocalStorage. It creates two-way data synchronization with an attribute in AppStorage. 174 175 176```ts 177AppStorage.setOrCreate('PropA', 47); 178let storage = new LocalStorage(); 179storage.setOrCreate('PropA', 48); 180 181@Entry(storage) 182@Component 183struct CompA { 184 @StorageLink('PropA') storageLink: number = 1; 185 @LocalStorageLink('PropA') localStorageLink: number = 1; 186 187 build() { 188 Column({ space: 20 }) { 189 Text(`From AppStorage ${this.storageLink}`) 190 .onClick(() => { 191 this.storageLink += 1 192 }) 193 194 Text(`From LocalStorage ${this.localStorageLink}`) 195 .onClick(() => { 196 this.localStorageLink += 1 197 }) 198 } 199 } 200} 201``` 202 203### Unrecommended: Using @StorageLink to Implement Event Notification 204 205Compared with the common mechanism for event notification, the two-way synchronization mechanism of @StorageLink and AppStorage is expensive in two aspects: (1) Variables in AppStorage may be bound to components on different pages, but the event notifications may not need to be sent to all these components; (2) Any change to the @StorageLink decorated variables may cause costly UI re-rendering. 206 207In the following example, any tap event in the **TapImage** component will trigger a change of the **tapIndex** attribute. As @StorageLink establishes a two-way data synchronization with AppStorage, the local change is synchronized to AppStorage. As a result, all custom components owning the **tapIndex** attribute bound to AppStorage are notified of the change. After @Watch observes the change to **tapIndex**, the state variable **tapColor** is updated, and the UI is re-rendered. (Because **tapIndex** is not directly bound to the UI, its change does not directly trigger UI re-rendering.) 208 209To use the preceding mechanism to implement event notification, ensure that variables in AppStorage are not directly bound to the UI and the @Watch decorated function is as simple as possible. (If the @Watch decorated function takes a long time to execute, the UI re-rendering efficiency will be affected.) 210 211 212```ts 213// xxx.ets 214class ViewData { 215 title: string; 216 uri: Resource; 217 color: Color = Color.Black; 218 219 constructor(title: string, uri: Resource) { 220 this.title = title; 221 this.uri = uri 222 } 223} 224 225@Entry 226@Component 227struct Gallery2 { 228 dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))] 229 scroller: Scroller = new Scroller() 230 231 build() { 232 Column() { 233 Grid(this.scroller) { 234 ForEach(this.dataList, (item: ViewData, index?: number) => { 235 GridItem() { 236 TapImage({ 237 uri: item.uri, 238 index: index 239 }) 240 }.aspectRatio(1) 241 242 }, (item: ViewData, index?: number) => { 243 return JSON.stringify(item) + index; 244 }) 245 }.columnsTemplate('1fr 1fr') 246 } 247 248 } 249} 250 251@Component 252export struct TapImage { 253 @StorageLink('tapIndex') @Watch('onTapIndexChange') tapIndex: number = -1; 254 @State tapColor: Color = Color.Black; 255 private index: number = 0; 256 private uri: Resource = { 257 id: 0, 258 type: 0, 259 moduleName: "", 260 bundleName: "" 261 }; 262 263 // Check whether the component is selected. 264 onTapIndexChange() { 265 if (this.tapIndex >= 0 && this.index === this.tapIndex) { 266 console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, red`) 267 this.tapColor = Color.Red; 268 } else { 269 console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, black`) 270 this.tapColor = Color.Black; 271 } 272 } 273 274 build() { 275 Column() { 276 Image(this.uri) 277 .objectFit(ImageFit.Cover) 278 .onClick(() => { 279 this.tapIndex = this.index; 280 }) 281 .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor }) 282 } 283 284 } 285} 286``` 287 288Compared with the use of @StorageLink, the use of **emit** implements event notification with less overhead, by allowing you to subscribe to an event and receive an event callback. 289 290> **NOTE** 291> 292> The **emit** API is not available in DevEco Studio Previewer. 293 294 295```ts 296// xxx.ets 297import emitter from '@ohos.events.emitter'; 298 299let NextID: number = 0; 300 301class ViewData { 302 title: string; 303 uri: Resource; 304 color: Color = Color.Black; 305 id: number; 306 307 constructor(title: string, uri: Resource) { 308 this.title = title; 309 this.uri = uri 310 this.id = NextID++; 311 } 312} 313 314@Entry 315@Component 316struct Gallery2 { 317 dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))] 318 scroller: Scroller = new Scroller() 319 private preIndex: number = -1 320 321 build() { 322 Column() { 323 Grid(this.scroller) { 324 ForEach(this.dataList, (item: ViewData) => { 325 GridItem() { 326 TapImage({ 327 uri: item.uri, 328 index: item.id 329 }) 330 }.aspectRatio(1) 331 .onClick(() => { 332 if (this.preIndex === item.id) { 333 return 334 } 335 let innerEvent: emitter.InnerEvent = { eventId: item.id } 336 // Selected: from black to red 337 let eventData: emitter.EventData = { 338 data: { 339 "colorTag": 1 340 } 341 } 342 emitter.emit(innerEvent, eventData) 343 344 if (this.preIndex != -1) { 345 console.info(`preIndex: ${this.preIndex}, index: ${item.id}, black`) 346 let innerEvent: emitter.InnerEvent = { eventId: this.preIndex } 347 // Deselected: from red to black 348 let eventData: emitter.EventData = { 349 data: { 350 "colorTag": 0 351 } 352 } 353 emitter.emit(innerEvent, eventData) 354 } 355 this.preIndex = item.id 356 }) 357 }, (item: ViewData) => JSON.stringify(item)) 358 }.columnsTemplate('1fr 1fr') 359 } 360 361 } 362} 363 364@Component 365export struct TapImage { 366 @State tapColor: Color = Color.Black; 367 private index: number = 0; 368 private uri: Resource = { 369 id: 0, 370 type: 0, 371 moduleName: "", 372 bundleName: "" 373 }; 374 375 onTapIndexChange(colorTag: emitter.EventData) { 376 if (colorTag.data != null) { 377 this.tapColor = colorTag.data.colorTag ? Color.Red : Color.Black 378 } 379 } 380 381 aboutToAppear() { 382 // Define the event ID. 383 let innerEvent: emitter.InnerEvent = { eventId: this.index } 384 emitter.on(innerEvent, data => { 385 this.onTapIndexChange(data) 386 }) 387 } 388 389 build() { 390 Column() { 391 Image(this.uri) 392 .objectFit(ImageFit.Cover) 393 .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor }) 394 } 395 } 396} 397``` 398 399The preceding notification logic is simple. It can be simplified into a ternary expression as follows: 400 401```ts 402// xxx.ets 403class ViewData { 404 title: string; 405 uri: Resource; 406 color: Color = Color.Black; 407 408 constructor(title: string, uri: Resource) { 409 this.title = title; 410 this.uri = uri 411 } 412} 413 414@Entry 415@Component 416struct Gallery2 { 417 dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))] 418 scroller: Scroller = new Scroller() 419 420 build() { 421 Column() { 422 Grid(this.scroller) { 423 ForEach(this.dataList, (item: ViewData, index?: number) => { 424 GridItem() { 425 TapImage({ 426 uri: item.uri, 427 index: index 428 }) 429 }.aspectRatio(1) 430 431 }, (item: ViewData, index?: number) => { 432 return JSON.stringify(item) + index; 433 }) 434 }.columnsTemplate('1fr 1fr') 435 } 436 437 } 438} 439 440@Component 441export struct TapImage { 442 @StorageLink('tapIndex') tapIndex: number = -1; 443 @State tapColor: Color = Color.Black; 444 private index: number = 0; 445 private uri: Resource = { 446 id: 0, 447 type: 0, 448 moduleName: "", 449 bundleName: "" 450 }; 451 452 build() { 453 Column() { 454 Image(this.uri) 455 .objectFit(ImageFit.Cover) 456 .onClick(() => { 457 this.tapIndex = this.index; 458 }) 459 .border({ 460 width: 5, 461 style: BorderStyle.Dotted, 462 color: (this.tapIndex >= 0 && this.index === this.tapIndex) ? Color.Red : Color.Black 463 }) 464 } 465 } 466} 467``` 468 469 470 471## Restrictions 472 473When using AppStorage together with [PersistentStorage](arkts-persiststorage.md) and [Environment](arkts-environment.md), pay attention to the following: 474 475- After an attribute is created in AppStorage, a call to **PersistentStorage.persistProp()** uses the attribute value in AppStorage and overwrites any attribute with the same name in PersistentStorage. In light of this, the opposite order of calls is recommended. For an example of incorrect usage, see [Accessing Attribute in AppStorage Before PersistentStorage](arkts-persiststorage.md#accessing-attribute-in-appstorage-before-persistentstorage). 476 477- After an attribute is created in AppStorage, a call to **Environment.envProp()** with the same attribute name will fail. This is because environment variables will not be written into AppStorage. Therefore, you are advised not to use the preset environment variable names in AppStorage. 478 479- Changes to the variables decorated by state decorators will cause UI re-rendering. If the changes are for message communication, rather than for UI re-rendering, the emitter mode is recommended. For the example, see [Unrecommended: Using @StorageLink to Implement Event Notification](#unrecommended-using-storagelink-to-implement-event-notification). 480<!--no_check--> 481