• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![en-us_image_0000001552978157](figures/en-us_image_0000001552978157.png)
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![en-us_image_0000001501938718](figures/en-us_image_0000001501938718.png)
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