1# AppStorage:应用全局的UI状态存储 2<!--Kit: ArkUI--> 3<!--Subsystem: ArkUI--> 4<!--Owner: @zzq212050299--> 5<!--Designer: @s10021109--> 6<!--Tester: @TerryTsao--> 7<!--Adviser: @zhang_yixin13--> 8 9在阅读本文档前,建议提前阅读:[状态管理概述](./arkts-state-management-overview.md),从而对状态管理框架中AppStorage的定位有一个宏观了解。 10 11AppStorage是与应用进程绑定的全局UI状态存储中心,由UI框架在应用启动时创建,将UI状态数据存储于运行内存,实现应用级全局状态共享。 12 13作为应用的“中枢”,AppStorage是[持久化数据PersistentStorage](arkts-persiststorage.md)和[环境变量Environment](arkts-environment.md)与UI交互的中转桥梁。其核心价值在于为开发者提供跨ability的大范围UI状态数据共享能力。 14 15AppStorage提供了API接口,允许开发者在自定义组件外手动触发AppStorage对应属性的增、删、改、查操作。建议配合[AppStorage API文档](../../reference/apis-arkui/arkui-ts/ts-state-management.md#appstorage)阅读。最佳实践请参考[状态管理最佳实践](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-status-management)。 16 17## 概述 18 19AppStorage是在应用启动时创建的单例,用于提供应用状态数据的中心存储。这些状态数据在应用级别可访问。AppStorage在应用运行过程中保留其属性。 20 21AppStorage中保存的属性通过唯一的字符串类型属性名(key)访问,该属性可以和UI组件同步,且可以在应用业务逻辑中被访问。 22 23AppStorage支持应用的[主线程](../../application-models/thread-model-stage.md)内多个[UIAbility](../../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md)实例间的UI状态数据共享。 24 25AppStorage中的属性通过唯一的字符串类型key值访问,支持与UI组件同步,并可在应用业务逻辑中被访问。其支持应用的[主线程](../../application-models/thread-model-stage.md)内多个[UIAbility](../../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md)实例间的UI状态数据共享。 26 27AppStorage中的属性可以被双向同步,并具有不同的功能,比如数据持久化(详见[PersistentStorage](arkts-persiststorage.md))。这些UI状态是通过业务逻辑实现,与UI解耦,如果希望这些UI状态在UI中使用,需要用到[@StorageProp](#storageprop)和[@StorageLink](#storagelink)。 28 29## \@StorageProp 30 31\@StorageProp与AppStorage中对应的属性建立单向数据同步。 32 33> **说明:** 34> 35> 从API version 11开始,该装饰器支持在原子化服务中使用。 36 37### 装饰器使用规则说明 38 39| \@StorageProp变量装饰器 | 说明 | 40| ----------------------- | ------------------------------------------------------------ | 41| 装饰器参数 | 常量字符串,必填(字符串需要有引号)。<br/>**说明:**<br/>使用null和undefined作为key时,会隐式转换为对应的字符串,不建议该用法。 | 42| 允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。<br/>>API Version 12及以上,支持Map、Set、Date、undefined和null类型,嵌套类型的场景请参考[观察变化和行为表现](#观察变化和行为表现)。<br/>同时,API Version 12及以上还支持上述支持类型的联合类型,比如string \| number, string \| undefined 或者 ClassA \| null,示例见[AppStorage支持联合类型](#appstorage支持联合类型)。 <br/>**说明:**<br/>变量类型必须被指定,建议和AppStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。| 43| 不允许装饰的变量类型 | 不支持装饰Function类型。 | 44| 同步类型 | 单向同步:从AppStorage的对应属性到组件的状态变量。<br/>组件本地的修改是允许的,但是AppStorage中给定的属性一旦发生变化,将覆盖本地的修改。 | 45| 被装饰变量的初始值 | 必须本地初始化,如果AppStorage实例中不存在属性,则用该初始值初始化该属性,并存入AppStorage中。 | 46 47### 变量的传递/访问规则说明 48 49| 传递/访问 | 说明 | 50| ---------- | ---------------------------------------- | 51| 从父节点初始化和更新 | 禁止从父节点初始化和更新@StorageProp。仅支持使用AppStorage中对应key的属性进行初始化,如果不存在对应key,则使用本地默认值进行初始化。 | 52| 初始化子节点 | 支持,可用于初始化[\@State](./arkts-state.md)、[\@Link](./arkts-link.md)、[\@Prop](./arkts-prop.md)、[\@Provide](./arkts-provide-and-consume.md)。 | 53| 是否支持组件外访问 | 否。 | 54 55 **图1** \@StorageProp初始化规则图示 56 57 58 59### 观察变化和行为表现 60 61**观察变化** 62 63- 当装饰的类型为boolean、string、number时,可以观察到数值的变化。 64 65- 当装饰的数据类型为class或者Object时,可以观察到对象整体赋值和属性变化(详见[从ui内部使用appstorage](#从ui内部使用appstorage))。 66 67- 当装饰的对象是数组时,可以观察到数组添加、删除、更新数组单元的变化。 68 69- 当装饰的对象是Date时,可以观察到Date整体的赋值,以及通过调用Date的接口`setFullYear`、`setMonth`、`setDate`、`setHours`、`setMinutes`、`setSeconds`、`setMilliseconds`、`setTime`、`setUTCFullYear`、`setUTCMonth`、`setUTCDate`、`setUTCHours`、`setUTCMinutes`、`setUTCSeconds`、`setUTCMilliseconds`更新Date的属性。详见[装饰Date类型变量](#装饰date类型变量)。 70 71- 当装饰的变量是Map时,可以观察到Map整体的赋值,以及通过调用Map的接口`set`、`clear`、`delete`更新Map的值。详见[装饰Map类型变量](#装饰map类型变量)。 72 73- 当装饰的变量是Set时,可以观察到Set整体的赋值,以及通过调用Set的接口`add`、`clear`、`delete`更新Set的值。详见[装饰Set类型变量](#装饰set类型变量)。 74 75**框架行为** 76 771. \@StorageProp(key)装饰的数值发生变化,不会同步写回AppStorage对应的属性;变化会触发自定义组件重新渲染,并且该变动仅作用于当前组件的私有成员变量,其他绑定该key的数据不会同步改变。 78 792. 当AppStorage中对应key的属性发生改变时,所有\@StorageProp(key)装饰的变量都会同步更新,本地的修改将被覆盖。 80 81## \@StorageLink 82 83\@StorageLink与AppStorage中对应的属性建立双向数据同步。 84 85> **说明:** 86> 87> 从API version 11开始,该装饰器支持在原子化服务中使用。 88 89### 装饰器使用规则说明 90 91| \@StorageLink变量装饰器 | 说明 | 92| ----------------------- | ------------------------------------------------------------ | 93| 装饰器参数 | key:常量字符串,必填(字符串需要有引号)。<br/>**注意:**<br/>使用null和undefined作为key时,会隐式转换为对应的字符串,不建议该用法。 | 94| 允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。<br/>API Version 12及以上支持Map、Set、Date、undefined和null类型。嵌套类型的场景请参考[观察变化和行为表现](#观察变化和行为表现-1)。<br/>>API Version 12及以上还支持上述支持类型的联合类型,比如string \| number, string \| undefined 或者 ClassA \| null,示例见[AppStorage支持联合类型](#appstorage支持联合类型)。 <br/>**注意:**<br/>变量类型必须被指定,建议和AppStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。 | 95| 不允许装饰的变量类型 | 不支持装饰Function类型。 | 96| 同步类型 | 双向同步:从AppStorage的对应属性到自定义组件,从自定义组件到AppStorage对应属性。 | 97| 被装饰变量的初始值 | 必须本地初始化,如果AppStorage实例中不存在属性,则用该初始值初始化该属性,并存入AppStorage中。 | 98 99 100### 变量的传递/访问规则说明 101 102| 传递/访问 | 说明 | 103| ---------- | ---------------------------------------- | 104| 从父节点初始化和更新 | 禁止。 | 105| 初始化子节点 | 支持,可用于初始化常规变量、\@State、\@Link、\@Prop、\@Provide。 | 106| 是否支持组件外访问 | 否。 | 107 108 **图2** \@StorageLink初始化规则图示 109 110 111 112### 观察变化和行为表现 113 114**观察变化** 115 116- 装饰的数据类型为boolean、string、number时,可以观察到数值变化。 117 118- 装饰的数据类型为class或Object时,可以观察到对象整体赋值和属性变化。(详见[从ui内部使用appstorage](#从ui内部使用appstorage))。 119 120- 当装饰的对象是数组时,可以观察到数组添加、删除、更新数组单元的变化。 121 122- 当装饰的对象是Date时,可以观察到Date整体的赋值,以及通过调用Date的接口`setFullYear`, `setMonth`, `setDate`, `setHours`, `setMinutes`, `setSeconds`, `setMilliseconds`, `setTime`, `setUTCFullYear`, `setUTCMonth`, `setUTCDate`, `setUTCHours`, `setUTCMinutes`, `setUTCSeconds`, `setUTCMilliseconds` 更新Date的属性。详见[装饰Date类型变量](#装饰date类型变量)。 123 124- 当装饰的变量是Map时,可以观察到Map整体的赋值,以及通过调用Map的接口`set`、`clear`、`delete`更新Map的值。详见[装饰Map类型变量](#装饰map类型变量)。 125 126- 当装饰的变量是Set时,可以观察到Set的整体赋值,以及通过调用Set的接口`add`、`clear`、`delete`更新Set的值。详见[装饰Set类型变量](#装饰set类型变量)。 127 128**框架行为** 129 1301. 当\@StorageLink(key)装饰的数值发生变化时,修改将被同步回AppStorage对应key的属性中。 131 1322. AppStorage中key对应的数据一旦改变,其绑定的所有的数据(包括双向\@StorageLink和单向\@StorageProp)都将被同步修改。 133 1343. `@StorageLink(key)`装饰的数据是状态变量,其变化不仅会同步到AppStorage,还会触发自定义组件的重新渲染。 135 136 137## 限制条件 138 1391. \@StorageProp/\@StorageLink的参数必须为string类型,否则编译期会报错。 140 141 ```ts 142 AppStorage.setOrCreate('propA', 47); 143 144 // 错误写法,编译报错 145 @StorageProp() storageProp: number = 1; 146 @StorageLink() storageLink: number = 2; 147 148 // 正确写法 149 @StorageProp('propA') storageProp: number = 1; 150 @StorageLink('propA') storageLink: number = 2; 151 ``` 152 1532. \@StorageProp与\@StorageLink不支持装饰Function类型的变量,框架会抛出运行时错误。 154 1553. AppStorage与[PersistentStorage](arkts-persiststorage.md)以及[Environment](arkts-environment.md)配合使用时,需要注意以下几点: 156 157 1. 在AppStorage中创建属性后,调用PersistentStorage.[persistProp](../../reference/apis-arkui/arkui-ts/ts-state-management.md#persistpropdeprecated)接口时,会使用AppStorage中已存在的值,并覆盖PersistentStorage中的同名属性。因此,建议使用相反的调用顺序。反例可见[在PersistentStorage之前访问AppStorage中的属性](arkts-persiststorage.md#在persistentstorage之前访问appstorage中的属性)。 158 159 2. 如果在AppStorage中已创建属性,再调用Environment.[envProp](../../reference/apis-arkui/arkui-ts/ts-state-management.md#envprop10)创建同名属性,会调用失败。因为AppStorage已有同名属性,Environment环境变量不会再写入AppStorage中,所以建议不要在AppStorage中使用Environment预置环境变量名。 160 1614. 状态装饰器装饰的变量,改变会引起UI的渲染更新。如果改变的变量仅用于消息传递,不用于UI更新,推荐使用emitter方式。具体示例可见[不建议借助@StorageLink的双向同步机制实现事件通知](#不建议借助storagelink的双向同步机制实现事件通知)。 162 1635. AppStorage同一进程内共享,UIAbility和<!--Del-->[<!--DelEnd-->UIExtensionAbility<!--Del-->](../../application-models/uiextensionability.md)<!--DelEnd-->是两个进程,所以在<!--Del-->[<!--DelEnd-->UIExtensionAbility<!--Del-->](../../application-models/uiextensionability.md)<!--DelEnd-->中不共享主进程的AppStorage。 164 165## 使用场景 166 167### 从应用逻辑使用AppStorage和LocalStorage 168 169AppStorage是单例,其所有API均为静态方法,使用方法类似于LocalStorage中对应的非静态方法。 170 171```ts 172AppStorage.setOrCreate('propA', 47); 173 174let storage: LocalStorage = new LocalStorage(); 175storage.setOrCreate('propA',17); 176let propA: number | undefined = AppStorage.get('propA'); // propA in AppStorage == 47, propA in LocalStorage == 17 177let link1: SubscribedAbstractProperty<number> = AppStorage.link('propA'); // link1.get() == 47 178let link2: SubscribedAbstractProperty<number> = AppStorage.link('propA'); // link2.get() == 47 179let prop: SubscribedAbstractProperty<number> = AppStorage.prop('propA'); // prop.get() == 47 180 181link1.set(48); // 双向同步: link1.get() == link2.get() == prop.get() == 48 182prop.set(1); // 单向同步: prop.get() == 1; 但 link1.get() == link2.get() == 48 183link1.set(49); // 双向同步: link1.get() == link2.get() == prop.get() == 49 184 185storage.get<number>('propA') // == 17 186storage.set('propA', 101); 187storage.get<number>('propA') // == 101 188 189AppStorage.get<number>('propA') // == 49 190link1.get() // == 49 191link2.get() // == 49 192prop.get() // == 49 193``` 194 195 196### 从UI内部使用AppStorage 197 198@StorageLink与AppStorage配合使用,通过AppStorage中的属性创建双向数据同步。 199@StorageProp与AppStorage配合使用,通过AppStorage中的属性创建单向数据同步。 200 201```ts 202class Data { 203 code: number; 204 205 constructor(code: number) { 206 this.code = code; 207 } 208} 209 210AppStorage.setOrCreate('propA', 47); 211AppStorage.setOrCreate('propB', new Data(50)); 212let storage = new LocalStorage(); 213storage.setOrCreate('linkA', 48); 214storage.setOrCreate('linkB', new Data(100)); 215 216@Entry(storage) 217@Component 218struct Index { 219 @StorageLink('propA') storageLink: number = 1; 220 @StorageProp('propA') storageProp: number = 1; 221 @StorageLink('propB') storageLinkObject: Data = new Data(1); 222 @StorageProp('propB') storagePropObject: Data = new Data(1); 223 224 build() { 225 Column({ space: 20 }) { 226 // @StorageLink与AppStorage建立双向联系,更改数据会同步回AppStorage中key为'propA'的值 227 Text(`storageLink ${this.storageLink}`) 228 .onClick(() => { 229 this.storageLink += 1; 230 }) 231 232 // @StorageProp与AppStorage建立单向联系,更改数据不会同步AppStoragekey为'propA'的值 233 // 但能被AppStorage的set/setorCreate更新值 234 Text(`storageProp ${this.storageProp}`) 235 .onClick(() => { 236 this.storageProp += 1; 237 }) 238 239 // AppStorage的API虽然能获取值,但是不具有刷新UI的能力,日志能看到数值更改 240 // 依赖@StorageLink/@StorageProp才能建立起与自定义组件的联系,刷新UI 241 Text(`change by AppStorage: ${AppStorage.get<number>('propA')}`) 242 .onClick(() => { 243 console.info(`Appstorage.get: ${AppStorage.get<number>('propA')}`); 244 AppStorage.set<number>('propA', 100); 245 }) 246 247 Text(`storageLinkObject ${this.storageLinkObject.code}`) 248 .onClick(() => { 249 this.storageLinkObject.code += 1; 250 }) 251 252 Text(`storagePropObject ${this.storagePropObject.code}`) 253 .onClick(() => { 254 this.storagePropObject.code += 1; 255 }) 256 } 257 } 258} 259``` 260 261### AppStorage支持联合类型 262 263在下面的示例中,变量linkA的类型为number | null,变量linkB的类型为number | undefined。Text组件初始化分别显示为null和undefined,点击切换为数字,再次点击切换回null和undefined。 264 265```ts 266@Component 267struct StorageLinkComponent { 268 @StorageLink('linkA') linkA: number | null = null; 269 @StorageLink('linkB') linkB: number | undefined = undefined; 270 271 build() { 272 Column() { 273 Text('@StorageLink接口初始化,@StorageLink取值') 274 Text(`${this.LinkA}`).fontSize(20).onClick(() => { 275 this.linkA ? this.linkA = null : this.linkA = 1; 276 }) 277 Text(`${this.LinkB}`).fontSize(20).onClick(() => { 278 this.linkB ? this.linkB = undefined : this.linkB = 1; 279 }) 280 } 281 .borderWidth(3).borderColor(Color.Red) 282 } 283} 284 285@Component 286struct StoragePropComponent { 287 @StorageProp('propA') propA: number | null = null; 288 @StorageProp('propB') propB: number | undefined = undefined; 289 290 build() { 291 Column() { 292 Text('@StorageProp接口初始化,@StorageProp取值') 293 Text(`${this.propA}`).fontSize(20).onClick(() => { 294 this.propA ? this.propA = null : this.propA = 1; 295 }) 296 Text(`${this.propB}`).fontSize(20).onClick(() => { 297 this.propB ? this.propB = undefined : this.propB = 1; 298 }) 299 } 300 .borderWidth(3).borderColor(Color.Blue) 301 } 302} 303 304@Entry 305@Component 306struct Index { 307 build() { 308 Row() { 309 Column() { 310 StorageLinkComponent() 311 StoragePropComponent() 312 } 313 .width('100%') 314 } 315 .height('100%') 316 } 317} 318``` 319 320### 装饰Date类型变量 321 322> **说明:** 323> 324> 从API version 12开始,AppStorage支持Date类型。 325 326以下示例中,@StorageLink装饰的selectedDate类型为Date。点击Button改变selectedDate的值,视图会随之刷新。 327 328```ts 329@Entry 330@Component 331struct DateSample { 332 @StorageLink('date') selectedDate: Date = new Date('2021-08-08'); 333 334 build() { 335 Column() { 336 Button('set selectedDate to 2023-07-08') 337 .margin(10) 338 .onClick(() => { 339 AppStorage.setOrCreate('date', new Date('2023-07-08')); 340 }) 341 Button('increase the year by 1') 342 .margin(10) 343 .onClick(() => { 344 this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1); 345 }) 346 Button('increase the month by 1') 347 .margin(10) 348 .onClick(() => { 349 this.selectedDate.setMonth(this.selectedDate.getMonth() + 1); 350 }) 351 Button('increase the day by 1') 352 .margin(10) 353 .onClick(() => { 354 this.selectedDate.setDate(this.selectedDate.getDate() + 1); 355 }) 356 DatePicker({ 357 start: new Date('1970-1-1'), 358 end: new Date('2100-1-1'), 359 selected: $$this.selectedDate 360 }) 361 }.width('100%') 362 } 363} 364``` 365 366### 装饰Map类型变量 367 368> **说明:** 369> 370> 从API version 12开始,AppStorage支持Map类型。 371 372在下面的示例中,@StorageLink装饰的message类型为Map\<number, string\>,点击Button改变message的值,视图会随之刷新。 373 374```ts 375@Entry 376@Component 377struct MapSample { 378 @StorageLink('map') message: Map<number, string> = new Map([[0, 'a'], [1, 'b'], [3, 'c']]); 379 380 build() { 381 Row() { 382 Column() { 383 ForEach(Array.from(this.message.entries()), (item: [number, string]) => { 384 Text(`${item[0]}`).fontSize(30) 385 Text(`${item[1]}`).fontSize(30) 386 Divider() 387 }) 388 Button('init map').onClick(() => { 389 this.message = new Map([[0, 'a'], [1, 'b'], [3, 'c']]); 390 }) 391 Button('set new one').onClick(() => { 392 this.message.set(4, 'd'); 393 }) 394 Button('clear').onClick(() => { 395 this.message.clear(); 396 }) 397 Button('replace the existing one').onClick(() => { 398 this.message.set(0, 'aa'); 399 }) 400 Button('delete the existing one').onClick(() => { 401 AppStorage.get<Map<number, string>>('map')?.delete(0); 402 }) 403 } 404 .width('100%') 405 } 406 .height('100%') 407 } 408} 409``` 410 411### 装饰Set类型变量 412 413> **说明:** 414> 415> 从API version 12开始,AppStorage支持Set类型。 416 417在下面的示例中,@StorageLink装饰的memberSet类型为Set\<number\>,点击Button改变memberSet的值,视图会随之刷新。 418 419```ts 420@Entry 421@Component 422struct SetSample { 423 @StorageLink('set') memberSet: Set<number> = new Set([0, 1, 2, 3, 4]); 424 425 build() { 426 Row() { 427 Column() { 428 ForEach(Array.from(this.memberSet.entries()), (item: [number, number]) => { 429 Text(`${item[0]}`) 430 .fontSize(30) 431 Divider() 432 }) 433 Button('init set') 434 .onClick(() => { 435 this.memberSet = new Set([0, 1, 2, 3, 4]); 436 }) 437 Button('set new one') 438 .onClick(() => { 439 AppStorage.get<Set<number>>('set')?.add(5); 440 }) 441 Button('clear') 442 .onClick(() => { 443 this.memberSet.clear(); 444 }) 445 Button('delete the first one') 446 .onClick(() => { 447 this.memberSet.delete(0); 448 }) 449 } 450 .width('100%') 451 } 452 .height('100%') 453 } 454} 455``` 456 457## AppStorage使用建议 458 459### 不建议借助@StorageLink的双向同步机制实现事件通知 460 461不建议使用@StorageLink和AppStorage的双向同步机制来实现事件通知。AppStorage中的变量可能绑定在多个页面的组件中,但事件通知不一定需要通知到所有这些组件。此外,当这些@StorageLink装饰的变量在UI中使用时,会触发UI刷新,造成不必要的性能影响。 462 463示例代码中,`TapImage`中的点击事件会触发`AppStorage`中`tapIndex`对应属性的改变。由于`@StorageLink`是双向同步的,修改会同步回`AppStorage`中,因此所有绑定`AppStorage`的`tapIndex`自定义组件都能感知到`tapIndex`的变化。使用`@Watch`监听到`tapIndex`的变化后,修改状态变量`tapColor`,从而触发UI刷新(此处`tapIndex`未直接绑定在UI上,因此`tapIndex`的变化不会直接触发UI刷新)。 464 465使用该机制实现事件通知时,应确保AppStorage中的变量不直接被绑定到UI上,同时控制[@Watch](./arkts-watch.md)函数的复杂度。如果@Watch函数执行时间过长,会影响UI刷新效率。 466 467```ts 468// xxx.ets 469class ViewData { 470 title: string; 471 uri: Resource; 472 color: Color = Color.Black; 473 474 constructor(title: string, uri: Resource) { 475 this.title = title; 476 this.uri = uri; 477 } 478} 479 480@Entry 481@Component 482struct Gallery { 483 // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 484 dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]; 485 scroller: Scroller = new Scroller(); 486 487 build() { 488 Column() { 489 Grid(this.scroller) { 490 ForEach(this.dataList, (item: ViewData, index?: number) => { 491 GridItem() { 492 TapImage({ 493 uri: item.uri, 494 index: index 495 }) 496 }.aspectRatio(1) 497 498 }, (item: ViewData, index?: number) => { 499 return JSON.stringify(item) + index; 500 }) 501 }.columnsTemplate('1fr 1fr') 502 } 503 504 } 505} 506 507@Component 508export struct TapImage { 509 @StorageLink('tapIndex') @Watch('onTapIndexChange') tapIndex: number = -1; 510 @State tapColor: Color = Color.Black; 511 private index: number = 0; 512 private uri: Resource = { 513 id: 0, 514 type: 0, 515 moduleName: '', 516 bundleName: '' 517 }; 518 519 // 判断是否被选中 520 onTapIndexChange() { 521 if (this.tapIndex >= 0 && this.index === this.tapIndex) { 522 console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, red`); 523 this.tapColor = Color.Red; 524 } else { 525 console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, black`); 526 this.tapColor = Color.Black; 527 } 528 } 529 530 build() { 531 Column() { 532 Image(this.uri) 533 .objectFit(ImageFit.Cover) 534 .onClick(() => { 535 this.tapIndex = this.index; 536 }) 537 .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor }) 538 } 539 540 } 541} 542``` 543 544相比借助@StorageLink的双向同步机制实现事件通知,开发者可以使用emit订阅某个事件并接收事件回调的方式来减少开销,增强代码的可读性。 545 546> **说明:** 547> 548> emit接口不支持在Previewer预览器中使用。 549 550 551```ts 552// xxx.ets 553import { emitter } from '@kit.BasicServicesKit'; 554 555let nextId: number = 0; 556 557class ViewData { 558 title: string; 559 uri: Resource; 560 color: Color = Color.Black; 561 id: number; 562 563 constructor(title: string, uri: Resource) { 564 this.title = title; 565 this.uri = uri; 566 this.id = nextId++; 567 } 568} 569 570@Entry 571@Component 572struct Gallery { 573 // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 574 dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]; 575 scroller: Scroller = new Scroller(); 576 private preIndex: number = -1; 577 578 build() { 579 Column() { 580 Grid(this.scroller) { 581 ForEach(this.dataList, (item: ViewData) => { 582 GridItem() { 583 TapImage({ 584 uri: item.uri, 585 index: item.id 586 }) 587 }.aspectRatio(1) 588 .onClick(() => { 589 if (this.preIndex === item.id) { 590 return; 591 } 592 let innerEvent: emitter.InnerEvent = { eventId: item.id }; 593 // 选中态:黑变红 594 let eventData: emitter.EventData = { 595 data: { 596 'colorTag': 1 597 } 598 }; 599 emitter.emit(innerEvent, eventData); 600 601 if (this.preIndex != -1) { 602 console.info(`preIndex: ${this.preIndex}, index: ${item.id}, black`); 603 let innerEvent: emitter.InnerEvent = { eventId: this.preIndex }; 604 // 取消选中态:红变黑 605 let eventData: emitter.EventData = { 606 data: { 607 'colorTag': 0 608 } 609 }; 610 emitter.emit(innerEvent, eventData); 611 } 612 this.preIndex = item.id; 613 }) 614 }, (item: ViewData) => JSON.stringify(item)) 615 }.columnsTemplate('1fr 1fr') 616 } 617 618 } 619} 620 621@Component 622export struct TapImage { 623 @State tapColor: Color = Color.Black; 624 private index: number = 0; 625 private uri: Resource = { 626 id: 0, 627 type: 0, 628 moduleName: '', 629 bundleName: '' 630 }; 631 632 onTapIndexChange(colorTag: emitter.EventData) { 633 if (colorTag.data != null) { 634 this.tapColor = colorTag.data.colorTag ? Color.Red : Color.Black 635 } 636 } 637 638 aboutToAppear() { 639 //定义事件ID 640 let innerEvent: emitter.InnerEvent = { eventId: this.index }; 641 emitter.on(innerEvent, data => { 642 this.onTapIndexChange(data); 643 }); 644 } 645 646 build() { 647 Column() { 648 Image(this.uri) 649 .objectFit(ImageFit.Cover) 650 .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor }) 651 } 652 } 653} 654``` 655 656以上通知事件逻辑简单,也可以简化成三元表达式。 657 658```ts 659// xxx.ets 660class ViewData { 661 title: string; 662 uri: Resource; 663 color: Color = Color.Black; 664 665 constructor(title: string, uri: Resource) { 666 this.title = title; 667 this.uri = uri; 668 } 669} 670 671@Entry 672@Component 673struct Gallery { 674 // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 675 dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]; 676 scroller: Scroller = new Scroller(); 677 678 build() { 679 Column() { 680 Grid(this.scroller) { 681 ForEach(this.dataList, (item: ViewData, index?: number) => { 682 GridItem() { 683 TapImage({ 684 uri: item.uri, 685 index: index 686 }) 687 }.aspectRatio(1) 688 689 }, (item: ViewData, index?: number) => { 690 return JSON.stringify(item) + index; 691 }) 692 }.columnsTemplate('1fr 1fr') 693 } 694 695 } 696} 697 698@Component 699export struct TapImage { 700 @StorageLink('tapIndex') tapIndex: number = -1; 701 private index: number = 0; 702 private uri: Resource = { 703 id: 0, 704 type: 0, 705 moduleName: '', 706 bundleName: '' 707 }; 708 709 build() { 710 Column() { 711 Image(this.uri) 712 .objectFit(ImageFit.Cover) 713 .onClick(() => { 714 this.tapIndex = this.index; 715 }) 716 .border({ 717 width: 5, 718 style: BorderStyle.Dotted, 719 color: (this.tapIndex >= 0 && this.index === this.tapIndex) ? Color.Red : Color.Black 720 }) 721 } 722 } 723} 724``` 725 726 727### \@StorageProp和AppStorage接口配合使用时,需要注意更新规则 728 729使用setOrCreate/set接口更新key的值时,如果值相同,setOrCreate不会通知\@StorageLink/\@StorageProp更新,但因为\@StorageProp本身有数据副本,更改值不会同步给AppStorage,这会导致开发者误认己通过AppStorage改了值,但实际上未通知\@StorageProp更新值的情况。 730示例如下。 731 732```ts 733AppStorage.setOrCreate('propA', false); 734 735@Entry 736@Component 737struct Index { 738 @StorageProp('propA') @Watch('onChange') propA: boolean = false; 739 740 onChange() { 741 console.info(`propA change`); 742 } 743 744 aboutToAppear(): void { 745 this.propA = true; 746 } 747 748 build() { 749 Column() { 750 Text(`${this.propA}`) 751 Button('change') 752 .onClick(() => { 753 AppStorage.setOrCreate('propA', false); 754 console.info(`propA: ${this.propA}`); 755 }) 756 } 757 } 758} 759``` 760 761上述示例,在点击事件之前,propA的值已经在本地被更改为true,而AppStorage中存的值仍为false。当点击事件通过setOrCreate接口尝试更新propA的值为false时,由于AppStorage中的值为false,两者相等,不会触发更新同步,因此@StorageProp的值仍为true。 762 763实现二者同步有以下两种方式: 7641. 将\@StorageProp更改为\@StorageLink。 7652. 本地更改值的方式变为使用AppStorage.setOrCreate('propA', true)的方式。