在聲明式UI編程框架中,UI是程序狀態(tài)的運(yùn)行結(jié)果,用戶構(gòu)建了一個(gè)UI模型,其中應(yīng)用的運(yùn)行時(shí)的狀態(tài)是參數(shù)。當(dāng)參數(shù)改變時(shí),UI作為返回結(jié)果,也將進(jìn)行對(duì)應(yīng)的改變。這些運(yùn)行時(shí)的狀態(tài)變化所帶來(lái)的UI的重新渲染,在ArkUI中統(tǒng)稱為狀態(tài)管理機(jī)制。

裝飾器總覽

ArkUI提供了多種裝飾器,根據(jù)狀態(tài)變量的影響范圍,將所有的裝飾器可以大致分為:

  • 管理組件擁有狀態(tài)的裝飾器:組件級(jí)別的狀態(tài)管理,可以觀察組件內(nèi)變化,和不同組件層級(jí)的變化,但需要同一個(gè)頁(yè)面內(nèi)。
  • 管理應(yīng)用擁有狀態(tài)的裝飾器:應(yīng)用級(jí)別的狀態(tài)管理,可以觀察不同頁(yè)面,甚至不同UIAbility的狀態(tài)變化,是應(yīng)用內(nèi)全局的狀態(tài)管理。

上圖中,Components部分的裝飾器為組件級(jí)別的狀態(tài)管理,Application部分為應(yīng)用的狀態(tài)管理。

管理組件擁有的狀態(tài),即圖中Components級(jí)別的狀態(tài)管理:

  • @State:@State裝飾的變量擁有其所屬組件的狀態(tài),可以作為其子組件單向和雙向同步的數(shù)據(jù)源。當(dāng)其數(shù)值改變時(shí),會(huì)引起相關(guān)組件的渲染刷新。
  • @Prop:@Prop裝飾的變量可以和父組件建立單向同步關(guān)系,@Prop裝飾的變量是可變的,但修改不會(huì)同步回父組件。
  • @Link:@Link裝飾的變量和父組件構(gòu)建雙向同步關(guān)系的狀態(tài)變量,父組件會(huì)接受來(lái)自@Link裝飾的變量的修改的同步,父組件的更新也會(huì)同步給@Link裝飾的變量。
  • @Provide/@Consume:@Provide/@Consume裝飾的變量用于跨組件層級(jí)(多層組件)同步狀態(tài)變量,可以不需要通過(guò)參數(shù)命名機(jī)制傳遞,通過(guò)alias(別名)或者屬性名綁定。
  • @Observed:@Observed裝飾class,需要觀察多層嵌套場(chǎng)景的class需要被@Observed裝飾。單獨(dú)使用@Observed沒有任何作用,需要和@ObjectLink、@Prop連用。
  • @ObjectLink:@ObjectLink裝飾的變量接收@Observed裝飾的class的實(shí)例,應(yīng)用于觀察多層嵌套場(chǎng)景,和父組件的數(shù)據(jù)源構(gòu)建雙向同步。

管理應(yīng)用擁有的狀態(tài),即圖中Application級(jí)別的狀態(tài)管理:

  • LocalStorage:頁(yè)面級(jí)UI狀態(tài)存儲(chǔ),通常用于UIAbility內(nèi)、頁(yè)面間的狀態(tài)共享。
  • AppStorage:特殊的單例LocalStorage對(duì)象,由UI框架在應(yīng)用程序啟動(dòng)時(shí)創(chuàng)建,為應(yīng)用程序UI狀態(tài)屬性提供中央存儲(chǔ)。
  • PersistentStorage:持久化存儲(chǔ)UI狀態(tài),通常和AppStorage配合使用,選擇AppStorage存儲(chǔ)的數(shù)據(jù)寫入磁盤,以確保這些屬性在應(yīng)用程序重新啟動(dòng)時(shí)的值與應(yīng)用程序關(guān)閉時(shí)的值相同。
  • Environment:應(yīng)用程序運(yùn)行的設(shè)備的環(huán)境參數(shù),環(huán)境參數(shù)會(huì)同步到AppStorage中,可以和AppStorage搭配使用。

其他狀態(tài)管理功能

  • @Watch用于監(jiān)聽狀態(tài)變量的變化。
  • $$運(yùn)算符:給內(nèi)置組件提供TS變量的引用,使得TS變量和內(nèi)置組件的內(nèi)部狀態(tài)保持同步。

管理組件擁有的狀態(tài)

@State裝飾器:組件內(nèi)狀態(tài)

@State裝飾的變量,或稱為狀態(tài)變量,一旦變量擁有了狀態(tài)屬性,就和自定義組件的渲染綁定起來(lái)。當(dāng)狀態(tài)改變時(shí),UI會(huì)發(fā)生對(duì)應(yīng)的渲染改變。

@State裝飾的變量擁有以下特點(diǎn):

  • @State裝飾的變量與子組件中的@Prop裝飾變量之間建立單向數(shù)據(jù)同步,與@Link、@ObjectLink裝飾變量之間建立雙向數(shù)據(jù)同步。
  • @State裝飾的變量生命周期與其所屬自定義組件的生命周期相同。
  • 支持多種數(shù)據(jù)類型:允許 Object、class、string、number、boolean、enum、Date類型,以及這些類型的數(shù)組。
  • 內(nèi)部私有:標(biāo)記為 @State 的屬性是私有變量,只能在組件內(nèi)訪問(wèn)。
  • 支持多個(gè)實(shí)例:組件不同實(shí)例的內(nèi)部狀態(tài)數(shù)據(jù)獨(dú)立。
  • 需要本地初始化:必須為所有 @State 變量分配初始值,將變量保持未初始化可能導(dǎo)致框架行為未定義,初始值需要是有意義的值,比如設(shè)置 class 類型的值為 null 就是無(wú)意義的,會(huì)導(dǎo)致編譯報(bào)錯(cuò)。
  • 創(chuàng)建自定義組件時(shí)支持通過(guò)狀態(tài)變量名設(shè)置初始值:在創(chuàng)建組件實(shí)例時(shí),可以通過(guò)變量名顯式指定 @State 狀態(tài)屬性的初始值。

裝飾class對(duì)象類型的變量
當(dāng)裝飾的數(shù)據(jù)類型為class或者Object時(shí),可以觀察到自身的賦值的變化,和其屬性賦值的變化,即Object.keys(observedObject)返回的所有屬性

創(chuàng)建一個(gè)Model對(duì)象

class Model {
  public value: string;

  constructor(value: string) {
    this.value = value;
  }
}

在父組件中初始化@State裝飾對(duì)象,父組件初始化將會(huì)覆蓋本地初始化。

@Entry
@Component
struct EntryComponent {
  build() {
    Column() {
      // 此處指定的參數(shù)都將在初始渲染時(shí)覆蓋本地定義的默認(rèn)值,并不是所有的參數(shù)都需要從父組件初始化
      MyComponent({ count: 1, increaseBy: 2 })
        .width(300)
      MyComponent({ title: new Model('Hello World 2'), count: 7 })
    }
  }
}

在本地初始化@State裝飾對(duì)象,@State變量更新會(huì)觸發(fā)組件UI更新

@Component
struct MyComponent {
  @State title: Model = new Model('Hello World');
  @State count: number = 0;
  private increaseBy: number = 1;

  build() {
    Column() {
      Text(`${this.title.value}`)
        .margin(10)
      Button(`Click to change title`)
        .onClick(() => {
          // @State變量的更新將觸發(fā)上面的Text組件內(nèi)容更新
          this.title.value = this.title.value === 'Hello ArkUI' ? 'Hello World' : 'Hello ArkUI';
        })
        .width(300)
        .margin(10)

      Button(`Click to increase count = ${this.count}`)
        .onClick(() => {
          // @State變量的更新將觸發(fā)該Button組件的內(nèi)容更新
          this.count += this.increaseBy;
        })
        .width(300)
        .margin(10)
    }
  }
}

image

@Prop裝飾器:父子單向同步

@Prop裝飾的變量可以和父組件建立單向的同步關(guān)系。@Prop裝飾的變量是可變的,但是變化不會(huì)同步回其父組件。

@Prop裝飾的變量擁有以下特點(diǎn):

  • 支持簡(jiǎn)單數(shù)據(jù)類型:僅支持 number 、 string 、 booleanObjectclassenum 類型;
  • 內(nèi)部私有:標(biāo)記為 @Prop 的屬性是私有變量,只能在組件內(nèi)訪問(wèn)。
  • 支持多個(gè)實(shí)例:組件不同實(shí)例的內(nèi)部狀態(tài)數(shù)據(jù)獨(dú)立。
  • @Prop裝飾器不能在@Entry裝飾的自定義組件中使用。
@Component
struct CountDownComponent {
  @Prop count: number = 0;
  costOfOneAttempt: number = 1;

  build() {
    Column() {
      if (this.count > 0) {
        Text(`You have ${this.count} Nuggets left`)
      } else {
        Text('Game over!')
      }
      // @Prop裝飾的變量不會(huì)同步給父組件
      Button(`Try again`).onClick(() => {
        this.count -= this.costOfOneAttempt;
      })
    }
  }
}

@Entry
@Component
struct ParentComponent {
  @State countDownStartValue: number = 10;

  build() {
    Column() {
      Text(`Grant ${this.countDownStartValue} nuggets to play.`)
      // 父組件的數(shù)據(jù)源的修改會(huì)同步給子組件
      Button(`+1 - Nuggets in New Game`).onClick(() => {
        this.countDownStartValue += 1;
      })
      // 父組件的修改會(huì)同步給子組件
      Button(`-1  - Nuggets in New Game`).onClick(() => {
        this.countDownStartValue -= 1;
      })

      CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 })
    }
  }
}

@Link裝飾器:父子雙向同步

子組件中被@Link裝飾的變量與其父組件中對(duì)應(yīng)的數(shù)據(jù)源建立雙向數(shù)據(jù)綁定。@Link裝飾的變量與其父組件中的數(shù)據(jù)源共享相同的值。

@Link裝飾的變量擁有以下特點(diǎn):

  • Object、class、string、number、boolean、enum類型,以及這些類型的數(shù)組。支持Date類型。
  • 內(nèi)部私有:標(biāo)記為 @Link 的屬性是私有變量,只能在組件內(nèi)訪問(wèn)。
  • 支持多個(gè)實(shí)例:組件不同實(shí)例的內(nèi)部狀態(tài)數(shù)據(jù)獨(dú)立。
  • 不支持內(nèi)部初始化:在創(chuàng)建組件的新實(shí)例時(shí),必須將值傳遞給 @Link 修飾的變量進(jìn)行初始化,不支持在組件內(nèi)部進(jìn)行初始化。
class GreenButtonState {
  width: number = 0;

  constructor(width: number) {
    this.width = width;
  }
}

@Component
struct GreenButton {
  @Link greenButtonState: GreenButtonState;

  build() {
    Button('Green Button')
      .width(this.greenButtonState.width)
      .height(40)
      .backgroundColor('#64bb5c')
      .fontColor('#FFFFFF,90%')
      .onClick(() => {
        if (this.greenButtonState.width < 700) {
          // 更新class的屬性,變化可以被觀察到同步回父組件
          this.greenButtonState.width += 60;
        } else {
          // 更新class,變化可以被觀察到同步回父組件
          this.greenButtonState = new GreenButtonState(180);
        }
      })
  }
}

@Component
struct YellowButton {
  @Link yellowButtonState: number;

  build() {
    Button('Yellow Button')
      .width(this.yellowButtonState)
      .height(40)
      .backgroundColor('#f7ce00')
      .fontColor('#FFFFFF,90%')
      .onClick(() => {
        // 子組件的簡(jiǎn)單類型可以同步回父組件
        this.yellowButtonState += 40.0;
      })
  }
}

@Entry
@Component
struct ShufflingContainer {
  @State greenButtonState: GreenButtonState = new GreenButtonState(180);
  @State yellowButtonProp: number = 180;

  build() {
    Column() {
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }) {
        // 簡(jiǎn)單類型從父組件@State向子組件@Link數(shù)據(jù)同步
        Button('Parent View: Set yellowButton')
          .width(312)
          .height(40)
          .margin(12)
          .fontColor('#FFFFFF,90%')
          .onClick(() => {
            this.yellowButtonProp = (this.yellowButtonProp < 700) ? this.yellowButtonProp + 40 : 100;
          })
        // class類型從父組件@State向子組件@Link數(shù)據(jù)同步
        Button('Parent View: Set GreenButton')
          .width(312)
          .height(40)
          .margin(12)
          .fontColor('#FFFFFF,90%')
          .onClick(() => {
            this.greenButtonState.width = (this.greenButtonState.width < 700) ? this.greenButtonState.width + 100 : 100;
          })
        // class類型初始化@Link
        GreenButton({ greenButtonState: $greenButtonState }).margin(12)
        // 簡(jiǎn)單類型初始化@Link
        YellowButton({ yellowButtonState: $yellowButtonProp }).margin(12)
      }
    }
  }
}

1.點(diǎn)擊子組件GreenButton和YellowButton中的Button,子組件會(huì)發(fā)生相應(yīng)變化,將變化同步給父組件。因?yàn)锧Link是雙向同步,會(huì)將變化同步給@State。

2.當(dāng)點(diǎn)擊父組件ShufflingContainer中的Button時(shí),@State變化,也會(huì)同步給@Link,子組件也會(huì)發(fā)生對(duì)應(yīng)的刷新。

管理應(yīng)用擁有的狀態(tài)

LocalStorage:頁(yè)面級(jí)UI狀態(tài)存儲(chǔ)

LocalStorage是頁(yè)面級(jí)的UI狀態(tài)存儲(chǔ),通過(guò)@Entry裝飾器接收的參數(shù)可以在頁(yè)面內(nèi)共享同一個(gè)LocalStorage實(shí)例。LocalStorage支持UIAbility實(shí)例內(nèi)多個(gè)頁(yè)面間狀態(tài)共享。
LocalStorage是ArkTS為構(gòu)建頁(yè)面級(jí)別狀態(tài)變量提供存儲(chǔ)的內(nèi)存內(nèi)“數(shù)據(jù)庫(kù)”。

  • 應(yīng)用程序可以創(chuàng)建多個(gè)LocalStorage實(shí)例,LocalStorage實(shí)例可以在頁(yè)面內(nèi)共享,也可以通過(guò)GetShared接口,實(shí)現(xiàn)跨頁(yè)面、UIAbility實(shí)例內(nèi)共享。
  • 組件樹的根節(jié)點(diǎn),即被@Entry裝飾的@Component,可以被分配一個(gè)LocalStorage實(shí)例,此組件的所有子組件實(shí)例將自動(dòng)獲得對(duì)該LocalStorage實(shí)例的訪問(wèn)權(quán)限。
  • 被@Component裝飾的組件最多可以訪問(wèn)一個(gè)LocalStorage實(shí)例和AppStorage,未被@Entry裝飾的組件不可被獨(dú)立分配LocalStorage實(shí)例,只能接受父組件通過(guò)@Entry傳遞來(lái)的LocalStorage實(shí)例。一個(gè)LocalStorage實(shí)例在組件樹上可以被分配給多個(gè)組件。
  • LocalStorage中的所有屬性都是可變的。

LocalStorage根據(jù)與@Component裝飾的組件的同步類型不同,提供了兩個(gè)裝飾器:

  • @LocalStorageProp:@LocalStorageProp裝飾的變量和與LocalStorage中給定屬性建立單向同步關(guān)系。
  • @LocalStorageLink:@LocalStorageLink裝飾的變量和在@Component中創(chuàng)建與LocalStorage中給定屬性建立雙向同步關(guān)系。

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

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

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

@StorageProp

  • 單向同步:從AppStorage的對(duì)應(yīng)屬性到組件的狀態(tài)變量。
  • Object、 class、string、number、boolean、enum類型,以及這些類型的數(shù)組。
  • 組件本地的修改是允許的,但是AppStorage中給定的屬性一旦發(fā)生變化,將覆蓋本地的修改。
  • @StorageProp不支持從父節(jié)點(diǎn)初始化,只能AppStorage中key對(duì)應(yīng)的屬性初始化,如果沒有對(duì)應(yīng)key的話,將使用本地默認(rèn)值初始化。

@StorageLink(key) 裝飾的變量是組件內(nèi)部的狀態(tài)數(shù)據(jù),當(dāng)這些狀態(tài)數(shù)據(jù)被修改時(shí),將會(huì)調(diào)用所在組件的 build() 方法進(jìn)行UI刷新。組件通過(guò)使用 @StorageLink(key) 裝飾的狀態(tài)變量與 AppStorage 建立雙向數(shù)據(jù)綁定。

  • 支持多種數(shù)據(jù)類型:支持的數(shù)據(jù)類型和 @State 一致且支持 object 。
  • 需要本地初始化:必須為所有 @StorageLink 變量分配初始值。
  • 數(shù)據(jù)狀態(tài)全局化:使用 @StorageLink 修飾的數(shù)據(jù)變化后全局都會(huì)改變。
  • 數(shù)據(jù)持久化:通過(guò)搭配 PersistentStorage 接口實(shí)現(xiàn)數(shù)據(jù)持久化。
@Entry @Component struct ComponentTest {

  @StorageLink('time') time: string = "1648643734154";// 使用StorageLink標(biāo)記并初始化

  build() {
    Column({space: 10}) {

      Text(`父組件【${this.time}】`) // 使用time值
        .fontSize(20)
        .backgroundColor(Color.Pink)

      Button('更新時(shí)間')
        .onClick(() => {
          this.time = new Date().getTime().toString();// 更改time的值
        })
    }
    .width('100%')
    .height('100%')
    .padding(10)
  }
}

其他狀態(tài)管理

@Watch裝飾器:狀態(tài)變量更改通知

@Watch 用來(lái)監(jiān)聽狀態(tài)變量的變化,當(dāng)它修飾的狀態(tài)變量發(fā)生變更時(shí),回調(diào)相應(yīng)的方式,

  • 當(dāng)觀察到狀態(tài)變量的變化的時(shí)候,對(duì)應(yīng)的@Watch的回調(diào)方法將被觸發(fā);
  • @Watch方法在自定義組件的屬性變更之后同步執(zhí)行;
  • 如果在@Watch的方法里改變了其他的狀態(tài)變量,也會(huì)引起狀態(tài)變更和@Watch的執(zhí)行;
  • 在第一次初始化的時(shí)候,@Watch裝飾的方法不會(huì)被調(diào)用,即認(rèn)為初始化不是狀態(tài)變量的改變。只有在后續(xù)狀態(tài)改變時(shí),才會(huì)調(diào)用@Watch回調(diào)方法。
  • 為了避免循環(huán)的產(chǎn)生,建議不要在@Watch的回調(diào)方法里修改當(dāng)前裝飾的狀態(tài)變量;
  • 不建議在@Watch函數(shù)中調(diào)用async await,異步行為可能會(huì)導(dǎo)致重新渲染速度的性能問(wèn)題。
//給狀態(tài)變量 `count` 增加一個(gè) `@Watch` 裝飾器,通過(guò) `@Watch` 注冊(cè)一個(gè)回調(diào)方法 `function_name` 
@State @Watch("function_name") count : number = 0;

//當(dāng)狀態(tài)變量 `count` 被改變時(shí), 觸發(fā) `function_name` 回調(diào)。
function_name(propName: string): void {}

@Watch和自定義組件更新

@Component
struct TotalView {
  @Prop @Watch('onCountUpdated') count: number = 0;
  @State total: number = 0;
  // @Watch 回調(diào)
  onCountUpdated(propName: string): void {
    this.total += this.count;
  }

  build() {
    Text(`Total: ${this.total}`)
  }
}

@Entry
@Component
struct CountModifier {
  @State count: number = 0;

  build() {
    Column() {
      Button('add to basket')
        .onClick(() => {
          this.count++
        })
      TotalView({ count: this.count })
    }
  }
}

$$語(yǔ)法:內(nèi)置組件雙向同步

$$運(yùn)算符為系統(tǒng)內(nèi)置組件提供TS變量的引用,使得TS變量和系統(tǒng)內(nèi)置組件的內(nèi)部狀態(tài)保持同步。
TextInput方法的text參數(shù)為例:

@Entry
@Component
struct TextInputExample {
  @State text: string = ''
  controller: TextInputController = new TextInputController()

  build() {
    Column({ space: 20 }) {
      Text(this.text)
      TextInput({ text: $$this.text, placeholder: 'input your word...', controller: this.controller })
        .placeholderColor(Color.Grey)
        .placeholderFont({ size: 14, weight: 400 })
        .caretColor(Color.Blue)
        .width(300)
    }.width('100%').height('100%').justifyContent(FlexAlign.Center)
  }
}