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