• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# AppStorage:应用全局的UI状态存储
2
3
4AppStorage是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。
5
6和AppStorage不同的是,LocalStorage是页面级的,通常应用于页面内的数据共享。而AppStorage是应用级的全局状态共享,还相当于整个应用的“中枢”,[持久化数据PersistentStorage](arkts-persiststorage.md)和[环境变量Environment](arkts-environment.md)都是通过AppStorage中转,才可以和UI交互。
7
8
9本文仅介绍AppStorage使用场景和相关的装饰器:\@StorageProp和\@StorageLink。
10
11
12## 概述
13
14AppStorage是在应用启动的时候会被创建的单例。它的目的是为了提供应用状态数据的中心存储,这些状态数据在应用级别都是可访问的。AppStorage将在应用运行过程保留其属性。属性通过唯一的键字符串值访问。
15
16AppStorage可以和UI组件同步,且可以在应用业务逻辑中被访问。
17
18AppStorage支持应用的[主线程](../application-models/thread-model-stage.md)内多个UIAbility实例间的状态共享。
19
20AppStorage中的属性可以被双向同步,数据可以是存在于本地或远程设备上,并具有不同的功能,比如数据持久化(详见[PersistentStorage](arkts-persiststorage.md))。这些数据是通过业务逻辑中实现,与UI解耦,如果希望这些数据在UI中使用,需要用到[@StorageProp](#storageprop)和[@StorageLink](#storagelink)。
21
22
23## \@StorageProp
24
25在上文中已经提到,如果要建立AppStorage和自定义组件的联系,需要使用\@StorageProp和\@StorageLink装饰器。使用\@StorageProp(key)/\@StorageLink(key)装饰组件内的变量,key标识了AppStorage的属性。
26
27当自定义组件初始化的时候,会使用AppStorage中对应key的属性值将\@StorageProp(key)/\@StorageLink(key)装饰的变量初始化。由于应用逻辑的差异,无法确认是否在组件初始化之前向AppStorage实例中存入了对应的属性,所以AppStorage不一定存在key对应的属性,因此\@StorageProp(key)/\@StorageLink(key)装饰的变量进行本地初始化是必要的。
28
29\@StorageProp(key)是和AppStorage中key对应的属性建立单向数据同步,允许本地改变,但是对于\@StorageProp,本地的修改永远不会同步回AppStorage中,相反,如果AppStorage给定key的属性发生改变,改变会被同步给\@StorageProp,并覆盖掉本地的修改。
30
31
32### 装饰器使用规则说明
33
34| \@StorageProp变量装饰器 | 说明                                                         |
35| ----------------------- | ------------------------------------------------------------ |
36| 装饰器参数              | key:常量字符串,必填(字符串需要有引号)。                  |
37| 允许装饰的变量类型      | Object、&nbsp;class、string、number、boolean、enum类型,以及这些类型的数组。嵌套类型的场景请参考[观察变化和行为表现](#观察变化和行为表现)。<br/>类型必须被指定,建议和AppStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。不支持any,不允许使用undefined和null。 |
38| 同步类型                | 单向同步:从AppStorage的对应属性到组件的状态变量。<br/>组件本地的修改是允许的,但是AppStorage中给定的属性一旦发生变化,将覆盖本地的修改。 |
39| 被装饰变量的初始值      | 必须指定,如果AppStorage实例中不存在属性,则作为初始化默认值,并存入AppStorage中。 |
40
41
42### 变量的传递/访问规则说明
43
44| 传递/访问      | 说明                                       |
45| ---------- | ---------------------------------------- |
46| 从父节点初始化和更新 | 禁止,\@StorageProp不支持从父节点初始化,只能AppStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化 |
47| 初始化子节点     | 支持,可用于初始化\@State、\@Link、\@Prop、\@Provide。 |
48| 是否支持组件外访问  | 否。                                       |
49
50
51  **图1** \@StorageProp初始化规则图示  
52
53
54![zh-cn_image_0000001552978157](figures/zh-cn_image_0000001552978157.png)
55
56
57### 观察变化和行为表现
58
59**观察变化**
60
61
62- 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
63
64- 当装饰的数据类型为class或者Object时,可以观察到赋值和属性赋值的变化,即Object.keys(observedObject)返回的所有属性。
65
66- 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
67
68
69**框架行为**
70
71
72- 当\@StorageProp(key)装饰的数值改变被观察到时,修改不会被同步回AppStorage对应属性键值key的属性中。
73
74- 当前\@StorageProp(key)单向绑定的数据会被修改,即仅限于当前组件的私有成员变量改变,其他的绑定该key的数据不会同步改变。
75
76- 当\@StorageProp(key)装饰的数据本身是状态变量,它的改变虽然不会同步回AppStorage中,但是会引起所属的自定义组件的重新渲染。
77
78- 当AppStorage中key对应的属性发生改变时,会同步给所有\@StorageProp(key)装饰的数据,\@StorageProp(key)本地的修改将被覆盖。
79
80
81## \@StorageLink
82
83\@StorageLink(key)是和AppStorage中key对应的属性建立双向数据同步:
84
851. 本地修改发生,该修改会被写回AppStorage中;
86
872. AppStorage中的修改发生后,该修改会被同步到所有绑定AppStorage对应key的属性上,包括单向(\@StorageProp和通过Prop创建的单向绑定变量)、双向(\@StorageLink和通过Link创建的双向绑定变量)变量和其他实例(比如PersistentStorage)。
88
89
90### 装饰器使用规则说明
91
92| \@StorageLink变量装饰器 | 说明                                       |
93| ------------------ | ---------------------------------------- |
94| 装饰器参数              | key:常量字符串,必填(字符串需要有引号)。                  |
95| 允许装饰的变量类型          | Object、class、string、number、boolean、enum类型,以及这些类型的数组。嵌套类型的场景请参考[观察变化和行为表现](#观察变化和行为表现)。<br/>类型必须被指定,建议和AppStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。不支持any,不允许使用undefined和null。 |
96| 同步类型               | 双向同步:从AppStorage的对应属性到自定义组件,从自定义组件到AppStorage对应属性。 |
97| 被装饰变量的初始值          | 必须指定,如果AppStorage实例中不存在属性,则作为初始化默认值,并存入AppStorage中。 |
98
99
100### 变量的传递/访问规则说明
101
102| 传递/访问      | 说明                                       |
103| ---------- | ---------------------------------------- |
104| 从父节点初始化和更新 | 禁止。                                      |
105| 初始化子节点     | 支持,可用于初始化常规变量、\@State、\@Link、\@Prop、\@Provide。 |
106| 是否支持组件外访问  | 否。                                       |
107
108
109  **图2** \@StorageLink初始化规则图示  
110
111
112![zh-cn_image_0000001501938718](figures/zh-cn_image_0000001501938718.png)
113
114
115### 观察变化和行为表现
116
117**观察变化**
118
119
120- 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
121
122- 当装饰的数据类型为class或者Object时,可以观察到赋值和属性赋值的变化,即Object.keys(observedObject)返回的所有属性。
123
124- 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
125
126
127**框架行为**
128
129
1301. 当\@StorageLink(key)装饰的数值改变被观察到时,修改将被同步回AppStorage对应属性键值key的属性中。
131
1322. AppStorage中属性键值key对应的数据一旦改变,属性键值key绑定的所有的数据(包括双向\@StorageLink和单向\@StorageProp)都将同步修改;
133
1343. 当\@StorageLink(key)装饰的数据本身是状态变量,它的改变不仅仅会同步回AppStorage中,还会引起所属的自定义组件的重新渲染。
135
136
137## 使用场景
138
139
140### 从应用逻辑使用AppStorage和LocalStorage
141
142AppStorage是单例,它的所有API都是静态的,使用方法类似于中LocalStorage对应的非静态方法。
143
144
145```ts
146AppStorage.setOrCreate('PropA', 47);
147
148let storage: LocalStorage = new LocalStorage();
149storage.setOrCreate('PropA',17);
150let propA: number | undefined = AppStorage.get('PropA') // propA in AppStorage == 47, propA in LocalStorage == 17
151let link1: SubscribedAbstractProperty<number> = AppStorage.link('PropA'); // link1.get() == 47
152let link2: SubscribedAbstractProperty<number> = AppStorage.link('PropA'); // link2.get() == 47
153let prop: SubscribedAbstractProperty<number> = AppStorage.prop('PropA'); // prop.get() == 47
154
155link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
156prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48
157link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49
158
159storage.get<number>('PropA') // == 17
160storage.set('PropA', 101);
161storage.get<number>('PropA') // == 101
162
163AppStorage.get<number>('PropA') // == 49
164link1.get() // == 49
165link2.get() // == 49
166prop.get() // == 49
167```
168
169
170### 从UI内部使用AppStorage和LocalStorage
171
172\@StorageLink变量装饰器与AppStorage配合使用,正如\@LocalStorageLink与LocalStorage配合使用一样。此装饰器使用AppStorage中的属性创建双向数据同步。
173
174
175```ts
176AppStorage.setOrCreate('PropA', 47);
177let storage = new LocalStorage();
178storage.setOrCreate('PropA', 48);
179
180@Entry(storage)
181@Component
182struct CompA {
183  @StorageLink('PropA') storageLink: number = 1;
184  @LocalStorageLink('PropA') localStorageLink: number = 1;
185
186  build() {
187    Column({ space: 20 }) {
188      Text(`From AppStorage ${this.storageLink}`)
189        .onClick(() => {
190          this.storageLink += 1
191        })
192
193      Text(`From LocalStorage ${this.localStorageLink}`)
194        .onClick(() => {
195          this.localStorageLink += 1
196        })
197    }
198  }
199}
200```
201
202### 不建议借助@StorageLink的双向同步机制实现事件通知
203
204不建议开发者使用@StorageLink和AppStorage的双向同步的机制来实现事件通知,因为AppStorage中的变量可能绑定在多个不同页面的组件中,但事件通知则不一定需要通知到所有的这些组件。并且,当这些@StorageLink装饰的变量在UI中使用时,会触发UI刷新,带来不必要的性能影响。
205
206示例代码中,TapImage中的点击事件,会触发AppStorage中tapIndex对应属性的改变。因为@StorageLink是双向同步,修改会同步回AppStorage中,所以,所有绑定AppStorage的tapIndex自定义组件里都能感知到tapIndex的变化。使用@Watch监听到tapIndex的变化后,修改状态变量tapColor从而触发UI刷新(此处tapIndex并未直接绑定在UI上,因此tapIndex的变化不会直接触发UI刷新)。
207
208使用该机制来实现事件通知需要确保AppStorage中的变量尽量不要直接绑定在UI上,且需要控制@Watch函数的复杂度(如果@Watch函数执行时间长,会影响UI刷新效率)。
209
210
211```ts
212// xxx.ets
213class ViewData {
214  title: string;
215  uri: Resource;
216  color: Color = Color.Black;
217
218  constructor(title: string, uri: Resource) {
219    this.title = title;
220    this.uri = uri
221  }
222}
223
224@Entry
225@Component
226struct Gallery2 {
227  dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
228  scroller: Scroller = new Scroller()
229
230  build() {
231    Column() {
232      Grid(this.scroller) {
233        ForEach(this.dataList, (item: ViewData, index?: number) => {
234          GridItem() {
235            TapImage({
236              uri: item.uri,
237              index: index
238            })
239          }.aspectRatio(1)
240
241        }, (item: ViewData, index?: number) => {
242          return JSON.stringify(item) + index;
243        })
244      }.columnsTemplate('1fr 1fr')
245    }
246
247  }
248}
249
250@Component
251export struct TapImage {
252  @StorageLink('tapIndex') @Watch('onTapIndexChange') tapIndex: number = -1;
253  @State tapColor: Color = Color.Black;
254  private index: number = 0;
255  private uri: Resource = {
256    id: 0,
257    type: 0,
258    moduleName: "",
259    bundleName: ""
260  };
261
262  // 判断是否被选中
263  onTapIndexChange() {
264    if (this.tapIndex >= 0 && this.index === this.tapIndex) {
265      console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, red`)
266      this.tapColor = Color.Red;
267    } else {
268      console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, black`)
269      this.tapColor = Color.Black;
270    }
271  }
272
273  build() {
274    Column() {
275      Image(this.uri)
276        .objectFit(ImageFit.Cover)
277        .onClick(() => {
278          this.tapIndex = this.index;
279        })
280        .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
281    }
282
283  }
284}
285```
286
287相比借助@StorageLink的双向同步机制实现事件通知,开发者可以使用emit订阅某个事件并接收事件回调的方式来减少开销,增强代码的可读性。
288
289> **说明:**
290>
291> emit接口不支持在Previewer预览器中使用。
292
293
294```ts
295// xxx.ets
296import emitter from '@ohos.events.emitter';
297
298let NextID: number = 0;
299
300class ViewData {
301  title: string;
302  uri: Resource;
303  color: Color = Color.Black;
304  id: number;
305
306  constructor(title: string, uri: Resource) {
307    this.title = title;
308    this.uri = uri
309    this.id = NextID++;
310  }
311}
312
313@Entry
314@Component
315struct Gallery2 {
316  dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
317  scroller: Scroller = new Scroller()
318  private preIndex: number = -1
319
320  build() {
321    Column() {
322      Grid(this.scroller) {
323        ForEach(this.dataList, (item: ViewData) => {
324          GridItem() {
325            TapImage({
326              uri: item.uri,
327              index: item.id
328            })
329          }.aspectRatio(1)
330          .onClick(() => {
331            if (this.preIndex === item.id) {
332              return
333            }
334            let innerEvent: emitter.InnerEvent = { eventId: item.id }
335            // 选中态:黑变红
336            let eventData: emitter.EventData = {
337              data: {
338                "colorTag": 1
339              }
340            }
341            emitter.emit(innerEvent, eventData)
342
343            if (this.preIndex != -1) {
344              console.info(`preIndex: ${this.preIndex}, index: ${item.id}, black`)
345              let innerEvent: emitter.InnerEvent = { eventId: this.preIndex }
346              // 取消选中态:红变黑
347              let eventData: emitter.EventData = {
348                data: {
349                  "colorTag": 0
350                }
351              }
352              emitter.emit(innerEvent, eventData)
353            }
354            this.preIndex = item.id
355          })
356        }, (item: ViewData) => JSON.stringify(item))
357      }.columnsTemplate('1fr 1fr')
358    }
359
360  }
361}
362
363@Component
364export struct TapImage {
365  @State tapColor: Color = Color.Black;
366  private index: number = 0;
367  private uri: Resource = {
368    id: 0,
369    type: 0,
370    moduleName: "",
371    bundleName: ""
372  };
373
374  onTapIndexChange(colorTag: emitter.EventData) {
375    if (colorTag.data != null) {
376      this.tapColor = colorTag.data.colorTag ? Color.Red : Color.Black
377    }
378  }
379
380  aboutToAppear() {
381    //定义事件ID
382    let innerEvent: emitter.InnerEvent = { eventId: this.index }
383    emitter.on(innerEvent, data => {
384    this.onTapIndexChange(data)
385    })
386  }
387
388  build() {
389    Column() {
390      Image(this.uri)
391        .objectFit(ImageFit.Cover)
392        .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
393    }
394  }
395}
396```
397
398以上通知事件逻辑简单,也可以简化成三元表达式。
399
400```ts
401// xxx.ets
402class ViewData {
403  title: string;
404  uri: Resource;
405  color: Color = Color.Black;
406
407  constructor(title: string, uri: Resource) {
408    this.title = title;
409    this.uri = uri
410  }
411}
412
413@Entry
414@Component
415struct Gallery2 {
416  dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
417  scroller: Scroller = new Scroller()
418
419  build() {
420    Column() {
421      Grid(this.scroller) {
422        ForEach(this.dataList, (item: ViewData, index?: number) => {
423          GridItem() {
424            TapImage({
425              uri: item.uri,
426              index: index
427            })
428          }.aspectRatio(1)
429
430        }, (item: ViewData, index?: number) => {
431          return JSON.stringify(item) + index;
432        })
433      }.columnsTemplate('1fr 1fr')
434    }
435
436  }
437}
438
439@Component
440export struct TapImage {
441  @StorageLink('tapIndex') tapIndex: number = -1;
442  @State tapColor: Color = Color.Black;
443  private index: number = 0;
444  private uri: Resource = {
445    id: 0,
446    type: 0,
447    moduleName: "",
448    bundleName: ""
449  };
450
451  build() {
452    Column() {
453      Image(this.uri)
454        .objectFit(ImageFit.Cover)
455        .onClick(() => {
456          this.tapIndex = this.index;
457        })
458        .border({
459          width: 5,
460          style: BorderStyle.Dotted,
461          color: (this.tapIndex >= 0 && this.index === this.tapIndex) ? Color.Red : Color.Black
462        })
463    }
464  }
465}
466```
467
468
469
470## 限制条件
471
472AppStorage与[PersistentStorage](arkts-persiststorage.md)以及[Environment](arkts-environment.md)配合使用时,需要注意以下几点:
473
474- 在AppStorage中创建属性后,调用PersistentStorage.persistProp()接口时,会使用在AppStorage中已经存在的值,并覆盖PersistentStorage中的同名属性,所以建议要使用相反的调用顺序,反例可见[在PersistentStorage之前访问AppStorage中的属性](arkts-persiststorage.md#在persistentstorage之前访问appstorage中的属性);
475
476- 如果在AppStorage中已经创建属性后,再调用Environment.envProp()创建同名的属性,会调用失败。因为AppStorage已经有同名属性,Environment环境变量不会再写入AppStorage中,所以建议AppStorage中属性不要使用Environment预置环境变量名。
477
478- 状态装饰器装饰的变量,改变会引起UI的渲染更新,如果改变的变量不是用于UI更新,只是用于消息传递,推荐使用 emitter方式。例子可见[不建议借助@StorageLink的双向同步机制实现事件通知](#不建议借助storagelink的双向同步机制实现事件通知)。
479<!--no_check-->
480