🚀前言
狀態管理是指在應用程式中維護和更新應用程式狀態的過程。在一個程式中,可能有很多不同的元件和模組,它們需要共享和相互作用的狀態。如果沒有一個明確的方式來管理這些狀態,就會導致程式碼混亂、不易維護和難以擴充套件。
狀態管理的目標是提供一種機制,使得所有的元件和模組都可以訪問和更新同一個狀態。這個狀態通常是儲存在一箇中央儲存區域中,被稱為狀態儲存或狀態容器。狀態管理通常與應用程式的響應式設計緊密相連,以便在狀態改變時自動更新應用程式的介面。
🚀一、ArkTS語言狀態管理
🔎1.概述
在宣告式UI程式設計框架中,應用程式的UI是由程式狀態驅動的。使用者構建一個UI模型,其中應用的執行時狀態作為引數傳遞進去。當引數改變時,UI會根據新的引數重新渲染。這個執行時狀態的變化是由狀態管理機制來處理的,它會監控狀態的變化,並自動更新UI的渲染。
在ArkUI中,自定義元件的變數必須被裝飾器裝飾為狀態變數,這樣它們的改變才能引起UI的重新渲染。如果不使用狀態變數,UI只能在初始化時渲染,後續將不會再重新整理。狀態變數和UI之間的關係如下圖所示:
-
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提供了多種裝飾器主要分為:管理元件擁有的狀態、管理應用擁有的狀態、其他狀態管理功能,主要圖形如下:
☀️1.2.1 管理元件擁有的狀態
🌈1.2.1.1 @State 元件內狀態
@State變數裝飾器只支援Object、class、string、number、boolean、enum型別,以及這些型別的陣列。不支援複雜型別(比如Date型別)
父子元件初始化和傳遞裝飾圖如下:
🍬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 父子單向同步
@Prop變數裝飾器只支援string、number、boolean、enum型別,以及這些型別的陣列。不支援複雜型別(比如any型別)
父子元件初始化和傳遞裝飾圖如下:
🍬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型別)
父子元件初始化和傳遞裝飾圖如下:
🍬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 與後代元件雙向同步
@Prop變數裝飾器只支援string、number、boolean、enum型別,以及這些型別的陣列。不支援複雜型別(比如any型別)
父子元件初始化和傳遞裝飾圖如下:
🍬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()
}
}
}
🌈1.2.1.5 @Observed/@ObjectLink 巢狀類物件屬性變化
型別必須是@Observed裝飾的class,可用於初始化常規變數、@State、@Link、@Prop、@Provide
巢狀類物件裝飾圖如下:
🍬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 管理應用擁有的狀態
🌈1.2.2.1 LocalStorage:頁面級UI狀態儲存
🍬1.2.2.1.1 變化規則
-
當@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狀態
🍬1.2.2.3.1 變化規則
類似AppStorage,流程圖如下:
🍬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