AppStorage:應(yīng)用全局的UI狀態(tài)存儲

2024-01-25 12:06 更新

AppStorage是應(yīng)用全局的UI狀態(tài)存儲,是和應(yīng)用的進(jìn)程綁定的,由UI框架在應(yīng)用程序啟動時(shí)創(chuàng)建,為應(yīng)用程序UI狀態(tài)屬性提供中央存儲。

和AppStorage不同的是,LocalStorage是頁面級的,通常應(yīng)用于頁面內(nèi)的數(shù)據(jù)共享。而AppStorage是應(yīng)用級的全局狀態(tài)共享,還相當(dāng)于整個應(yīng)用的“中樞”,持久化數(shù)據(jù)PersistentStorage環(huán)境變量Environment都是通過和AppStorage中轉(zhuǎn),才可以和UI交互。

本文僅介紹AppStorage使用場景和相關(guān)的裝飾器:@StorageProp和@StorageLink。

概述

AppStorage是在應(yīng)用啟動的時(shí)候會被創(chuàng)建的單例。它的目的是為了提供應(yīng)用狀態(tài)數(shù)據(jù)的中心存儲,這些狀態(tài)數(shù)據(jù)在應(yīng)用級別都是可訪問的。AppStorage將在應(yīng)用運(yùn)行過程保留其屬性。屬性通過唯一的鍵字符串值訪問。

AppStorage可以和UI組件同步,且可以在應(yīng)用業(yè)務(wù)邏輯中被訪問。

AppStorage中的屬性可以被雙向同步,數(shù)據(jù)可以是存在于本地或遠(yuǎn)程設(shè)備上,并具有不同的功能,比如數(shù)據(jù)持久化(詳見PersistentStorage)。這些數(shù)據(jù)是通過業(yè)務(wù)邏輯中實(shí)現(xiàn),與UI解耦,如果希望這些數(shù)據(jù)在UI中使用,需要用到@StorageProp@StorageLink。

@StorageProp

在上文中已經(jīng)提到,如果要建立AppStorage和自定義組件的聯(lián)系,需要使用@StorageProp和@StorageLink裝飾器。使用@StorageProp(key)/@StorageLink(key)裝飾組件內(nèi)的變量,key標(biāo)識了AppStorage的屬性。

當(dāng)自定義組件初始化的時(shí)候,@StorageProp(key)/@StorageLink(key)裝飾的變量會通過給定的key,綁定在AppStorage對應(yīng)的屬性,完成初始化。本地初始化是必要的,因?yàn)闊o法保證AppStorage一定存在給定的key,這取決于應(yīng)用邏輯,是否在組件初始化之前在AppStorage實(shí)例中存入對應(yīng)的屬性。

@StorageProp(key)是和AppStorage中key對應(yīng)的屬性建立單向數(shù)據(jù)同步,我們允許本地改變的發(fā)生,但是對于@StorageProp,本地的修改永遠(yuǎn)不會同步回AppStorage中,相反,如果AppStorage給定key的屬性發(fā)生改變,改變會被同步給@StorageProp,并覆蓋掉本地的修改。

裝飾器使用規(guī)則說明

@StorageProp變量裝飾器

說明

裝飾器參數(shù)

key:常量字符串,必填(字符串需要有引號)。

允許裝飾的變量類型

Object class、string、number、boolean、enum類型,以及這些類型的數(shù)組。嵌套類型的場景請參考觀察變化和行為表現(xiàn)。

類型必須被指定,且必須和AppStorage中對應(yīng)屬性相同。不支持any,不允許使用undefined和null。

同步類型

單向同步:從AppStorage的對應(yīng)屬性到組件的狀態(tài)變量。

組件本地的修改是允許的,但是AppStorage中給定的屬性一旦發(fā)生變化,將覆蓋本地的修改。

被裝飾變量的初始值

必須指定,如果AppStorage實(shí)例中不存在屬性,則作為初始化默認(rèn)值,并存入AppStorage中。

變量的傳遞/訪問規(guī)則說明

傳遞/訪問

說明

從父節(jié)點(diǎn)初始化和更新

禁止,@StorageProp不支持從父節(jié)點(diǎn)初始化,只能AppStorage中key對應(yīng)的屬性初始化,如果沒有對應(yīng)key的話,將使用本地默認(rèn)值初始化

初始化子節(jié)點(diǎn)

支持,可用于初始化@State、@Link、@Prop、@Provide。

是否支持組件外訪問

否。

圖1 @StorageProp初始化規(guī)則圖示

觀察變化和行為表現(xiàn)

觀察變化

  • 當(dāng)裝飾的數(shù)據(jù)類型為boolean、string、number類型時(shí),可以觀察到數(shù)值的變化。
  • 當(dāng)裝飾的數(shù)據(jù)類型為class或者Object時(shí),可以觀察到賦值和屬性賦值的變化,即Object.keys(observedObject)返回的所有屬性。
  • 當(dāng)裝飾的對象是array時(shí),可以觀察到數(shù)組添加、刪除、更新數(shù)組單元的變化。

框架行為

  • 當(dāng)@StorageProp(key)裝飾的數(shù)值改變被觀察到時(shí),修改不會被同步回AppStorage對應(yīng)屬性鍵值key的屬性中。
  • 當(dāng)前@StorageProp(key)單向綁定的數(shù)據(jù)會被修改,即僅限于當(dāng)前組件的私有成員變量改變,其他的綁定該key的數(shù)據(jù)不會同步改變。
  • 當(dāng)@StorageProp(key)裝飾的數(shù)據(jù)本身是狀態(tài)變量,它的改變雖然不會同步回AppStorage中,但是會引起所屬的自定義組件的重新渲染。
  • 當(dāng)AppStorage中key對應(yīng)的屬性發(fā)生改變時(shí),會同步給所有@StorageProp(key)裝飾的數(shù)據(jù),@StorageProp(key)本地的修改將被覆蓋。

@StorageLink

@StorageLink(key)是和AppStorage中key對應(yīng)的屬性建立雙向數(shù)據(jù)同步:

  1. 本地修改發(fā)生,該修改會被寫回AppStorage中;
  2. AppStorage中的修改發(fā)生后,該修改會被同步到所有綁定AppStorage對應(yīng)key的屬性上,包括單向(@StorageProp和通過Prop創(chuàng)建的單向綁定變量)、雙向(@StorageLink和通過Link創(chuàng)建的雙向綁定變量)變量和其他實(shí)例(比如PersistentStorage)。

裝飾器使用規(guī)則說明

@StorageLink變量裝飾器

說明

裝飾器參數(shù)

key:常量字符串,必填(字符串需要有引號)。

允許裝飾的變量類型

Object、class、string、number、boolean、enum類型,以及這些類型的數(shù)組。嵌套類型的場景請參考觀察變化和行為表現(xiàn)

類型必須被指定,且必須和AppStorage中對應(yīng)屬性相同。不支持any,不允許使用undefined和null。

同步類型

雙向同步:從AppStorage的對應(yīng)屬性到自定義組件,從自定義組件到AppStorage對應(yīng)屬性。

被裝飾變量的初始值

必須指定,如果AppStorage實(shí)例中不存在屬性,則作為初始化默認(rèn)值,并存入AppStorage中。

變量的傳遞/訪問規(guī)則說明

傳遞/訪問

說明

從父節(jié)點(diǎn)初始化和更新

禁止。

初始化子節(jié)點(diǎn)

支持,可用于初始化常規(guī)變量、@State、@Link、@Prop、@Provide。

是否支持組件外訪問

否。

圖2 @StorageLink初始化規(guī)則圖示

觀察變化和行為表現(xiàn)

觀察變化

  • 當(dāng)裝飾的數(shù)據(jù)類型為boolean、string、number類型時(shí),可以觀察到數(shù)值的變化。
  • 當(dāng)裝飾的數(shù)據(jù)類型為class或者Object時(shí),可以觀察到賦值和屬性賦值的變化,即Object.keys(observedObject)返回的所有屬性。
  • 當(dāng)裝飾的對象是array時(shí),可以觀察到數(shù)組添加、刪除、更新數(shù)組單元的變化。

框架行為

  1. 當(dāng)@StorageLink(key)裝飾的數(shù)值改變被觀察到時(shí),修改將被同步回AppStorage對應(yīng)屬性鍵值key的屬性中。
  2. AppStorage中屬性鍵值key對應(yīng)的數(shù)據(jù)一旦改變,屬性鍵值key綁定的所有的數(shù)據(jù)(包括雙向@StorageLink和單向@StorageProp)都將同步修改。
  3. 當(dāng)@StorageLink(key)裝飾的數(shù)據(jù)本身是狀態(tài)變量,它的改變不僅僅會同步回AppStorage中,還會引起所屬的自定義組件的重新渲染。

使用場景

從應(yīng)用邏輯使用AppStorage和LocalStorage

AppStorage是單例,它的所有API都是靜態(tài)的,使用方法類似于LocalStorage對應(yīng)的非靜態(tài)方法。

  1. AppStorage.SetOrCreate('PropA', 47);
  2. let storage: LocalStorage = new LocalStorage({ 'PropA': 17 });
  3. let propA: number = AppStorage.Get('PropA') // propA in AppStorage == 47, propA in LocalStorage == 17
  4. var link1: SubscribedAbstractProperty<number> = AppStorage.Link('PropA'); // link1.get() == 47
  5. var link2: SubscribedAbstractProperty<number> = AppStorage.Link('PropA'); // link2.get() == 47
  6. var prop: SubscribedAbstractProperty<number> = AppStorage.Prop('PropA'); // prop.get() == 47
  7. link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
  8. prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48
  9. link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49
  10. storage.get('PropA') // == 17
  11. storage.set('PropA', 101);
  12. storage.get('PropA') // == 101
  13. AppStorage.Get('PropA') // == 49
  14. link1.get() // == 49
  15. link2.get() // == 49
  16. prop.get() // == 49

從UI內(nèi)部使用AppStorage和LocalStorage

@StorageLink變量裝飾器與AppStorage配合使用,正如@LocalStorageLink與LocalStorage配合使用一樣。此裝飾器使用AppStorage中的屬性創(chuàng)建雙向數(shù)據(jù)同步。

  1. AppStorage.SetOrCreate('PropA', 47);
  2. let storage = new LocalStorage({ 'PropA': 48 });
  3. @Entry(storage)
  4. @Component
  5. struct CompA {
  6. @StorageLink('PropA') storLink: number = 1;
  7. @LocalStorageLink('PropA') localStorLink: number = 1;
  8. build() {
  9. Column({ space: 20 }) {
  10. Text(`From AppStorage ${this.storLink}`)
  11. .onClick(() => this.storLink += 1)
  12. Text(`From LocalStorage ${this.localStorLink}`)
  13. .onClick(() => this.localStorLink += 1)
  14. }
  15. }
  16. }

不建議借助@StorageLink的雙向同步機(jī)制實(shí)現(xiàn)事件通知

不建議開發(fā)者使用@StorageLink和AppStorage的雙向同步的機(jī)制來實(shí)現(xiàn)事件通知,AppStorage是和UI相關(guān)的數(shù)據(jù)存儲,改變會帶來UI的刷新,相對于一般的事件通知,UI刷新的成本較大。

TapImage中的點(diǎn)擊事件,會觸發(fā)AppStorage中tapIndex對應(yīng)屬性的改變。因?yàn)锧StorageLink是雙向同步,修改會同步回AppStorage中,所以,所有綁定AppStorage的tapIndex自定義組件都會被通知UI刷新。UI刷新帶來的成本是巨大的,因此不建議開發(fā)者使用此方式來實(shí)現(xiàn)基本的事件通知功能。

  1. // xxx.ets
  2. class ViewData {
  3. title: string;
  4. uri: Resource;
  5. color: Color = Color.Black;
  6. constructor(title: string, uri: Resource) {
  7. this.title = title;
  8. this.uri = uri
  9. }
  10. }
  11. @Entry
  12. @Component
  13. struct Gallery2 {
  14. dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
  15. scroller: Scroller = new Scroller()
  16. build() {
  17. Column() {
  18. Grid(this.scroller) {
  19. ForEach(this.dataList, (item: ViewData, index?: number) => {
  20. GridItem() {
  21. TapImage({
  22. uri: item.uri,
  23. index: index
  24. })
  25. }.aspectRatio(1)
  26. }, (item: ViewData, index?: number) => {
  27. return JSON.stringify(item) + index;
  28. })
  29. }.columnsTemplate('1fr 1fr')
  30. }
  31. }
  32. }
  33. @Component
  34. export struct TapImage {
  35. @StorageLink('tapIndex') @Watch('onTapIndexChange') tapIndex: number = -1;
  36. @State tapColor: Color = Color.Black;
  37. private index: number = 0;
  38. private uri: Resource = {
  39. id: 0,
  40. type: 0,
  41. moduleName: "",
  42. bundleName: ""
  43. };
  44. // 判斷是否被選中
  45. onTapIndexChange() {
  46. if (this.tapIndex >= 0 && this.index === this.tapIndex) {
  47. console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, red`)
  48. this.tapColor = Color.Red;
  49. } else {
  50. console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, black`)
  51. this.tapColor = Color.Black;
  52. }
  53. }
  54. build() {
  55. Column() {
  56. Image(this.uri)
  57. .objectFit(ImageFit.Cover)
  58. .onClick(() => {
  59. this.tapIndex = this.index;
  60. })
  61. .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
  62. }
  63. }
  64. }

開發(fā)者可以使用emit訂閱某個事件并接收事件回調(diào),可以減少開銷,增強(qiáng)代碼的可讀性。

  1. // xxx.ets
  2. import emitter from '@ohos.events.emitter';
  3. let NextID: number = 0;
  4. class ViewData {
  5. title: string;
  6. uri: Resource;
  7. color: Color = Color.Black;
  8. id: number;
  9. constructor(title: string, uri: Resource) {
  10. this.title = title;
  11. this.uri = uri
  12. this.id = NextID++;
  13. }
  14. }
  15. @Entry
  16. @Component
  17. struct Gallery2 {
  18. dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
  19. scroller: Scroller = new Scroller()
  20. private preIndex: number = -1
  21. build() {
  22. Column() {
  23. Grid(this.scroller) {
  24. ForEach(this.dataList, (item: ViewData) => {
  25. GridItem() {
  26. TapImage({
  27. uri: item.uri,
  28. index: item.id
  29. })
  30. }.aspectRatio(1)
  31. .onClick(() => {
  32. if (this.preIndex === item.id) {
  33. return
  34. }
  35. let innerEvent: emitter.InnerEvent = { eventId: item.id }
  36. // 選中態(tài):黑變紅
  37. let eventData: emitter.EventData = {
  38. data: {
  39. "colorTag": 1
  40. }
  41. }
  42. emitter.emit(innerEvent, eventData)
  43. if (this.preIndex != -1) {
  44. console.info(`preIndex: ${this.preIndex}, index: ${item.id}, black`)
  45. let innerEvent: emitter.InnerEvent = { eventId: this.preIndex }
  46. // 取消選中態(tài):紅變黑
  47. let eventData: emitter.EventData = {
  48. data: {
  49. "colorTag": 0
  50. }
  51. }
  52. emitter.emit(innerEvent, eventData)
  53. }
  54. this.preIndex = item.id
  55. })
  56. }, (item: ViewData) => JSON.stringify(item))
  57. }.columnsTemplate('1fr 1fr')
  58. }
  59. }
  60. }
  61. @Component
  62. export struct TapImage {
  63. @State tapColor: Color = Color.Black;
  64. private index: number = 0;
  65. private uri: Resource = {
  66. id: 0,
  67. type: 0,
  68. moduleName: "",
  69. bundleName: ""
  70. };
  71. onTapIndexChange(colorTag: emitter.EventData) {
  72. if (colorTag.data != null) {
  73. this.tapColor = colorTag.data.colorTag ? Color.Red : Color.Black
  74. }
  75. }
  76. aboutToAppear() {
  77. //定義事件ID
  78. let innerEvent: emitter.InnerEvent = { eventId: this.index }
  79. emitter.on(innerEvent, data => {
  80. this.onTapIndexChange(data)
  81. })
  82. }
  83. build() {
  84. Column() {
  85. Image(this.uri)
  86. .objectFit(ImageFit.Cover)
  87. .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
  88. }
  89. }
  90. }

以上通知事件邏輯簡單,也可以簡化成三元表達(dá)式。

  1. // xxx.ets
  2. class ViewData {
  3. title: string;
  4. uri: Resource;
  5. color: Color = Color.Black;
  6. constructor(title: string, uri: Resource) {
  7. this.title = title;
  8. this.uri = uri
  9. }
  10. }
  11. @Entry
  12. @Component
  13. struct Gallery2 {
  14. dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
  15. scroller: Scroller = new Scroller()
  16. build() {
  17. Column() {
  18. Grid(this.scroller) {
  19. ForEach(this.dataList, (item: ViewData, index?: number) => {
  20. GridItem() {
  21. TapImage({
  22. uri: item.uri,
  23. index: index
  24. })
  25. }.aspectRatio(1)
  26. }, (item: ViewData, index?: number) => {
  27. return JSON.stringify(item) + index;
  28. })
  29. }.columnsTemplate('1fr 1fr')
  30. }
  31. }
  32. }
  33. @Component
  34. export struct TapImage {
  35. @StorageLink('tapIndex') tapIndex: number = -1;
  36. @State tapColor: Color = Color.Black;
  37. private index: number = 0;
  38. private uri: Resource = {
  39. id: 0,
  40. type: 0,
  41. moduleName: "",
  42. bundleName: ""
  43. };
  44. build() {
  45. Column() {
  46. Image(this.uri)
  47. .objectFit(ImageFit.Cover)
  48. .onClick(() => {
  49. this.tapIndex = this.index;
  50. })
  51. .border({
  52. width: 5,
  53. style: BorderStyle.Dotted,
  54. color: (this.tapIndex >= 0 && this.index === this.tapIndex) ? Color.Red : Color.Black
  55. })
  56. }
  57. }
  58. }

限制條件

AppStorage與PersistentStorage以及Environment配合使用時(shí),需要注意以下幾點(diǎn):

  • 在AppStorage中創(chuàng)建屬性后,調(diào)用PersistentStorage.persistProp()接口時(shí),會使用在AppStorage中已經(jīng)存在的值,并覆蓋PersistentStorage中的同名屬性,所以建議要使用相反的調(diào)用順序,反例可見在PersistentStorage之前訪問AppStorage中的屬性。
  • 如果在AppStorage中已經(jīng)創(chuàng)建屬性后,再調(diào)用Environment.envProp()創(chuàng)建同名的屬性,會調(diào)用失敗。因?yàn)锳ppStorage已經(jīng)有同名屬性,Environment環(huán)境變量不會再寫入AppStorage中,所以建議AppStorage中屬性不要使用Environment預(yù)置環(huán)境變量名。
  • 狀態(tài)裝飾器裝飾的變量,改變會引起UI的渲染更新,如果改變的變量不是用于UI更新,只是用于消息傳遞,推薦使用 emitter方式。例子可見不建議借助@StorageLink的雙向同步機(jī)制實(shí)現(xiàn)事件通知。
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號