一、MVVM模式
MVVM模式是一种软件架构模式,由三个部分组成:Model(数据模型层),View(视图层),ViewModel(视图模型层)。核心是分离应用程序的视图和业务逻辑,通过数据绑定实现视图和业务逻辑的解耦。
以下是ArkUI MVVM模式示意图:

- Model层:存储数据和相关逻辑的模型。它表示组件或其他相关业务逻辑之间传输的数据。Model是对原始数据的进一步处理。
- View层:在ArkUI中通常是@Component装饰组件渲染的UI。
- ViewModel层:在ArkUI中,ViewModel是存储在自定义组件的状态变量(如@State @Prop等装饰器修饰的变量)、LocalStorage和AppStorage中的数据。
-
- 自定义组件通过执行其build()方法或者@Builder装饰的方法来渲染UI,即ViewModel可以渲染View。
- View可以通过相应event handler来改变ViewModel,即事件驱动ViewModel的改变,另外ViewModel提供了@Watch回调方法用于监听状态数据的改变。
- 在ViewModel被改变时,需要同步回Model层,这样才能保证ViewModel和Model的一致性,即应用自身数据的一致性。
- ViewModel结构设计应始终为了适配自定义组件的构建和更新,这也是将Model和ViewModel分开的原因。
二、组件内状态管理(V1)
V1状态管理中装饰器有:@State, @Prop, @Link, @Observerd, @ObjectLink, @Provide, @Consume, @LocalStorageLink, @LocalStorageProp, @StorageLink, @StorageProp, @Watch,我们下面一一分析这些装饰器的作用。
@State 组件内状态
@State修饰的变量称之为状态变量,它是组件内状态装饰器,当状态变量改变时对应的UI也会发生相应的变化,大部分时候和其他装饰器联用时@State修饰的变量往往是作为数据源。
@State支持修饰基础类型、class、数组、map、set等类型,现在先来感受下组件内状态管理的基本用法:
less
@Entry
@Component
struct EntryIndex {
@State count: number = 0
build() {
Scroll() {
Column() {
Button(`${this.count}`, {
type: ButtonType.Capsule
}).onClick(() => {
this.count++
})
}
}
}
}
以上示例中点击按钮后count计数会+1,这个时候因为组件Button和count变量有数据绑定的关系,所以Button组件会重新渲染内容改变。这就是@State最基础的用法,实际项目中有很多情况是复杂组件之间的变量传递,下面这张图体现了 哪些装饰器修饰的变量可以传递给@State 以及 @State装饰的变量可以传递给哪些装饰器修饰的变量

@State状态变量是不会根据父组件或子组件传递变量的改变而改变,所以他是组件内状态,接下来看下他是如何与其他装饰器联动的。
@Prop 父子单向同步
@Prop 装饰的变量可以和父组件建立单向同步的关系,@Prop状态变量是可变的,但是它的变化是不会同步给父组件,父组件变量变化会同步给该@Prop状态变量。
@Prop支持装饰基础类型、class、数组、map、set等类型,大多数情况下父组件传递变量是@State修饰的。下图体现了 @Prop状态变量可以接收哪些父组件装饰器的状态变量 以及 @Prop状态变量可以传递哪些装饰器修饰的子组件状态变量

看起来基本和@State支持情况一致。ps: @Prop是深度拷贝变量,所以会有一定的性能损耗,大对象建议用Link代替。
@Link 父子双向同步
@Link 装饰的变量可以和父组件建立单向同步的关系,@Link状态变量是可变的,但是它的变化会同步给父组件,父组件变量变化会同步给该@Link状态变量。
@Link支持装饰基础类型、class、数组、map、set等类型。其实总结就一句话,@Link除了能父子双向同步之外和@Prop基本一致。
我们直接看下实际代码应用中@Link和@Prop的差异:
less
@Entry
@Component
struct EntryIndex {
@State count: number = 0
build() {
Scroll() {
Column() {
Button(`关联子组件的计数器: ${this.count}`, {
type: ButtonType.Capsule
}).onClick(() => {
this.count++
})
ChildComponent1({
count: this.count,
})
}
}
}
}
@Component
struct ChildComponent1 {
@Prop count: number
build() {
Column() {
Button(`子组件1的计数器(Prop): ${this.count}`, {
type: ButtonType.Capsule
}).margin(12)
.onClick(() => {
this.count++
})
}
}
}
@Component
struct ChildComponent2 {
@Link count: number
build() {
Column() {
Button(`子组件2的计数器(Link): ${this.count}`, {
type: ButtonType.Capsule
}).margin(12)
.onClick(() => {
this.count++
})
}
}
}
父组件有一个@State装饰的count状态变量,分别传递给两个子组件,子组件1的count是用@Prop装饰,子组件2的count是用@Link装饰。当代码运行时:
- 点击父组件的Button,无论是父组件自身的Button内容会+1,还是其他两个子组件的Button都会+1。
- 点击子组件1的Button,仅仅只有子组件1的Button内容会+1,父组件和子组件2的Button内容不变。
- 点击子组件2的Button,父组件和子组件2Button内容会+1,子组件1的Button内容变成和父组件和子组件2一致。
以上几种装饰器已经能实现大部分组件状态管理的需求,但实际项目中还是有很多更复杂的场景,比如跨代组件状态同步。
@Provide和@Consume:后代组件双向同步
@Provider和@Consume两者配合应用于跨代组件双向同步(不仅仅是爷孙组件),所以他们不需要传递变量,而是采用别名绑定的方式。
@Provider和@Consume支持装饰基础类型、class、数组、map、set、Date等类型。
scss
@Entry
@Component
struct EntryIndex {
@Provide('desCount') desCount: number = 0
build() {
Scroll() {
Column() {
Button(`关联后代组件的计数器: ${this.desCount}`, {
type: ButtonType.Capsule
}).onClick(() => {
this.desCount++
})
// 伪代码表示组件层级嵌套
Child(){
DeDescendComponent1()
}
}
}
}
}
@Component
struct DescendComponent1 {
@Consume('desCount') desCount: number
build() {
Column() {
Button(`后代组件1的计数器: ${this.desCount}`, {
type: ButtonType.Capsule
}).onClick(() => {
this.desCount++
})
}
}
}
以上用@Provider和@Consume装饰的且别名为"desCount"的变量数据会双向同步。
@Provider装饰的变量当然也可以传递,只是这个变量不会同步数据,它只有和@Consume配合才能双向同步。

@Consume会特殊一些,它是禁止父组件传递变量给@Consume装饰的变量,因此它只有传递给子组件的权力。

@Observed 和 @ObjectLink:嵌套类对象属性变化
在实际应用开发中,对象传递可能比基础类型传递要更加常见,以上已经提及的装饰器只能观察到第一层,也就是说如果对象内部属性的变化是无法被观察到的。举个例子:
less
// 定义一个类
class CountObj {
name: string = ''
count: number = 0
}
@Entry
@Component
struct EntryIndex {
@State countObj: CountObj = new CountObj()
build() {
Scroll() {
Column() {
Button(`${this.countObj.count}`, {
type: ButtonType.Capsule
}).onClick(() => {
this.countObj.count++
})
}
}
}
}
在这个示例里,this.countObj.count++是不会让Button重新渲染的,因为@State只能监听 countObj 本身,而不能监听到内部属性,所以我们要做的第一步是用@Observed装饰这个类,使这个类具备了内部属性被观察的能力,但这也仅仅是第一层属性,如果内部属性是对象,我们还是要给这个对象类用@Observed装饰。
@ObjectLink装饰的对象实例必须是已经用@Observed装饰的类,@ObjectLink仅支持装饰 class 对象类型。

下面是一个使用示例,涉及到父组件 -> 子组件 -> 孙子组件的状态管理:
scss
@Entry
@Component
struct EntryIndex {
@State countArr: CountObj[] = []
build() {
Scroll() {
Column() {
Button(`增加对象计数器`, {
type: ButtonType.Capsule
}).onClick(() => {
let countObj = new CountObj()
countObj.name = '测试'
countObj.count = 1
this.countArr.push(countObj)
})
ChildComponent2({
countArr: this.countArr
})
}
}
}
}
@Component
struct ChildComponent2 {
@Link countArr: CountObj[]
build() {
Column() {
List(){
ForEach(this.countArr, (item: CountObj) => {
ListItem(){
DescendComponentList2({
desCountObj: item
})
}
})
}
}
}
}
@Component
struct DescendComponentList2 {
@ObjectLink desCountObj: CountObj
build() {
Text(`对象计数器(ObjectLink)${this.desCountObj.name}:${this.desCountObj.count}`)
.onClick(() => {
this.desCountObj.count++
})
}
}
- 父组件里@State装饰countArr数组对象,把countArr变量传递给了子组件,子组件用@Link装饰变量接收,这样保证父子是双向同步的,父组件和子组件都能观察到内数组的增删改查。
- 点击Button给countArr增加一个countObj对象,子组件观察到数组增加了,于是列表项增加了一个孙子组件。
- 孙子组件用@ObjectLink装饰了desCountObj变量,同时CountObj已经被@Observed装饰了,这个时候孙子组件内对desCountObj对象内属性的操作都会同步给子组件和父组件。
通过这样的方式,未来增加其他兄弟组件都能观察到属性的变化,从而实现各组件UI联动。
三、管理应用内状态
在第二章已经介绍了组件树上共享状态变量,从而实现UI和数据的绑定和联动。那么实际项目遇到的情况可能更加复杂,不同页面之间甚至是不同UIAbility之间都存在数据共享的情况。
页面间状态存储
LocalStorage是ArkTS提供的页面级别的状态存储,可以从应用逻辑里使用LocalStorage,也可以从UI内部使用LocalStorage。
应用逻辑中使用简单看下示例代码:
typescript
let para: Record<string,number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para); // 创建新实例并使用给定对象初始化
let propA: number | undefined = storage.get('PropA') // propA == 47
let link1: SubscribedAbstractProperty<number> = storage.link('PropA'); // link1.get() == 47
let link2: SubscribedAbstractProperty<number> = storage.link('PropA'); // link2.get() == 47
let prop: SubscribedAbstractProperty<number> = 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
这样在LocalStorage对象内保存了 'PropA' 属性,由link方法获取的变量支持双向同步,prop方法获取的变量支持单向同步。我们重点看下UI内部的使用方式。
首先有两个装饰器@LocalStorageLink和@LocalStorageProp,一看名字我们就大致明白了这两个装饰器的作用,@LocalStorageLink支持双向同步,@LocalStorageProp支持单向同步,他们都是用别名的方式来绑定关系,接下来看一个示例来理解下:
typescript
let localStorage = LocalStorage.getShared()
localStorage.setOrCreate("count", 0)
@Entry(LocalStorage.getShared())
@Component
struct EntryIndex {
@LocalStorageLink('count') count: number = 0
build() {
Scroll() {
Column() {
Button(`关联其他页面的计数器: ${this.count}`, {
type: ButtonType.Capsule
}).onClick(() => {
this.count++
})
}
}
}
}
@Entry(LocalStorage.getShared())
@Component
export struct EntryNextPage {
@LocalStorageProp('count') count: number = 0
build() {
Column(){
Button(`单向关联上个页面的计数器: ${this.count}`, {
type: ButtonType.Capsule
}).onClick(() => {
this.count++
})
}
}
}
- 首先第一步要绑定LocalStorage和页面,两个页面的@Entry都绑定LocalStorage.getShared(),这代表他们共用一个LocalStorage对象,然后创建一个key为count的共享属性。
- 接着我们在组件内就可以通过定义相同的别名'count'来观察到属性变化,在第一个页面用的是@LocalStorageLink双向同步,所以当点击按钮后count++,其他所有被@LocalStorageLink和@LocalStorageProp装饰的变量都会观察到+1,从而重新渲染组件内容。
- 在第二个页面采用@LocalStorageProp观察count,所以点击Button使count++,这只能让自身页面的Button内容改变,而不能让其他页面同步变化。
应用全局状态存储
AppStorage是应用全局的UI状态存储,是和应用的进程绑定的。其实它的用法和LocalStorage基本上一模一样,装饰器@StorageLink双向同步和@StorageProp单向同步。使用示例:
typescript
AppStorage.setOrCreate("count", 0)
// EntryIndex是EntryAbility的页面
@Entry
@Component
struct EntryIndex {
@StorageLink('count') count: number = 0
build() {
Scroll() {
Column() {
Button(`关联全局的计数器: ${this.count}`, {
type: ButtonType.Capsule
}).onClick(() => {
this.count++
})
}
}
}
}
// SecondIndex是SecondAbility的页面
@Entry
@Component
export struct SecondIndex {
@StorageLink('count') count: number = 0
build() {
Column(){
Button(`关联全局的计数器: ${this.count}`, {
type: ButtonType.Capsule
}).onClick(() => {
this.count++
})
}
}
}
AppStorage.setOrCreate("count", 0)创建了一个共享属性,其他页面通过@StorageLink和@StorageProp就能联动了,
以上存储属于运行时内存存储,如果想要持久化存储,可以使用PersistentStorage,而且ArkTS已经把PersistentStorage和AppStorage关联起来可以共享同一属性,我们在UI内部使用时依然只需要关心@StorageLink和@StorageProp装饰器,在某处创建好需要持久化的属性即可。

参考资料