鴻蒙HarmonyOS實戰-ArkTS語言(狀態管理)

蜀道山QAQ發表於2024-03-21

🚀前言

狀態管理是指在應用程式中維護和更新應用程式狀態的過程。在一個程式中,可能有很多不同的元件和模組,它們需要共享和相互作用的狀態。如果沒有一個明確的方式來管理這些狀態,就會導致程式碼混亂、不易維護和難以擴充套件。

狀態管理的目標是提供一種機制,使得所有的元件和模組都可以訪問和更新同一個狀態。這個狀態通常是儲存在一箇中央儲存區域中,被稱為狀態儲存或狀態容器。狀態管理通常與應用程式的響應式設計緊密相連,以便在狀態改變時自動更新應用程式的介面。

🚀一、ArkTS語言狀態管理

🔎1.概述

在宣告式UI程式設計框架中,應用程式的UI是由程式狀態驅動的。使用者構建一個UI模型,其中應用的執行時狀態作為引數傳遞進去。當引數改變時,UI會根據新的引數重新渲染。這個執行時狀態的變化是由狀態管理機制來處理的,它會監控狀態的變化,並自動更新UI的渲染。

在ArkUI中,自定義元件的變數必須被裝飾器裝飾為狀態變數,這樣它們的改變才能引起UI的重新渲染。如果不使用狀態變數,UI只能在初始化時渲染,後續將不會再重新整理。狀態變數和UI之間的關係如下圖所示:

image

  • View(UI):UI渲染,指將build方法內的UI描述和@Builder裝飾的方法內的UI描述對映到介面。

  • State:狀態,指驅動UI更新的資料。使用者透過觸發元件的事件方法,改變狀態資料。狀態資料的改變,引起UI的重新渲染。

🦋1.1 基本概念

@Component
struct MyComponent {
  //狀態變數:被狀態裝飾器裝飾的變數,狀態變數值的改變會引起UI的渲染更新
  @State count: number = 0;
  //常規變數:沒有被狀態裝飾器裝飾的變數,通常應用於輔助計算。
  private increaseBy: number = 1;

  build() {
  }
}

@Component
struct Parent {
  build() {
    Column() {
      // 從父元件初始化,覆蓋本地定義的預設值
      MyComponent({ count: 1, increaseBy: 2 })
    }
  }
}

🦋1.2 裝飾器總覽

ArkUI提供了多種裝飾器主要分為:管理元件擁有的狀態、管理應用擁有的狀態、其他狀態管理功能,主要圖形如下:

image

☀️1.2.1 管理元件擁有的狀態

image

🌈1.2.1.1 @State 元件內狀態

image

@State變數裝飾器只支援Object、class、string、number、boolean、enum型別,以及這些型別的陣列。不支援複雜型別(比如Date型別)

父子元件初始化和傳遞裝飾圖如下:

image

🍬1.2.1.1.1 變化規則

1、可變型別(boolean、string、number)

// for simple type
@State count: number = 0;
// value changing can be observed
this.count = 1;

2、可變型別(class、Object)

class ClassA {
  public value: string;

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

class Model {
  public value: string;
  public name: ClassA;
  constructor(value: string, a: ClassA) {
    this.value = value;
    this.name = a;
  }
}

// class型別
@State title: Model = new Model('Hello', new ClassA('World'));

// class型別賦值
this.title = new Model('Hi', new ClassA('ArkUI'));

// class屬性的賦值
this.title.value = 'Hi'

// 巢狀的屬性賦值觀察不到
this.title.name.value = 'ArkUI'

3、可變型別(array)

class Model {
  public value: number;
  constructor(value: number) {
    this.value = value;
  }
}
@State title: Model[] = [new Model(11), new Model(1)]

this.title = [new Model(2)]

this.title[0] = new Model(2)

this.title.pop()

this.title.push(new Model(12))

🍬1.2.1.1.2 使用場景
1、簡單型別

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

  build() {
    Button(`click times: ${this.count}`)
      .onClick(() => {
        this.count += 1;
      })
  }
}

2、其他型別

class Model {
  public value: string;

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

@Entry
@Component
struct EntryComponent {
  build() {
    Column() {
      // 此處指定的引數都將在初始渲染時覆蓋本地定義的預設值,並不是所有的引數都需要從父元件初始化
      MyComponent({ count: 1, increaseBy: 2 })
      MyComponent({ title: new Model('Hello, World 2'), count: 7 })
    }
  }
}

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

  build() {
    Column() {
      Text(`${this.title.value}`)
      Button(`Click to change title`).onClick(() => {
        // @State變數的更新將觸發上面的Text元件內容更新
        this.title.value = this.title.value === 'Hello ArkUI' ? 'Hello World' : 'Hello ArkUI';
      })

      Button(`Click to increase count=${this.count}`).onClick(() => {
        // @State變數的更新將觸發該Button元件的內容更新
        this.count += this.increaseBy;
      })
    }
  }
}

🌈1.2.1.2 @Prop 父子單向同步

image

@Prop變數裝飾器只支援string、number、boolean、enum型別,以及這些型別的陣列。不支援複雜型別(比如any型別)

父子元件初始化和傳遞裝飾圖如下:

image

🍬1.2.1.2.1 變化規則

1、簡單型別

// 簡單型別
@Prop count: number;
// 賦值的變化可以被觀察到
this.count = 1;

對於@State和@Prop的同步場景:

  • 使用父元件中@State變數的值初始化子元件中的@Prop變數。當@State變數變化時,該變數值也會同步更新至@Prop變數。

  • @Prop裝飾的變數的修改不會影響其資料來源@State裝飾變數的值。

  • 除了@State,資料來源也可以用@Link或@Prop裝飾,對@Prop的同步機制是相同的。

  • 資料來源和@Prop變數的型別需要相同。

🍬1.2.1.2.2 使用場景

1、父元件@State到子元件@Prop簡單資料型別同步

@Component
struct CountDownComponent {
  @Prop count: number;
  costOfOneAttempt: number = 1;

  build() {
    Column() {
      if (this.count > 0) {
        Text(`You have ${this.count} Nuggets left`)
      } else {
        Text('Game over!')
      }
      // @Prop裝飾的變數不會同步給父元件
      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.`)
      // 父元件的資料來源的修改會同步給子元件
      Button(`+1 - Nuggets in New Game`).onClick(() => {
        this.countDownStartValue += 1;
      })
      // 父元件的修改會同步給子元件
      Button(`-1  - Nuggets in New Game`).onClick(() => {
        this.countDownStartValue -= 1;
      })
      CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 })
    }
  }
}

2、父元件@State陣列項到子元件@Prop簡單資料型別同步

@Component
struct Child {
  @Prop value: number;

  build() {
    Text(`${this.value}`)
      .fontSize(50)
      .onClick(()=>{this.value++})
  }
}
@Entry
@Component
struct Index {
  @State arr: number[] = [1,2,3];
  build() {
    Row() {
      Column() {
        Child({value: this.arr[0]})
        Child({value: this.arr[1]})
        Child({value: this.arr[2]})
        Divider().height(5)
        ForEach(this.arr, 
          item => {
            Child({value: item})
          }, 
          item => item.toString()
        )
        Text('replace entire arr')
        .fontSize(50)
        .onClick(()=>{
          // 兩個陣列都包含項“3”。
          this.arr = this.arr[0] == 1 ? [3,4,5] : [1,2,3];
        })
      }
    }
  }
}

3、從父元件中的@State類物件屬性到@Prop簡單型別的同步

class Book {
  public title: string;
  public pages: number;
  public readIt: boolean = false;

  constructor(title: string, pages: number) {
    this.title = title;
    this.pages = pages;
  }
}

@Component
struct ReaderComp {
  @Prop title: string;
  @Prop readIt: boolean;

  build() {
    Row() {
      Text(this.title)
      Text(`... ${this.readIt ? 'I have read' : 'I have not read it'}`)
        .onClick(() => this.readIt = true)
    }
  }
}
@Entry
@Component
struct Library {
  @State book: Book = new Book('100 secrets of C++', 765);
  build() {
    Column() {
      ReaderComp({ title: this.book.title, readIt: this.book.readIt })
      ReaderComp({ title: this.book.title, readIt: this.book.readIt })
    }
  }
}

4、@Prop本地初始化不和父元件同步

@Component
struct MyComponent {
  @Prop customCounter: number;
  @Prop customCounter2: number = 5;

  build() {
    Column() {
      Row() {
        Text(`From Main: ${this.customCounter}`).width(90).height(40).fontColor('#FF0010')
      }

      Row() {
        Button('Click to change locally !').width(180).height(60).margin({ top: 10 })
          .onClick(() => {
            this.customCounter2++
          })
      }.height(100).width(180)
      Row() {
        Text(`Custom Local: ${this.customCounter2}`).width(90).height(40).fontColor('#FF0010')
      }
    }
  }
}
@Entry
@Component
struct MainProgram {
  @State mainCounter: number = 10;
  build() {
    Column() {
      Row() {
        Column() {
          Button('Click to change number').width(480).height(60).margin({ top: 10, bottom: 10 })
            .onClick(() => {
              this.mainCounter++
            })
        }
      }
      Row() {
        Column()
        // customCounter必須從父元件初始化,因為MyComponent的customCounter成員變數缺少本地初始化;此處,customCounter2可以不做初始化。
        MyComponent({ customCounter: this.mainCounter })
        // customCounter2也可以從父元件初始化,父元件初始化的值會覆蓋子元件customCounter2的本地初始化的值
        MyComponent({ customCounter: this.mainCounter, customCounter2: this.mainCounter })
      }
    }
  }
}

🌈1.2.1.3 @Link 父子雙向同步

父元件中@State, @StorageLink和@Link 和子元件@Link可以建立雙向資料同步。

@Link 變數裝飾器只支援string、number、boolean、enum型別,以及這些型別的陣列。不支援複雜型別(比如any型別)

父子元件初始化和傳遞裝飾圖如下:

image

🍬1.2.1.3.1 變化規則
  • 當裝飾的資料型別為boolean、string、number型別時,可以同步觀察到數值的變化。

  • 當裝飾的資料型別為class或者Object時,可以觀察到賦值和屬性賦值的變化,即Object.keys(observedObject)返回的所有屬性。

  • 當裝飾的物件是array時,可以觀察到陣列新增、刪除、更新陣列單元的變化。

🍬1.2.1.3.2 使用場景

1、簡單型別和類物件型別的@Link

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(150.0)
      .backgroundColor('#00ff00')
      .onClick(() => {
        if (this.greenButtonState.width < 700) {
          // 更新class的屬性,變化可以被觀察到同步回父元件
          this.greenButtonState.width += 125;
        } else {
          // 更新class,變化可以被觀察到同步回父元件
          this.greenButtonState = new GreenButtonState(100);
        }
      })
  }
}
@Component
struct YellowButton {
  @Link yellowButtonState: number;
  build() {
    Button('Yellow Button')
      .width(this.yellowButtonState)
      .height(150.0)
      .backgroundColor('#ffff00')
      .onClick(() => {
        // 子元件的簡單型別可以同步回父元件
        this.yellowButtonState += 50.0;
      })
  }
}
@Entry
@Component
struct ShufflingContainer {
  @State greenButtonState: GreenButtonState = new GreenButtonState(300);
  @State yellowButtonProp: number = 100;
  build() {
    Column() {
      // 簡單型別從父元件@State向子元件@Link資料同步
      Button('Parent View: Set yellowButton')
        .onClick(() => {
          this.yellowButtonProp = (this.yellowButtonProp < 700) ? this.yellowButtonProp + 100 : 100;
        })
      // class型別從父元件@State向子元件@Link資料同步
      Button('Parent View: Set GreenButton')
        .onClick(() => {
          this.greenButtonState.width = (this.greenButtonState.width < 700) ? this.greenButtonState.width + 100 : 100;
        })
      // class型別初始化@Link
      GreenButton({ greenButtonState: $greenButtonState })
      // 簡單型別初始化@Link
      YellowButton({ yellowButtonState: $yellowButtonProp })
    }
  }
}

2、陣列型別的@Link

@Component
struct Child {
  @Link items: number[];

  build() {
    Column() {
      Button(`Button1: push`).onClick(() => {
        this.items.push(this.items.length + 1);
      })
      Button(`Button2: replace whole item`).onClick(() => {
        this.items = [100, 200, 300];
      })
    }
  }
}
@Entry
@Component
struct Parent {
  @State arr: number[] = [1, 2, 3];
  build() {
    Column() {
      Child({ items: $arr })
      ForEach(this.arr,
        item => {
          Text(`${item}`)
        },
        item => item.toString()
      )
    }
  }
}
🌈1.2.1.4 @Provide/@Consume 與後代元件雙向同步

image

@Prop變數裝飾器只支援string、number、boolean、enum型別,以及這些型別的陣列。不支援複雜型別(比如any型別)

父子元件初始化和傳遞裝飾圖如下:

image

🍬1.2.1.4.1 變化規則
  • 當裝飾的資料型別為boolean、string、number型別時,可以觀察到數值的變化。

  • 當裝飾的資料型別為class或者Object的時候,可以觀察到賦值和屬性賦值的變化(屬性為Object.keys(observedObject)返回的所有屬性)。

  • 當裝飾的物件是array的時候,可以觀察到陣列的新增、刪除、更新陣列單元。

🍬1.2.1.4.2 使用場景
@Component
struct CompD {
  // @Consume裝飾的變數透過相同的屬性名繫結其祖先元件CompA內的@Provide裝飾的變數
  @Consume reviewVotes: number;

  build() {
    Column() {
      Text(`reviewVotes(${this.reviewVotes})`)
      Button(`reviewVotes(${this.reviewVotes}), give +1`)
        .onClick(() => this.reviewVotes += 1)
    }
    .width('50%')
  }
}
@Component
struct CompC {
  build() {
    Row({ space: 5 }) {
      CompD()
      CompD()
    }
  }
}
@Component
struct CompB {
  build() {
    CompC()
  }
}
@Entry
@Component
struct CompA {
  // @Provide裝飾的變數reviewVotes由入口元件CompA提供其後代元件
  @Provide reviewVotes: number = 0;
  build() {
    Column() {
      Button(`reviewVotes(${this.reviewVotes}), give +1`)
        .onClick(() => this.reviewVotes += 1)
      CompB()
    }
  }
}

image

型別必須是@Observed裝飾的class,可用於初始化常規變數、@State、@Link、@Prop、@Provide

巢狀類物件裝飾圖如下:
image

🍬1.2.1.5.1 變化規則
class ClassA {
  public c: number;

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

@Observed
class ClassB {
  public a: ClassA;
  public b: number;

  constructor(a: ClassA, b: number) {
    this.a = a;
    this.b = b;
  }
}

@ObjectLink b: ClassB

// 賦值變化可以被觀察到
this.b.a = new ClassA(5)
this.b.b = 5

// ClassA沒有被@Observed裝飾,其屬性的變化觀察不到
this.b.a.c = 5
🍬1.2.1.5.2 使用場景

1、巢狀物件

// objectLinkNestedObjects.ets
let NextID: number = 1;

@Observed
class ClassA {
  public id: number;
  public c: number;

  constructor(c: number) {
    this.id = NextID++;
    this.c = c;
  }
}

@Observed
class ClassB {
  public a: ClassA;

  constructor(a: ClassA) {
    this.a = a;
  }
}

@Component
struct ViewA {
  label: string = 'ViewA1';
  @ObjectLink a: ClassA;

  build() {
    Row() {
      Button(`ViewA [${this.label}] this.a.c=${this.a.c} +1`)
        .onClick(() => {
          this.a.c += 1;
        })
    }
  }
}
@Entry
@Component
struct ViewB {
  @State b: ClassB = new ClassB(new ClassA(0));
  build() {
    Column() {
      ViewA({ label: 'ViewA #1', a: this.b.a })
      ViewA({ label: 'ViewA #2', a: this.b.a })
      Button(`ViewB: this.b.a.c+= 1`)
        .onClick(() => {
          this.b.a.c += 1;
        })
      Button(`ViewB: this.b.a = new ClassA(0)`)
        .onClick(() => {
          this.b.a = new ClassA(0);
        })
      Button(`ViewB: this.b = new ClassB(ClassA(0))`)
        .onClick(() => {
          this.b = new ClassB(new ClassA(0));
        })
    }
  }
}

2、物件陣列

@Component
struct ViewA {
  // 子元件ViewA的@ObjectLink的型別是ClassA
  @ObjectLink a: ClassA;
  label: string = 'ViewA1';

  build() {
    Row() {
      Button(`ViewA [${this.label}] this.a.c = ${this.a.c} +1`)
        .onClick(() => {
          this.a.c += 1;
        })
    }
  }
}
@Entry
@Component
struct ViewB {
  // ViewB中有@State裝飾的ClassA[]
  @State arrA: ClassA[] = [new ClassA(0), new ClassA(0)];
  build() {
    Column() {
      ForEach(this.arrA,
        (item) => {
          ViewA({ label: `#${item.id}`, a: item })
        },
        (item) => item.id.toString()
      )
      // 使用@State裝飾的陣列的陣列項初始化@ObjectLink,其中陣列項是被@Observed裝飾的ClassA的例項
      ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] })
      ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] })
      Button(`ViewB: reset array`)
        .onClick(() => {
          this.arrA = [new ClassA(0), new ClassA(0)];
        })
      Button(`ViewB: push`)
        .onClick(() => {
          this.arrA.push(new ClassA(0))
        })
      Button(`ViewB: shift`)
        .onClick(() => {
          this.arrA.shift()
        })
      Button(`ViewB: chg item property in middle`)
        .onClick(() => {
          this.arrA[Math.floor(this.arrA.length / 2)].c = 10;
        })
      Button(`ViewB: chg item property in middle`)
        .onClick(() => {
          this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11);
        })
    }
  }
}

3、二維陣列

@Observed
class StringArray extends Array<String> {
}

@Observed
class StringArray extends Array<String> {
}

@Component
struct ItemPage {
  @ObjectLink itemArr: StringArray;

  build() {
    Row() {
      Text('ItemPage')
        .width(100).height(100)

      ForEach(this.itemArr,
        item => {
          Text(item)
            .width(100).height(100)
        },
        item => item
      )
    }
  }
}

@Entry
@Component
struct IndexPage {
  @State arr: Array<StringArray> = [new StringArray(), new StringArray(), new StringArray()];

  build() {
    Column() {
      ItemPage({ itemArr: this.arr[0] })
      ItemPage({ itemArr: this.arr[1] })
      ItemPage({ itemArr: this.arr[2] })

      Divider()

      ForEach(this.arr,
        itemArr => {
          ItemPage({ itemArr: itemArr })
        },
        itemArr => itemArr[0]
      )

      Divider()

      Button('update')
        .onClick(() => {
          console.error('Update all items in arr');
          if (this.arr[0][0] !== undefined) {
            // 正常情況下需要有一個真實的ID來與ForEach一起使用,但此處沒有
            // 因此需要確保推送的字串是唯一的。
            this.arr[0].push(`${this.arr[0].slice(-1).pop()}${this.arr[0].slice(-1).pop()}`);
            this.arr[1].push(`${this.arr[1].slice(-1).pop()}${this.arr[1].slice(-1).pop()}`);
            this.arr[2].push(`${this.arr[2].slice(-1).pop()}${this.arr[2].slice(-1).pop()}`);
          } else {
            this.arr[0].push('Hello');
            this.arr[1].push('World');
            this.arr[2].push('!');
          }
        })
    }
  }
}
☀️1.2.2 管理應用擁有的狀態

image

🌈1.2.2.1 LocalStorage:頁面級UI狀態儲存
🍬1.2.2.1.1 變化規則

image

  • 當@LocalStorageLink(key)裝飾的數值改變被觀察到時,修改將被同步回LocalStorage對應屬性鍵值key的屬性中。

  • LocalStorage中屬性鍵值key對應的資料一旦改變,屬性鍵值key繫結的所有的資料(包括雙向@LocalStorageLink和單向@LocalStorageProp)都將同步修改;

  • 當@LocalStorageLink(key)裝飾的資料本身是狀態變數,它的改變不僅僅會同步回LocalStorage中,還會引起所屬的自定義元件的重新渲染。

🍬1.2.2.1.2 使用場景

1、應用邏輯使用LocalStorage

let storage = new LocalStorage({ 'PropA': 47 }); // 建立新例項並使用給定物件初始化
let propA = storage.get('PropA') // propA == 47
let link1 = storage.link('PropA'); // link1.get() == 47
let link2 = storage.link('PropA'); // link2.get() == 47
let prop = storage.prop('PropA'); // prop.get() = 47
link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
prop.set(1); // one-way sync: prop.get()=1; but link1.get() == link2.get() == 48
link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49

2、從UI內部使用LocalStorage

// 建立新例項並使用給定物件初始化
let storage = new LocalStorage({ 'PropA': 47 });

@Component
struct Child {
  // @LocalStorageLink變數裝飾器與LocalStorage中的'PropA'屬性建立雙向繫結
  @LocalStorageLink('PropA') storLink2: number = 1;

  build() {
    Button(`Child from LocalStorage ${this.storLink2}`)
      // 更改將同步至LocalStorage中的'PropA'以及Parent.storLink1
      .onClick(() => this.storLink2 += 1)
  }
}
// 使LocalStorage可從@Component元件訪問
@Entry(storage)
@Component
struct CompA {
  // @LocalStorageLink變數裝飾器與LocalStorage中的'PropA'屬性建立雙向繫結
  @LocalStorageLink('PropA') storLink1: number = 1;
  build() {
    Column({ space: 15 }) {
      Button(`Parent from LocalStorage ${this.storLink1}`) // initial value from LocalStorage will be 47, because 'PropA' initialized already
        .onClick(() => this.storLink1 += 1)
      // @Component子元件自動獲得對CompA LocalStorage例項的訪問許可權。
      Child()
    }
  }
}

3、@LocalStorageProp和LocalStorage單向同步的簡單場景

// 建立新例項並使用給定物件初始化
let storage = new LocalStorage({ 'PropA': 47 });
// 使LocalStorage可從@Component元件訪問
@Entry(storage)
@Component
struct CompA {
  // @LocalStorageProp變數裝飾器與LocalStorage中的'PropA'屬性建立單向繫結
  @LocalStorageProp('PropA') storProp1: number = 1;

  build() {
    Column({ space: 15 }) {
      // 點選後從47開始加1,只改變當前元件顯示的storProp1,不會同步到LocalStorage中
      Button(`Parent from LocalStorage ${this.storProp1}`)
        .onClick(() => this.storProp1 += 1)
      Child()
    }
  }
}
@Component
struct Child {
  // @LocalStorageProp變數裝飾器與LocalStorage中的'PropA'屬性建立單向繫結
  @LocalStorageProp('PropA') storProp2: number = 2;
  build() {
    Column({ space: 15 }) {
      // 當CompA改變時,當前storProp2不會改變,顯示47
      Text(`Parent from LocalStorage ${this.storProp2}`)
    }
  }
}

4、@LocalStorageLink和LocalStorage雙向同步的簡單場景

// 構造LocalStorage例項
let storage = new LocalStorage({ 'PropA': 47 });
// 呼叫link9+介面構造'PropA'的雙向同步資料,linkToPropA 是全域性變數
let linkToPropA = storage.link('PropA');

@Entry(storage)
@Component
struct CompA {

  // @LocalStorageLink('PropA')在CompA自定義元件中建立'PropA'的雙向同步資料,初始值為47,因為在構造LocalStorage已經給“PropA”設定47
  @LocalStorageLink('PropA') storLink: number = 1;

  build() {
    Column() {
      Text(`incr @LocalStorageLink variable`)
        // 點選“incr @LocalStorageLink variable”,this.storLink加1,改變同步回storage,全域性變數linkToPropA也會同步改變 

        .onClick(() => this.storLink += 1)
      // 並不建議在元件內使用全域性變數linkToPropA.get(),因為可能會有生命週期不同引起的錯誤。
      Text(`@LocalStorageLink: ${this.storLink} - linkToPropA: ${linkToPropA.get()}`)
    }
  }
}

5、兄弟節點之間同步狀態變數

let storage = new LocalStorage({ countStorage: 1 });

@Component
struct Child {
  // 子元件例項的名字
  label: string = 'no name';
  // 和LocalStorage中“countStorage”的雙向繫結資料
  @LocalStorageLink('countStorage') playCountLink: number = 0;

  build() {
    Row() {
      Text(this.label)
        .width(50).height(60).fontSize(12)
      Text(`playCountLink ${this.playCountLink}: inc by 1`)
        .onClick(() => {
          this.playCountLink += 1;
        })
        .width(200).height(60).fontSize(12)
    }.width(300).height(60)
  }
}
@Entry(storage)
@Component
struct Parent {
  @LocalStorageLink('countStorage') playCount: number = 0;
  build() {
    Column() {
      Row() {
        Text('Parent')
          .width(50).height(60).fontSize(12)
        Text(`playCount ${this.playCount} dec by 1`)
          .onClick(() => {
            this.playCount -= 1;
          })
          .width(250).height(60).fontSize(12)
      }.width(300).height(60)
      Row() {
        Text('LocalStorage')
          .width(50).height(60).fontSize(12)
        Text(`countStorage ${this.playCount} incr by 1`)
          .onClick(() => {
            storage.set<number>('countStorage', 1 + storage.get<number>('countStorage'));
          })
          .width(250).height(60).fontSize(12)
      }.width(300).height(60)
      Child({ label: 'ChildA' })
      Child({ label: 'ChildB' })
      Text(`playCount in LocalStorage for debug ${storage.get<number>('countStorage')}`)
        .width(300).height(60).fontSize(12)
    }
  }
}

6、將LocalStorage例項從UIAbility共享到一個或多個檢視

// EntryAbility.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
let para:Record<string,number> = { 'PropA': 47 };
let localStorage: LocalStorage = new LocalStorage(para);
export default class EntryAbility extends UIAbility {
  storage: LocalStorage = localStorage

  onWindowStageCreate(windowStage: window.WindowStage) {
    windowStage.loadContent('pages/Index', this.storage);
  }
}
// 透過GetShared介面獲取stage共享的LocalStorage例項
let storage = LocalStorage.GetShared()

@Entry(storage)
@Component
struct CompA {
  // can access LocalStorage instance using 
  // @LocalStorageLink/Prop decorated variables
  @LocalStorageLink('PropA') varA: number = 1;

  build() {
    Column() {
      Text(`${this.varA}`).fontSize(50)
    }
  }
}
🌈1.2.2.2 AppStorage:AppStorage
🍬1.2.2.2.1 變化規則

和前面一樣傳遞的引數變成@StorageProp和@StorageLink

  • 當裝飾的資料型別為boolean、string、number型別時,可以觀察到數值的變化。
  • 當裝飾的資料型別為class或者Object時,可以觀察到賦值和屬性賦值的變化,即Object.keys(observedObject)返回的所有屬性。
  • 當裝飾的物件是array時,可以觀察到陣列新增、刪除、更新陣列單元的變化。
🍬1.2.2.2.2 使用場景

1、從應用邏輯使用AppStorage和LocalStorage

AppStorage.SetOrCreate('PropA', 47);

let storage: LocalStorage = new LocalStorage({ 'PropA': 17 });
let propA: number = AppStorage.Get('PropA') // propA in AppStorage == 47, propA in LocalStorage == 17
var link1: SubscribedAbstractProperty<number> = AppStorage.Link('PropA'); // link1.get() == 47
var link2: SubscribedAbstractProperty<number> = AppStorage.Link('PropA'); // link2.get() == 47
var prop: SubscribedAbstractProperty<number> = AppStorage.Prop('PropA'); // prop.get() == 47

link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48
link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49

storage.get('PropA') // == 17 
storage.set('PropA', 101);
storage.get('PropA') // == 101

AppStorage.Get('PropA') // == 49
link1.get() // == 49
link2.get() // == 49
prop.get() // == 49

2、從UI內部使用AppStorage和LocalStorage

AppStorage.SetOrCreate('PropA', 47);
let storage = new LocalStorage({ 'PropA': 48 });

@Entry(storage)
@Component
struct CompA {
  @StorageLink('PropA') storLink: number = 1;
  @LocalStorageLink('PropA') localStorLink: number = 1;

  build() {
    Column({ space: 20 }) {
      Text(`From AppStorage ${this.storLink}`)
        .onClick(() => this.storLink += 1)
      Text(`From LocalStorage ${this.localStorLink}`)
        .onClick(() => this.localStorLink += 1)
    }
  }
}

3、不建議藉助@StorageLink的雙向同步機制實現事件通知

// xxx.ets
class ViewData {
  title: string;
  uri: Resource;
  color: Color = Color.Black;

  constructor(title: string, uri: Resource) {
    this.title = title;
    this.uri = uri
  }
}

@Entry
@Component
struct Gallery2 {
  dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
  scroller: Scroller = new Scroller()

  build() {
    Column() {
      Grid(this.scroller) {
        ForEach(this.dataList, (item: ViewData, index?: number) => {
          GridItem() {
            TapImage({
              uri: item.uri,
              index: index
            })
          }.aspectRatio(1)
        }, (item: ViewData, index?: number) => {
          return JSON.stringify(item) + index;
        })
      }.columnsTemplate('1fr 1fr')
    }
  }
}
@Component
export struct TapImage {
  @StorageLink('tapIndex') @Watch('onTapIndexChange') tapIndex: number = -1;
  @State tapColor: Color = Color.Black;
  private index: number = 0;
  private uri: Resource = {
    id: 0,
    type: 0,
    moduleName: "",
    bundleName: ""
  };
  // 判斷是否被選中
  onTapIndexChange() {
    if (this.tapIndex >= 0 && this.index === this.tapIndex) {
      console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, red`)
      this.tapColor = Color.Red;
    } else {
      console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, black`)
      this.tapColor = Color.Black;
    }
  }
  build() {
    Column() {
      Image(this.uri)
        .objectFit(ImageFit.Cover)
        .onClick(() => {
          this.tapIndex = this.index;
        })
        .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
    }
  }
}
// xxx.ets
import emitter from '@ohos.events.emitter';

let NextID: number = 0;

class ViewData {
  title: string;
  uri: Resource;
  color: Color = Color.Black;
  id: number;

  constructor(title: string, uri: Resource) {
    this.title = title;
    this.uri = uri
    this.id = NextID++;
  }
}

@Entry
@Component
struct Gallery2 {
  dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
  scroller: Scroller = new Scroller()
  private preIndex: number = -1

  build() {
    Column() {
      Grid(this.scroller) {
        ForEach(this.dataList, (item: ViewData) => {
          GridItem() {
            TapImage({
              uri: item.uri,
              index: item.id
            })
          }.aspectRatio(1)
          .onClick(() => {
            if (this.preIndex === item.id) {
              return
            }
            let innerEvent: emitter.InnerEvent = { eventId: item.id }
            // 選中態:黑變紅
            let eventData: emitter.EventData = {
              data: {
                "colorTag": 1
              }
            }
            emitter.emit(innerEvent, eventData)
            if (this.preIndex != -1) {
              console.info(`preIndex: ${this.preIndex}, index: ${item.id}, black`)
              let innerEvent: emitter.InnerEvent = { eventId: this.preIndex }
              // 取消選中態:紅變黑
              let eventData: emitter.EventData = {
                data: {
                  "colorTag": 0
                }
              }
              emitter.emit(innerEvent, eventData)
            }
            this.preIndex = item.id
          })
        }, (item: ViewData) => JSON.stringify(item))
      }.columnsTemplate('1fr 1fr')
    }
  }
}
@Component
export struct TapImage {
  @State tapColor: Color = Color.Black;
  private index: number = 0;
  private uri: Resource = {
    id: 0,
    type: 0,
    moduleName: "",
    bundleName: ""
  };
  onTapIndexChange(colorTag: emitter.EventData) {
    if (colorTag.data != null) {
      this.tapColor = colorTag.data.colorTag ? Color.Red : Color.Black
    }
  }
  aboutToAppear() {
    //定義事件ID
    let innerEvent: emitter.InnerEvent = { eventId: this.index }
    emitter.on(innerEvent, data => {
      this.onTapIndexChange(data)
    })
  }
  build() {
    Column() {
      Image(this.uri)
        .objectFit(ImageFit.Cover)
        .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
    }
  }
}

以上通知事件邏輯簡化成三元表示式

// xxx.ets
class ViewData {
  title: string;
  uri: Resource;
  color: Color = Color.Black;

  constructor(title: string, uri: Resource) {
    this.title = title;
    this.uri = uri
  }
}

@Entry
@Component
struct Gallery2 {
  dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
  scroller: Scroller = new Scroller()

  build() {
    Column() {
      Grid(this.scroller) {
        ForEach(this.dataList, (item: ViewData, index?: number) => {
          GridItem() {
            TapImage({
              uri: item.uri,
              index: index
            })
          }.aspectRatio(1)
        }, (item: ViewData, index?: number) => {
          return JSON.stringify(item) + index;
        })
      }.columnsTemplate('1fr 1fr')
    }
  }
}
@Component
export struct TapImage {
  @StorageLink('tapIndex') tapIndex: number = -1;
  @State tapColor: Color = Color.Black;
  private index: number = 0;
  private uri: Resource = {
    id: 0,
    type: 0,
    moduleName: "",
    bundleName: ""
  };
  build() {
    Column() {
      Image(this.uri)
        .objectFit(ImageFit.Cover)
        .onClick(() => {
          this.tapIndex = this.index;
        })
        .border({
          width: 5,
          style: BorderStyle.Dotted,
          color: (this.tapIndex >= 0 && this.index === this.tapIndex) ? Color.Red : Color.Black
        })
    }
  }
}

AppStorage與PersistentStorage以及Environment配合使用時,需要注意以下幾點:

  • 在AppStorage中建立屬性後,呼叫PersistentStorage.persistProp()介面時,會使用在AppStorage中已經存在的值,並覆蓋PersistentStorage中的同名屬性,所以建議要使用相反的呼叫順序,反例可見在PersistentStorage之前訪問AppStorage中的屬性;

  • 如果在AppStorage中已經建立屬性後,再呼叫Environment.envProp()建立同名的屬性,會呼叫失敗。因為AppStorage已經有同名屬性,Environment環境變數不會再寫入AppStorage中,所以建議AppStorage中屬性不要使用Environment預置環境變數名。

  • 狀態裝飾器裝飾的變數,改變會引起UI的渲染更新,如果改變的變數不是用於UI更新,只是用於訊息傳遞,推薦使用 emitter方式。例子可見不建議藉助@StorageLink的雙向同步機制實現事件通知。

🌈1.2.2.3 PersistentStorage:持久化儲存UI狀態

image

🍬1.2.2.3.1 變化規則

類似AppStorage,流程圖如下:

image

🍬1.2.2.3.2 使用場景

PersistentStorage.PersistProp('aProp', 47);

@Entry
@Component
struct Index {
  @State message: string = 'Hello World'
  @StorageLink('aProp') aProp: number = 48

  build() {
    Row() {
      Column() {
        Text(this.message)
        // 應用退出時會儲存當前結果。重新啟動後,會顯示上一次的儲存結果
        Text(`${this.aProp}`)
          .onClick(() => {
            this.aProp += 1;
          })
      }
    }
  }
}
🌈1.2.2.4 Environment:裝置環境查詢

Environment是ArkUI框架在應用程式啟動時建立的單例物件。它為AppStorage提供了一系列描述應用程式執行狀態的屬性。Environment的所有屬性都是不可變的(即應用不可寫入),所有的屬性都是簡單型別。

🍬1.2.2.4.1 變化規則

不可讀寫

🍬1.2.2.4.2 使用場景

1、從UI中訪問Environment引數

// 將裝置languageCode存入AppStorage中
Environment.EnvProp('languageCode', 'en');
let enable = AppStorage.Get('languageCode');

@Entry
@Component
struct Index {
  @StorageProp('languageCode') languageCode: string = 'en';

  build() {
    Row() {
      Column() {
        // 輸出當前裝置的languageCode
        Text(this.languageCode)
      }
    }
  }
}

2、應用邏輯使用Environment

// 使用Environment.EnvProp將裝置執行languageCode存入AppStorage中;
Environment.EnvProp('languageCode', 'en');
// 從AppStorage獲取單向繫結的languageCode的變數
const lang: SubscribedAbstractProperty<string> = AppStorage.Prop('languageCode');

if (lang.get() === 'zh') {
  console.info('你好');
} else {
  console.info('Hello!');
}
☀️1.2.3 其他狀態管理功能
  • @Watch:用於監聽狀態變數的變化。

運算子:給內建元件提供TS變數的引用,使得TS變數和內建元件的內部狀態保持同步。

🌈1.2.2.1 使用場景

1、@Watch和自定義元件更新

clike @Component struct TotalView { @Prop @Watch('onCountUpdated') count: number; 
@State total: number = 0; // @Watch cb 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 }) } } } ``` 

2、@Watch與@Link組合使用

clike class PurchaseItem { static NextId: number = 0; public id: number; public price: number; constructor(price: number) { this.id = PurchaseItem.NextId++; this.price = price; } }
@Component struct BasketViewer { @Link @Watch('onBasketUpdated') shopBasket: PurchaseItem[];
@State totalPurchase: number = 0; updateTotal(): number { let total = this.shopBasket.reduce((sum, i) => sum + i.price, 0); // 超過100歐元可享受折扣 if (total >= 100) { total = 0.9 * total; } return total; } // @Watch 回撥 onBasketUpdated(propName: string): void { this.totalPurchase = this.updateTotal(); } build() { Column() { ForEach(this.shopBasket, (item) => { Text(`Price: ${item.price.toFixed(2)} €`) }, item => item.id.toString() ) Text(`Total: ${this.totalPurchase.toFixed(2)} €`) } } } @Entry @Component struct BasketModifier { @State shopBasket: PurchaseItem[] = []; build() { Column() { Button('Add to basket') .onClick(() => { this.shopBasket.push(new PurchaseItem(Math.round(100 * Math.random()))) }) BasketViewer({ shopBasket: $shopBasket }) } } }
🌈1.2.2.2 $$語法:內建元件雙向同步
clike // xxx.ets @Entry @Component struct RefreshExample { @State isRefreshing: boolean = false
@State counter: number = 0 build() { Column() { Text('Pull Down and isRefreshing: ' + this.isRefreshing) .fontSize(30) .margin(10) Refresh({ refreshing: $$this.isRefreshing, offset: 120, friction: 100 }) { Text('Pull Down and refresh: ' + this.counter) .fontSize(30) .margin(10) } .onStateChange((refreshStatus: RefreshStatus) => { console.info('Refresh onStatueChange state is ' + refreshStatus) }) } } } 

🚀寫在最後

  • 如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:
  • 點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。
  • 關注小編,同時可以期待後續文章ing🚀,不定期分享原創知識。
  • 想要獲取更多完整鴻蒙最新VIP學習資源,請移步前往小編:Gitee → https://gitee.com/sudaosan/harmony-os-learning-materials

image

相關文章