應(yīng)用通過狀態(tài)去渲染更新UI是程序設(shè)計中相對復(fù)雜电湘,但又十分重要的每庆,往往決定了應(yīng)用程序的性能筐带。程序的狀態(tài)數(shù)據(jù)通常包含了數(shù)組、對象缤灵,或者是嵌套對象組合而成伦籍。在這些情況下,ArkUI采取MVVM = Model + View + ViewModel模式腮出,其中狀態(tài)管理模塊起到的就是ViewModel的作用帖鸦,將數(shù)據(jù)與視圖綁定在一起,更新數(shù)據(jù)的時候直接更新視圖胚嘲。
Model層:存儲數(shù)據(jù)和相關(guān)邏輯的模型作儿。它表示組件或其他相關(guān)業(yè)務(wù)邏輯之間傳輸?shù)臄?shù)據(jù)。Model是對原始數(shù)據(jù)的進一步處理馋劈。
View層:在ArkUI中通常是@Components修飾組件渲染的UI攻锰。
-
ViewModel層:在ArkUI中,ViewModel是存儲在自定義組件的狀態(tài)變量妓雾、LocalStorage和AppStorage中的數(shù)據(jù)口注。
- 自定義組件通過執(zhí)行其build()方法或者@Builder裝飾的方法來渲染UI,即ViewModel可以渲染View君珠。
- View可以通過相應(yīng)event handler來改變ViewModel,即事件驅(qū)動ViewModel的改變娇斑,另外ViewModel提供了@Watch回調(diào)方法用于監(jiān)聽狀態(tài)數(shù)據(jù)的改變策添。
- 在ViewModel被改變時,需要同步回Model層毫缆,這樣才能保證ViewModel和Model的一致性唯竹,即應(yīng)用自身數(shù)據(jù)的一致性。
- ViewModel結(jié)構(gòu)設(shè)計應(yīng)始終為了適配自定義組件的構(gòu)建和更新苦丁,這也是將Model和ViewModel分開的原因浸颓。
目前很多關(guān)于UI構(gòu)造和更新的問題,都是由于ViewModel的設(shè)計并沒有很好的支持自定義組件的渲染旺拉,或者試圖去讓自定義組件強行適配Model層产上,而中間沒有用ViewModel來進行分離。例如蛾狗,一個應(yīng)用程序直接將SQL數(shù)據(jù)庫中的數(shù)據(jù)讀入內(nèi)存晋涣,這種數(shù)據(jù)模型不能很好的直接適配自定義組件的渲染,所以在應(yīng)用程序開發(fā)中需要適配ViewModel層沉桌。
根據(jù)上面涉及SQL數(shù)據(jù)庫的示例谢鹊,應(yīng)用程序應(yīng)設(shè)計為:
Model:針對數(shù)據(jù)庫高效操作的數(shù)據(jù)模型算吩。
ViewModel:針對ArkUI狀態(tài)管理功能進行高效的UI更新的視圖模型。
-
部署 converters/adapters: converters/adapters作用于Model和ViewModel的相互轉(zhuǎn)換佃扼。
- converters/adapters可以轉(zhuǎn)換最初從數(shù)據(jù)庫讀取的Model偎巢,來創(chuàng)建并初始化ViewModel。
- 在應(yīng)用的使用場景中兼耀,UI會通過event handler改變ViewModel压昼,此時converters/adapters需要將ViewModel的更新數(shù)據(jù)同步回Model。
雖然與強制將UI擬合到SQL數(shù)據(jù)庫模式(MV模式)相比翠订,MVVM的設(shè)計比較復(fù)雜巢音,但應(yīng)用程序開發(fā)人員可以通過ViewModel層的隔離,來簡化UI的設(shè)計和實現(xiàn)尽超,以此來收獲更好的UI性能官撼。
ViewModel的數(shù)據(jù)源
ViewModel通常包含多個頂層數(shù)據(jù)源。@State和@Provide裝飾的變量以及LocalStorage和AppStorage都是頂層數(shù)據(jù)源似谁,其余裝飾器都是與數(shù)據(jù)源做同步的數(shù)據(jù)傲绣。裝飾器的選擇取決于狀態(tài)需要在自定義組件之間的共享范圍。共享范圍從小到大的排序是:
@State:組件級別的共享巩踏,通過命名參數(shù)機制傳遞秃诵,例如:CompA: ({ aProp: this.aProp }),表示傳遞層級(共享范圍)是父子之間的傳遞塞琼。
@Provide:組件級別的共享菠净,可以通過key和@Consume綁定,因此不用參數(shù)傳遞彪杉,實現(xiàn)多層級的數(shù)據(jù)共享毅往,共享范圍大于@State。
LocalStorage:頁面級別的共享派近,可以通過@Entry在當前組件樹上共享LocalStorage實例攀唯。
AppStorage:應(yīng)用全局的UI狀態(tài)存儲,和應(yīng)用進程綁定渴丸,在整個應(yīng)用內(nèi)的狀態(tài)數(shù)據(jù)的共享侯嘀。
@State裝飾的變量與一個或多個子組件共享狀態(tài)數(shù)據(jù)
@State可以初始化多種狀態(tài)變量,@Prop谱轨、@Link和@ObjectLink可以和其建立單向或雙向同步戒幔。
-
使用Parent根節(jié)點中@State裝飾的testNum作為ViewModel數(shù)據(jù)項。將testNum傳遞給其子組件LinkChild和Sibling碟嘴。
// xxx.ets @Entry @Component struct Parent { @State @Watch("testNumChange1") testNum: number = 1; testNumChange1(propName: string): void { console.log(`Parent: testNumChange value ${this.testNum}`) } build() { Column() { LinkChild({ testNum: $testNum }) Sibling({ testNum: $testNum }) } } }
-
LinkChild和Sibling中用@Link和父組件的數(shù)據(jù)源建立雙向同步溪食。其中LinkChild中創(chuàng)建了LinkLinkChild和PropLinkChild。
@Component struct Sibling { @Link @Watch("testNumChange") testNum: number; testNumChange(propName: string): void { console.log(`Sibling: testNumChange value ${this.testNum}`); } build() { Text(`Sibling: ${this.testNum}`) } } @Component struct LinkChild { @Link @Watch("testNumChange") testNum: number; testNumChange(propName: string): void { console.log(`LinkChild: testNumChange value ${this.testNum}`); } build() { Column() { Button('incr testNum') .onClick(() => { console.log(`LinkChild: before value change value ${this.testNum}`); this.testNum = this.testNum + 1 console.log(`LinkChild: after value change value ${this.testNum}`); }) Text(`LinkChild: ${this.testNum}`) LinkLinkChild({ testNumGrand: $testNum }) PropLinkChild({ testNumGrand: this.testNum }) } .height(200).width(200) } }
-
LinkLinkChild和PropLinkChild聲明如下娜扇,PropLinkChild中的@Prop和其父組件建立單向同步關(guān)系错沃。
@Component struct LinkLinkChild { @Link @Watch("testNumChange") testNumGrand: number; testNumChange(propName: string): void { console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`); } build() { Text(`LinkLinkChild: ${this.testNumGrand}`) } } @Component struct PropLinkChild { @Prop @Watch("testNumChange") testNumGrand: number = 0; testNumChange(propName: string): void { console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); } build() { Text(`PropLinkChild: ${this.testNumGrand}`) .height(70) .backgroundColor(Color.Red) .onClick(() => { this.testNumGrand += 1; }) } }
當LinkChild中的@Link testNum更改時栅组。
更改首先同步到其父組件Parent,然后更改從Parent同步到Sibling枢析。
LinkChild中的@Link testNum更改也同步給子組件LinkLinkChild和PropLinkChild玉掸。
@State裝飾器與@Provide、LocalStorage醒叁、AppStorage的區(qū)別:
- @State如果想要將更改傳遞給孫子節(jié)點司浪,需要先將更改傳遞給子組件,再從子節(jié)點傳遞給孫子節(jié)點把沼。
- 共享只能通過構(gòu)造函數(shù)的參數(shù)傳遞啊易,即命名參數(shù)機制CompA: ({ aProp: this.aProp })。
完整的代碼示例如下:
@Component
struct LinkLinkChild {
@Link @Watch("testNumChange") testNumGrand: number;
testNumChange(propName: string): void {
console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`LinkLinkChild: ${this.testNumGrand}`)
}
}
@Component
struct PropLinkChild {
@Prop @Watch("testNumChange") testNumGrand: number = 0;
testNumChange(propName: string): void {
console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`PropLinkChild: ${this.testNumGrand}`)
.height(70)
.backgroundColor(Color.Red)
.onClick(() => {
this.testNumGrand += 1;
})
}
}
@Component
struct Sibling {
@Link @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`Sibling: testNumChange value ${this.testNum}`);
}
build() {
Text(`Sibling: ${this.testNum}`)
}
}
@Component
struct LinkChild {
@Link @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`LinkChild: testNumChange value ${this.testNum}`);
}
build() {
Column() {
Button('incr testNum')
.onClick(() => {
console.log(`LinkChild: before value change value ${this.testNum}`);
this.testNum = this.testNum + 1
console.log(`LinkChild: after value change value ${this.testNum}`);
})
Text(`LinkChild: ${this.testNum}`)
LinkLinkChild({ testNumGrand: $testNum })
PropLinkChild({ testNumGrand: this.testNum })
}
.height(200).width(200)
}
}
@Entry
@Component
struct Parent {
@State @Watch("testNumChange1") testNum: number = 1;
testNumChange1(propName: string): void {
console.log(`Parent: testNumChange value ${this.testNum}`)
}
build() {
Column() {
LinkChild({ testNum: $testNum })
Sibling({ testNum: $testNum })
}
}
}
@Provide裝飾的變量與任何后代組件共享狀態(tài)數(shù)據(jù)
@Provide裝飾的變量可以與任何后代組件共享狀態(tài)數(shù)據(jù)饮睬,其后代組件使用@Consume創(chuàng)建雙向同步租谈,詳情見@Provide和@Consume。
因此捆愁,@Provide-@Consume模式比使用@State-@Link-@Link從父組件將更改傳遞到孫子組件更方便割去。@Provide-@Consume適合在單個頁面UI組件樹中共享狀態(tài)數(shù)據(jù)。
使用@Provide-@Consume模式時昼丑,@Consume和其祖先組件中的@Provide通過綁定相同的key連接呻逆,而不是在組件的構(gòu)造函數(shù)中通過參數(shù)來進行傳遞。
以下示例通過@Provide-@Consume模式菩帝,將更改從父組件傳遞到孫子組件咖城。
@Component
struct LinkLinkChild {
@Consume @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`LinkLinkChild: testNum value ${this.testNum}`);
}
build() {
Text(`LinkLinkChild: ${this.testNum}`)
}
}
@Component
struct PropLinkChild {
@Prop @Watch("testNumChange") testNumGrand: number = 0;
testNumChange(propName: string): void {
console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`PropLinkChild: ${this.testNumGrand}`)
.height(70)
.backgroundColor(Color.Red)
.onClick(() => {
this.testNumGrand += 1;
})
}
}
@Component
struct Sibling {
@Consume @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`Sibling: testNumChange value ${this.testNum}`);
}
build() {
Text(`Sibling: ${this.testNum}`)
}
}
@Component
struct LinkChild {
@Consume @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`LinkChild: testNumChange value ${this.testNum}`);
}
build() {
Column() {
Button('incr testNum')
.onClick(() => {
console.log(`LinkChild: before value change value ${this.testNum}`);
this.testNum = this.testNum + 1
console.log(`LinkChild: after value change value ${this.testNum}`);
})
Text(`LinkChild: ${this.testNum}`)
LinkLinkChild({ /* empty */ })
PropLinkChild({ testNumGrand: this.testNum })
}
.height(200).width(200)
}
}
@Entry
@Component
struct Parent {
@Provide @Watch("testNumChange1") testNum: number = 1;
testNumChange1(propName: string): void {
console.log(`Parent: testNumChange value ${this.testNum}`)
}
build() {
Column() {
LinkChild({ /* empty */ })
Sibling({ /* empty */ })
}
}
}
給LocalStorage實例中對應(yīng)的屬性建立雙向或單向同步
通過@LocalStorageLink和@LocalStorageProp,給LocalStorage實例中的屬性建立雙向或單向同步呼奢【品保可以將LocalStorage實例視為@State變量的Map。
LocalStorage對象可以在ArkUI應(yīng)用程序的幾個頁面上共享控妻。因此,使用@LocalStorageLink揭绑、@LocalStorageProp和LocalStorage可以在應(yīng)用程序的多個頁面上共享狀態(tài)弓候。
以下示例中:
創(chuàng)建一個LocalStorage實例,并通過@Entry(storage)將其注入根節(jié)點他匪。
在Parent組件中初始化@LocalStorageLink("testNum")變量時菇存,將在LocalStorage實例中創(chuàng)建testNum屬性,并設(shè)置指定的初始值為1邦蜜,即@LocalStorageLink("testNum") testNum: number = 1依鸥。
在其子組件中,都使用@LocalStorageLink或@LocalStorageProp綁定同一個屬性名key來傳遞數(shù)據(jù)悼沈。
LocalStorage可以被認為是@State變量的Map贱迟,屬性名作為Map中的key姐扮。
@LocalStorageLink和LocalStorage中對應(yīng)的屬性的同步行為,和@State和@Link一致衣吠,都為雙向數(shù)據(jù)同步茶敏。
以下為組件的狀態(tài)更新圖:
@Component
struct LinkLinkChild {
@LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`LinkLinkChild: testNum value ${this.testNum}`);
}
build() {
Text(`LinkLinkChild: ${this.testNum}`)
}
}
@Component
struct PropLinkChild {
@LocalStorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1;
testNumChange(propName: string): void {
console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`PropLinkChild: ${this.testNumGrand}`)
.height(70)
.backgroundColor(Color.Red)
.onClick(() => {
this.testNumGrand += 1;
})
}
}
@Component
struct Sibling {
@LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`Sibling: testNumChange value ${this.testNum}`);
}
build() {
Text(`Sibling: ${this.testNum}`)
}
}
@Component
struct LinkChild {
@LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`LinkChild: testNumChange value ${this.testNum}`);
}
build() {
Column() {
Button('incr testNum')
.onClick(() => {
console.log(`LinkChild: before value change value ${this.testNum}`);
this.testNum = this.testNum + 1
console.log(`LinkChild: after value change value ${this.testNum}`);
})
Text(`LinkChild: ${this.testNum}`)
LinkLinkChild({ /* empty */ })
PropLinkChild({ /* empty */ })
}
.height(200).width(200)
}
}
// create LocalStorage object to hold the data
const storage = new LocalStorage();
@Entry(storage)
@Component
struct Parent {
@LocalStorageLink("testNum") @Watch("testNumChange1") testNum: number = 1;
testNumChange1(propName: string): void {
console.log(`Parent: testNumChange value ${this.testNum}`)
}
build() {
Column() {
LinkChild({ /* empty */ })
Sibling({ /* empty */ })
}
}
}
給AppStorage中對應(yīng)的屬性建立雙向或單向同步
AppStorage是LocalStorage的單例對象,ArkUI在應(yīng)用程序啟動時創(chuàng)建該對象缚俏,在頁面中使用@StorageLink和@StorageProp為多個頁面之間共享數(shù)據(jù)惊搏,具體使用方法和LocalStorage類似。
也可以使用PersistentStorage將AppStorage中的特定屬性持久化到本地磁盤的文件中忧换,再次啟動的時候@StorageLink和@StorageProp會恢復(fù)上次應(yīng)用退出的數(shù)據(jù)恬惯。
示例如下:
@Component
struct LinkLinkChild {
@StorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`LinkLinkChild: testNum value ${this.testNum}`);
}
build() {
Text(`LinkLinkChild: ${this.testNum}`)
}
}
@Component
struct PropLinkChild {
@StorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1;
testNumChange(propName: string): void {
console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
}
build() {
Text(`PropLinkChild: ${this.testNumGrand}`)
.height(70)
.backgroundColor(Color.Red)
.onClick(() => {
this.testNumGrand += 1;
})
}
}
@Component
struct Sibling {
@StorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`Sibling: testNumChange value ${this.testNum}`);
}
build() {
Text(`Sibling: ${this.testNum}`)
}
}
@Component
struct LinkChild {
@StorageLink("testNum") @Watch("testNumChange") testNum: number = 1;
testNumChange(propName: string): void {
console.log(`LinkChild: testNumChange value ${this.testNum}`);
}
build() {
Column() {
Button('incr testNum')
.onClick(() => {
console.log(`LinkChild: before value change value ${this.testNum}`);
this.testNum = this.testNum + 1
console.log(`LinkChild: after value change value ${this.testNum}`);
})
Text(`LinkChild: ${this.testNum}`)
LinkLinkChild({ /* empty */
})
PropLinkChild({ /* empty */
})
}
.height(200).width(200)
}
}
@Entry
@Component
struct Parent {
@StorageLink("testNum") @Watch("testNumChange1") testNum: number = 1;
testNumChange1(propName: string): void {
console.log(`Parent: testNumChange value ${this.testNum}`)
}
build() {
Column() {
LinkChild({ /* empty */
})
Sibling({ /* empty */
})
}
}
}
ViewModel的嵌套場景
大多數(shù)情況下,ViewModel數(shù)據(jù)項都是復(fù)雜類型的亚茬,例如酪耳,對象數(shù)組、嵌套對象或者這些類型的組合才写。對于嵌套場景葡兑,可以使用@Observed搭配@Prop或者@ObjectLink來觀察變化。
@Prop和@ObjectLink嵌套數(shù)據(jù)結(jié)構(gòu)
推薦設(shè)計單獨的@Component來渲染每一個數(shù)組或?qū)ο笤薏荨4藭r讹堤,對象數(shù)組或嵌套對象(屬性是對象的對象稱為嵌套對象)需要兩個@Component,一個@Component呈現(xiàn)外部數(shù)組/對象厨疙,另一個@Component呈現(xiàn)嵌套在數(shù)組/對象內(nèi)的類對象洲守。 @Prop、@Link沾凄、@ObjectLink修飾的變量只能觀察到第一層的變化梗醇。
-
對于類:
- 可以觀察到賦值的變化:this.obj=new ClassObj(...)
- 可以觀察到對象屬性的更改:this.obj.a=new ClassA(...)
- 不能觀察更深層級的屬性更改:this.obj.a.b = 47
-
對于數(shù)組:
- 可以觀察到數(shù)組的整體賦值:this.arr=[...]
- 可以觀察到數(shù)據(jù)項的刪除、插入和替換:this.arr[1] = new ClassA()撒蟀、this.arr.pop()叙谨、 this.arr.push(new ClassA(...))、this.arr.sort(...)
- 不能觀察更深層級的數(shù)組變化:this.arr[1].b = 47
如果要觀察嵌套類的內(nèi)部對象的變化保屯,可以使用@ObjectLink或@Prop手负。優(yōu)先考慮@ObjectLink,其通過嵌套對象內(nèi)部屬性的引用初始化自身姑尺。@Prop會對嵌套在內(nèi)部的對象的深度拷貝來進行初始化竟终,以實現(xiàn)單向同步。在性能上@Prop的深度拷貝比@ObjectLink的引用拷貝慢很多切蟋。
@ObjectLink或@Prop可以用來存儲嵌套內(nèi)部的類對象统捶,該類必須用@Observed類裝飾器裝飾,否則類的屬性改變并不會觸發(fā)更新,UI并不會刷新喘鸟。@Observed為其裝飾的類實現(xiàn)自定義構(gòu)造函數(shù)匆绣,此構(gòu)造函數(shù)創(chuàng)建了一個類的實例,并使用ES6代理包裝(由ArkUI框架實現(xiàn))迷守,攔截修飾class屬性的所有“get”和“set”犬绒。“set”觀察屬性值兑凿,當發(fā)生賦值操作時凯力,通知ArkUI框架更新±窕“get”收集哪些UI組件依賴該狀態(tài)變量咐鹤,實現(xiàn)最小化UI更新。
如果嵌套場景中圣絮,嵌套數(shù)據(jù)內(nèi)部是數(shù)組或者class時祈惶,需根據(jù)以下場景使用@Observed類裝飾器。
如果嵌套數(shù)據(jù)內(nèi)部是class扮匠,直接被@Observed裝飾捧请。
-
如果嵌套數(shù)據(jù)內(nèi)部是數(shù)組,可以通過以下方式來觀察數(shù)組變化棒搜。
@Observed class ObservedArray<T> extends Array<T> { constructor(args: T[]) { if (args instanceof Array) { super(...args); } else { super(args) } } /* otherwise empty */ }
ViewModel為外層class疹蛉。
class Outer { innerArrayProp : ObservedArray<string> = []; ... }
嵌套數(shù)據(jù)結(jié)構(gòu)中@Prop和@ObjectLink之的區(qū)別
以下示例中:
父組件ViewB渲染@State arrA:Array<ClassA>。@State可以觀察新數(shù)組的分配力麸、數(shù)組項插入可款、刪除和替換。
子組件ViewA渲染每一個ClassA的對象克蚂。
-
類裝飾器@Observed ClassA與@ObjectLink a: ClassA闺鲸。
可以觀察嵌套在Array內(nèi)的ClassA對象的變化。
-
不使用@Observed時:
ViewB中的this.arrA[Math.floor(this.arrA.length/2)].c=10將不會被觀察到埃叭,相應(yīng)的ViewA組件也不會更新摸恍。對于數(shù)組中的第一個和第二個數(shù)組項,每個數(shù)組項都初始化了兩個ViewA的對象赤屋,渲染了同一個ViewA實例误墓。在一個ViewA中的屬性賦值this.a.c += 1;時不會引發(fā)另外一個使用同一個ClassA初始化的ViewA的渲染更新。
let NextID: number = 1;
// 類裝飾器@Observed裝飾ClassA
@Observed
class ClassA {
public id: number;
public c: number;
constructor(c: number) {
this.id = NextID++;
this.c = c;
}
}
@Component
struct ViewA {
@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 {
@State arrA: ClassA[] = [new ClassA(0), new ClassA(0)];
build() {
Column() {
ForEach(this.arrA,
(item: ClassA) => {
ViewA({ label: `#${item.id}`, a: item })
},
(item: ClassA): string => { return item.id.toString(); }
)
Divider().height(10)
if (this.arrA.length) {
ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] })
ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] })
}
Divider().height(10)
Button(`ViewB: reset array`)
.onClick(() => {
// 替換整個數(shù)組益缎,會被@State this.arrA觀察到
this.arrA = [new ClassA(0), new ClassA(0)];
})
Button(`array push`)
.onClick(() => {
// 數(shù)組中插入數(shù)據(jù),會被@State this.arrA觀察到
this.arrA.push(new ClassA(0))
})
Button(`array shift`)
.onClick(() => {
// 數(shù)組中移除數(shù)據(jù)然想,會被@State this.arrA觀察到
this.arrA.shift()
})
Button(`ViewB: chg item property in middle`)
.onClick(() => {
// 替換數(shù)組中的某個元素莺奔,會被@State this.arrA觀察到
this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11);
})
Button(`ViewB: chg item property in middle`)
.onClick(() => {
// 改變數(shù)組中某個元素的屬性c,會被ViewA中的@ObjectLink觀察到
this.arrA[Math.floor(this.arrA.length / 2)].c = 10;
})
}
}
}
在ViewA中,將@ObjectLink替換為@Prop令哟。
@Component
struct ViewA {
@Prop a: ClassA = new ClassA(0);
label : string = "ViewA1";
build() {
Row() {
Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`)
.onClick(() => {
// change object property
this.a.c += 1;
})
}
}
}
與用@Prop修飾不同恼琼,用@ObjectLink修飾時,點擊數(shù)組的第一個或第二個元素屏富,后面兩個ViewA會發(fā)生同步的變化晴竞。
@Prop是單向數(shù)據(jù)同步,ViewA內(nèi)的Button只會觸發(fā)Button自身的刷新狠半,不會傳播到其他的ViewA實例中噩死。在ViewA中的ClassA只是一個副本,并不是其父組件中@State arrA : Array<ClassA>中的對象神年,也不是其他ViewA的ClassA已维,這使得數(shù)組的元素和ViewA中的元素表面是傳入的同一個對象,實際上在UI上渲染使用的是兩個互不相干的對象已日。
需要注意@Prop和@ObjectLink還有一個區(qū)別:@ObjectLink裝飾的變量是僅可讀的垛耳,不能被賦值;@Prop裝飾的變量可以被賦值飘千。
@ObjectLink實現(xiàn)雙向同步堂鲜,因為它是通過數(shù)據(jù)源的引用初始化的。
@Prop是單向同步护奈,需要深拷貝數(shù)據(jù)源缔莲。
對于@Prop賦值新的對象,就是簡單地將本地的值覆寫逆济,但是對于實現(xiàn)雙向數(shù)據(jù)同步的@ObjectLink酌予,覆寫新的對象相當于要更新數(shù)據(jù)源中的數(shù)組項或者class的屬性,這個對于 TypeScript/JavaScript是不能實現(xiàn)的奖慌。
MVVM應(yīng)用示例
以下示例深入探討了嵌套ViewModel的應(yīng)用程序設(shè)計抛虫,特別是自定義組件如何渲染一個嵌套的Object,該場景在實際的應(yīng)用開發(fā)中十分常見简僧。
開發(fā)一個電話簿應(yīng)用建椰,實現(xiàn)功能如下:
顯示聯(lián)系人和設(shè)備("Me")電話號碼 。
選中聯(lián)系人時岛马,進入可編輯態(tài)“Edit”棉姐,可以更新該聯(lián)系人詳細信息,包括電話號碼啦逆,住址伞矩。
在更新聯(lián)系人信息時,只有在單擊保存“Save Changes”之后夏志,才會保存更改乃坤。
可以點擊刪除聯(lián)系人“Delete Contact”,可以在聯(lián)系人列表刪除該聯(lián)系人。
ViewModel需要包括:
- AddressBook(class)
- me(設(shè)備): 存儲一個Person類湿诊。
- contacts(設(shè)備聯(lián)系人):存儲一個Person類數(shù)組狱杰。
AddressBook類聲明如下:
export class AddressBook {
me: Person;
contacts: ObservedArray<Person>;
constructor(me: Person, contacts: Person[]) {
this.me = me;
this.contacts = new ObservedArray<Person>(contacts);
}
}
- Person (class)
- name : string
- address : Address
- phones: ObservedArray<string>
- Address (class)
- street : string
- zip : number
- city : string
Address類聲明如下:
@Observed
export class Address {
street: string;
zip: number;
city: string;
constructor(street: string,
zip: number,
city: string) {
this.street = street;
this.zip = zip;
this.city = city;
}
}
Person類聲明如下:
let nextId = 0;
@Observed
export class Person {
id_: string;
name: string;
address: Address;
phones: ObservedArray<string>;
constructor(name: string,
street: string,
zip: number,
city: string,
phones: string[]) {
this.id_ = `${nextId}`;
nextId++;
this.name = name;
this.address = new Address(street, zip, city);
this.phones = new ObservedArray<string>(phones);
}
}
需要注意的是,因為phones是嵌套屬性厅须,如果要觀察到phones的變化仿畸,需要extends array,并用@Observed修飾它朗和。ObservedArray類的聲明如下错沽。
@Observed
export class ObservedArray<T> extends Array<T> {
constructor(args: T[]) {
console.log(`ObservedArray: ${JSON.stringify(args)} `)
if (args instanceof Array) {
super(...args);
} else {
super(args)
}
}
}
- selected : 對Person的引用。
更新流程如下:
- 在根節(jié)點PageEntry中初始化所有的數(shù)據(jù)例隆,將me和contacts和其子組件AddressBookView建立雙向數(shù)據(jù)同步甥捺,selectedPerson默認為me,需要注意镀层,selectedPerson并不是PageEntry數(shù)據(jù)源中的數(shù)據(jù)镰禾,而是數(shù)據(jù)源中,對某一個Person的引用唱逢。
PageEntry和AddressBookView聲明如下:
@Component
struct AddressBookView {
@ObjectLink me : Person;
@ObjectLink contacts : ObservedArray<Person>;
@State selectedPerson: Person = new Person("", "", 0, "", []);
aboutToAppear() {
this.selectedPerson = this.me;
}
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start}) {
Text("Me:")
PersonView({
person: this.me,
phones: this.me.phones,
selectedPerson: this.selectedPerson
})
Divider().height(8)
ForEach(this.contacts, (contact: Person) => {
PersonView({
person: contact,
phones: contact.phones as ObservedArray<string>,
selectedPerson: this.selectedPerson
})
},
(contact: Person): string => { return contact.id_; }
)
Divider().height(8)
Text("Edit:")
PersonEditView({
selectedPerson: this.selectedPerson,
name: this.selectedPerson.name,
address: this.selectedPerson.address,
phones: this.selectedPerson.phones
})
}
.borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5)
}
}
@Entry
@Component
struct PageEntry {
@Provide addrBook: AddressBook = new AddressBook(
new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********", "18*********"]),
[
new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]),
new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]),
new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]),
]);
build() {
Column() {
AddressBookView({
me: this.addrBook.me,
contacts: this.addrBook.contacts,
selectedPerson: this.addrBook.me
})
}
}
}
-
PersonView吴侦,即電話簿中聯(lián)系人姓名和首選電話的View,當用戶選中坞古,即高亮當前Person备韧,需要同步回其父組件AddressBookView的selectedPerson,所以需要通過@Link建立雙向同步痪枫。
PersonView聲明如下:// 顯示聯(lián)系人姓名和首選電話 // 為了更新電話號碼织堂,這里需要@ObjectLink person和@ObjectLink phones, // 顯示首選號碼不能使用this.person.phones[0]奶陈,因為@ObjectLink person只代理了Person的屬性易阳,數(shù)組內(nèi)部的變化觀察不到 // 觸發(fā)onClick事件更新selectedPerson @Component struct PersonView { @ObjectLink person : Person; @ObjectLink phones : ObservedArray<string>; @Link selectedPerson : Person; build() { Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { Text(this.person.name) if (this.phones.length > 0) { Text(this.phones[0]) } } .height(55) .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff") .onClick(() => { this.selectedPerson = this.person; }) } }
-
選中的Person會在PersonEditView中顯示詳細信息,對于PersonEditView的數(shù)據(jù)同步分為以下三種方式:
在Edit狀態(tài)通過Input.onChange回調(diào)事件接受用戶的鍵盤輸入時吃粒,在點擊“Save Changes”之前潦俺,這個修改是不希望同步回數(shù)據(jù)源的,但又希望刷新在當前的PersonEditView中徐勃,所以@Prop深拷貝當前Person的詳細信息事示;
PersonEditView通過@Link seletedPerson: Person和AddressBookView的``selectedPerson建立雙向同步,當用戶點擊“Save Changes”的時候僻肖,@Prop的修改將被賦值給@Link seletedPerson: Person肖爵,這就意味這,數(shù)據(jù)將被同步回數(shù)據(jù)源臀脏。
PersonEditView中通過@Consume addrBook: AddressBook和根節(jié)點PageEntry建立跨組件層級的直接的雙向同步關(guān)系劝堪,當用戶在PersonEditView界面刪除某一個聯(lián)系人時法挨,會直接同步回PageEntry,PageEntry的更新會通知AddressBookView刷新contracts的列表頁幅聘。 PersonEditView聲明如下:
// 渲染Person的詳細信息
// @Prop裝飾的變量從父組件AddressBookView深拷貝數(shù)據(jù)眷柔,將變化保留在本地, TextInput的變化只會在本地副本上進行修改肋杖。
// 點擊 "Save Changes" 會將所有數(shù)據(jù)的復(fù)制通過@Prop到@Link, 同步到其他組件
@Component
struct PersonEditView {
@Consume addrBook : AddressBook;
/* 指向父組件selectedPerson的引用 */
@Link selectedPerson: Person;
/*在本地副本上編輯,直到點擊保存*/
@Prop name: string = "";
@Prop address : Address = new Address("", 0, "");
@Prop phones : ObservedArray<string> = [];
selectedPersonIndex() : number {
return this.addrBook.contacts.findIndex((person: Person) => person.id_ == this.selectedPerson.id_);
}
build() {
Column() {
TextInput({ text: this.name})
.onChange((value) => {
this.name = value;
})
TextInput({text: this.address.street})
.onChange((value) => {
this.address.street = value;
})
TextInput({text: this.address.city})
.onChange((value) => {
this.address.city = value;
})
TextInput({text: this.address.zip.toString()})
.onChange((value) => {
const result = Number.parseInt(value);
this.address.zip= Number.isNaN(result) ? 0 : result;
})
if (this.phones.length > 0) {
ForEach(this.phones,
(phone: ResourceStr, index?:number) => {
TextInput({ text: phone })
.width(150)
.onChange((value) => {
console.log(`${index}. ${value} value has changed`)
this.phones[index!] = value;
})
},
(phone: ResourceStr, index?:number) => `${index}`
)
}
Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) {
Text("Save Changes")
.onClick(() => {
// 將本地副本更新的值賦值給指向父組件selectedPerson的引用
// 避免創(chuàng)建新對象宋舷,在現(xiàn)有屬性上進行修改
this.selectedPerson.name = this.name;
this.selectedPerson.address = new Address(this.address.street, this.address.zip, this.address.city)
this.phones.forEach((phone : string, index : number) => { this.selectedPerson.phones[index] = phone } );
})
if (this.selectedPersonIndex()!=-1) {
Text("Delete Contact")
.onClick(() => {
let index = this.selectedPersonIndex();
console.log(`delete contact at index ${index}`);
// 刪除當前聯(lián)系人
this.addrBook.contacts.splice(index, 1);
// 刪除當前selectedPerson巷怜,選中態(tài)前移一位
index = (index < this.addrBook.contacts.length) ? index : index-1;
// 如果contract被刪除完葛超,則設(shè)置me為選中態(tài)
this.selectedPerson = (index>=0) ? this.addrBook.contacts[index] : this.addrBook.me;
})
}
}
}
}
}
其中關(guān)于@ObjectLink和@Link的區(qū)別要注意以下幾點:
- 在AddressBookView中實現(xiàn)和父組件PageView的雙向同步,需要用@ObjectLink me : Person和@ObjectLink contacts : ObservedArray<Person>延塑,而不能用@Link绣张,原因如下:
- @Link需要和其數(shù)據(jù)源類型完全相同,且僅能觀察到第一層的變化关带;
- @ObjectLink可以被數(shù)據(jù)源的屬性初始化侥涵,且代理了@Observed裝飾類的屬性,可以觀察到被裝飾類屬性的變化宋雏。 - 當 聯(lián)系人姓名 (Person.name) 或者首選電話號碼 (Person.phones[0]) 發(fā)生更新時芜飘,PersonView也需要同步刷新,其中Person.phones[0]屬于第二層的更新磨总,如果使用@Link將無法觀察到嗦明,而且@Link需要和其數(shù)據(jù)源類型完全相同。所以在PersonView中也需要使用@ObjectLink蚪燕,即@ObjectLink person : Person和@ObjectLink phones : ObservedArray<string>娶牌。
在這個例子中,我們可以大概了解到如何構(gòu)建ViewModel馆纳,在應(yīng)用的根節(jié)點中诗良,ViewModel的數(shù)據(jù)可能是可以巨大的嵌套數(shù)據(jù),但是在ViewModel和View的適配和渲染中厕诡,我們盡可能將ViewModel的數(shù)據(jù)項和View相適配累榜,這樣的話在針對每一層的View,都是一個相對“扁平”的數(shù)據(jù)灵嫌,僅觀察當前層就可以了壹罚。
在應(yīng)用實際開發(fā)中,也許我們無法避免去構(gòu)建一個十分龐大的Model寿羞,但是我們可以在UI樹狀結(jié)構(gòu)中合理地去拆分數(shù)據(jù)猖凛,使得ViewModel和View更好的適配,從而搭配最小化更新來實現(xiàn)高性能開發(fā)绪穆。
完整應(yīng)用代碼如下:
// ViewModel classes
let nextId = 0;
@Observed
export class ObservedArray<T> extends Array<T> {
constructor(args: T[]) {
console.log(`ObservedArray: ${JSON.stringify(args)} `)
if (args instanceof Array) {
super(...args);
} else {
super(args)
}
}
}
@Observed
export class Address {
street: string;
zip: number;
city: string;
constructor(street: string,
zip: number,
city: string) {
this.street = street;
this.zip = zip;
this.city = city;
}
}
@Observed
export class Person {
id_: string;
name: string;
address: Address;
phones: ObservedArray<string>;
constructor(name: string,
street: string,
zip: number,
city: string,
phones: string[]) {
this.id_ = `${nextId}`;
nextId++;
this.name = name;
this.address = new Address(street, zip, city);
this.phones = new ObservedArray<string>(phones);
}
}
export class AddressBook {
me: Person;
contacts: ObservedArray<Person>;
constructor(me: Person, contacts: Person[]) {
this.me = me;
this.contacts = new ObservedArray<Person>(contacts);
}
}
// 渲染出Person對象的名稱和Observed數(shù)組<string>中的第一個號碼
// 為了更新電話號碼辨泳,這里需要@ObjectLink person和@ObjectLink phones虱岂,
// 不能使用this.person.phones,內(nèi)部數(shù)組的更改不會被觀察到菠红。
// 在AddressBookView第岖、PersonEditView中的onClick更新selectedPerson
@Component
struct PersonView {
@ObjectLink person: Person;
@ObjectLink phones: ObservedArray<string>;
@Link selectedPerson: Person;
build() {
Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) {
Text(this.person.name)
if (this.phones.length) {
Text(this.phones[0])
}
}
.height(55)
.backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff")
.onClick(() => {
this.selectedPerson = this.person;
})
}
}
@Component
struct phonesNumber {
@ObjectLink phoneNumber: ObservedArray<string>
build() {
Column() {
ForEach(this.phoneNumber,
(phone: ResourceStr, index?: number) => {
TextInput({ text: phone })
.width(150)
.onChange((value) => {
console.log(`${index}. ${value} value has changed`)
this.phoneNumber[index!] = value;
})
},
(phone: ResourceStr, index: number) => `${this.phoneNumber[index] + index}`
)
}
}
}
// 渲染Person的詳細信息
// @Prop裝飾的變量從父組件AddressBookView深拷貝數(shù)據(jù),將變化保留在本地, TextInput的變化只會在本地副本上進行修改试溯。
// 點擊 "Save Changes" 會將所有數(shù)據(jù)的復(fù)制通過@Prop到@Link, 同步到其他組件
@Component
struct PersonEditView {
@Consume addrBook: AddressBook;
/* 指向父組件selectedPerson的引用 */
@Link selectedPerson: Person;
/*在本地副本上編輯蔑滓,直到點擊保存*/
@Prop name: string = "";
@Prop address: Address = new Address("", 0, "");
@Prop phones: ObservedArray<string> = [];
selectedPersonIndex(): number {
return this.addrBook.contacts.findIndex((person: Person) => person.id_ == this.selectedPerson.id_);
}
build() {
Column() {
TextInput({ text: this.name })
.onChange((value) => {
this.name = value;
})
TextInput({ text: this.address.street })
.onChange((value) => {
this.address.street = value;
})
TextInput({ text: this.address.city })
.onChange((value) => {
this.address.city = value;
})
TextInput({ text: this.address.zip.toString() })
.onChange((value) => {
const result = Number.parseInt(value);
this.address.zip = Number.isNaN(result) ? 0 : result;
})
if (this.phones.length > 0) {
phonesNumber({ phoneNumber: this.phones })
}
Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) {
Text("Save Changes")
.onClick(() => {
// 將本地副本更新的值賦值給指向父組件selectedPerson的引用
// 避免創(chuàng)建新對象,在現(xiàn)有屬性上進行修改
this.selectedPerson.name = this.name;
this.selectedPerson.address = new Address(this.address.street, this.address.zip, this.address.city)
this.phones.forEach((phone: string, index: number) => {
this.selectedPerson.phones[index] = phone
});
})
if (this.selectedPersonIndex() != -1) {
Text("Delete Contact")
.onClick(() => {
let index = this.selectedPersonIndex();
console.log(`delete contact at index ${index}`);
// 刪除當前聯(lián)系人
this.addrBook.contacts.splice(index, 1);
// 刪除當前selectedPerson遇绞,選中態(tài)前移一位
index = (index < this.addrBook.contacts.length) ? index : index - 1;
// 如果contract被刪除完键袱,則設(shè)置me為選中態(tài)
this.selectedPerson = (index >= 0) ? this.addrBook.contacts[index] : this.addrBook.me;
})
}
}
}
}
}
@Component
struct AddressBookView {
@ObjectLink me: Person;
@ObjectLink contacts: ObservedArray<Person>;
@State selectedPerson: Person = new Person("", "", 0, "", []);
aboutToAppear() {
this.selectedPerson = this.me;
}
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start }) {
Text("Me:")
PersonView({
person: this.me,
phones: this.me.phones,
selectedPerson: this.selectedPerson
})
Divider().height(8)
ForEach(this.contacts, (contact: Person) => {
PersonView({
person: contact,
phones: contact.phones as ObservedArray<string>,
selectedPerson: this.selectedPerson
})
},
(contact: Person): string => {
return contact.id_;
}
)
Divider().height(8)
Text("Edit:")
PersonEditView({
selectedPerson: this.selectedPerson,
name: this.selectedPerson.name,
address: this.selectedPerson.address,
phones: this.selectedPerson.phones
})
}
.borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5)
}
}
@Entry
@Component
struct PageEntry {
@Provide addrBook: AddressBook = new AddressBook(
new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********", "18*********"]),
[
new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["11*********", "12*********"]),
new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["13*********", "14*********"]),
new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["15*********", "168*********"]),
]);
build() {
Column() {
AddressBookView({
me: this.addrBook.me,
contacts: this.addrBook.contacts,
selectedPerson: this.addrBook.me
})
}
}
}
寫在最后
- 如果你覺得這篇內(nèi)容對你還蠻有幫助,我想邀請你幫我三個小忙:
- 點贊摹闽,轉(zhuǎn)發(fā)蹄咖,有你們的 『點贊和評論』,才是我創(chuàng)造的動力付鹿。
- 關(guān)注小編澜汤,同時可以期待后續(xù)文章ing??,不定期分享原創(chuàng)知識倘屹。
- 想要獲取更多完整鴻蒙最新學(xué)習(xí)知識點银亲,請移步前往小編:
https://gitee.com/MNxiaona/733GH/blob/master/jianshu