在声明式 UI 框架中,数据的改变触发 UI 的重新渲染。在 ArkUI 中不是所有数据的变化都会触发 UI 重新渲染,只有 状态变量 才会引起 UI 重新渲染。
状态变量
状态变量: 指被状态装饰器装饰的变量,只有这种变量的改变才会引起 UI 的重新渲染。
常规变量: 指没有被状态装饰器装饰的变量,不会引起 UI 的重新渲染。
按影响范围分为:
- 组件级别的状态: @State, @Prop, @Link, @Provide/@Consume, @Observed, @ObjectLink
- 应用级别的状态:@StorageLink/@LocalStorageLink, @StorageProp/@LocalStorageProp
按传递方向分为:
- 只读的单向传递
- 可变更的双向传递
@State 组件内状态
@State 装饰的状态变量,是组件内的状态,是私有的,只能从组件内部访问,不与父组件中任何类型的状态变量同步。
允许装饰的变量类型:Object、class、string、number、boolean、enum,Date, 以及这些类型的数组。API11及以上支持Map、Set, undefined,null 和 联合类型
@State 装饰原始类型
@Entry
@Component
struct StatePage {
@State text: string = 'HarmonyOS'
@State count: number = 0
build() {
Column({ space: 10 }) {
Text('string: ' + this.text).fontSize(17)
Text('number: ' + this.count)
Button('text = "Android"').onClick((event: ClickEvent) => {
this.text = 'Android'
})
Button('count++').onClick((event: ClickEvent) => {
this.count++
})
}
.width('100%')
.height('100%')
}
}
点击 Button 修改了 text,count 变量的值,会触发 UI 重新渲染,显示最新的值。
@State 装饰 class
export class User {
name: string
car?: Car
constructor(name: string, car?: Car) {
this.name = name;
this.car = car;
}
}
export class Car {
brand: string
engine?: Engine
constructor(brand: string, engine?: Engine) {
this.brand = brand;
this.engine = engine;
}
}
export class Engine {
type?: string
constructor(type: string) {
this.type = type;
}
}
@Entry
@Component
struct StateClassPage {
@State user: User = new User('Jack')
build() {
Column({space: 10}) {
Text('Class: ' + `name=${this.user.name}, car=${this.user.car?.brand}`).fontSize(17)
Column({ space: 10 }) {
Button(`user = new User('Mike')`).onClick((event: ClickEvent) => {
// 有效刷新
this.user = new User('Mike')
})
Button(`user.name = 'Pony New'`).onClick((event: ClickEvent) => {
// 有效刷新
this.user.name = 'Pony New'
})
Button(`user.car = new Car('Benz')`).onClick((event: ClickEvent) => {
// 有效刷新,@State 可以观察到一级属性 car 的赋值
this.user.car = new Car('Benz')
})
Button(`this.user.car.brand = 'BMW'`).onClick((event: ClickEvent) => {
// 无效刷新, user.car.brand, user.car.engine 为二级属性, @State 观察不到, 需要使用 @Observed 和 @ObjectLink
if (this.user.car) {
this.user.car.brand = 'BMW'
}
})
}
}.width('100%')
}
}
对于 @State 装饰的 Class 状态变量, ArkUI 只能观察到状态变量的一级属性的变化,从而引起 UI 重新渲染。如上例中 user.name 和 user.car 是 User 的一级属性,user.car.brand 是 User 的二级属性, 所以点击Button执行 this.user.car.brand = 'BMW'
UI 没有刷新,这种情况,需要使用 @Observed/@ObjectLink
@State 装饰数组
@Entry
@Component
struct StateArrayPage {
@State stringArray: string[] = ['Java', 'Kotlin']
@State userArray: User[] = [new User('Jack'), new User('Make')]
build() {
Column({ space: 10 }) {
ForEach(this.stringArray, (item: string) => {
Text(item)
})
Row({ space: 6 }) {
Button('add').onClick((event: ClickEvent) => {
this.stringArray.push('Typescript')
})
Button('remove').onClick((event: ClickEvent) => {
if (this.stringArray.length > 0) {
this.stringArray.splice(0, 1)
}
})
Button('update').onClick((event: ClickEvent) => {
if (this.stringArray.length > 0) {
this.stringArray[0] = 'Javascript by updated'
}
})
Button('new Array()').onClick((event: ClickEvent) => {
this.stringArray = ['New Array1', 'New Array2']
})
}
Divider().height(20).color(Color.Gray)
ForEach(this.userArray, (item: User) => {
Text(`name=${item.name},car=${item.car}`)
})
Column({ space: 10 }) {
Button('add').onClick((event: ClickEvent) => {
this.userArray.push(new User('Jane'))
})
Button('remove').onClick((event: ClickEvent) => {
if (this.stringArray.length > 0) {
this.userArray.splice(0, 1)
}
})
Button('update userArray[0]=new User("Lace")').onClick((event: ClickEvent) => {
if (this.stringArray.length > 0) {
// class 数组, 可以观察到数组中元素 重新赋值
this.userArray[0] = new User('Lace')
}
})
Button('userArray[0].name="Shane"').onClick((event: ClickEvent) => {
// 无效刷新, 对于 class数组, 观察不到数组中 元素的属性 赋值
this.userArray[0].name = 'Shane'
})
}
}
.width('100%')
}
}
对于数组,ArkUI 可以观察到 数组新增,删除,重新创建数组, 数组项重新赋值,从而引起 UI 重新渲染,无法观察到 数组中某个元素的 属性的变化
@Prop 父 —> 子单向同步
@Prop 是单向同步的:对父组件状态变量值的修改,将同步给子组件@Prop装饰的变量,子组件@Prop变量的修改不会同步到父组件的状态变量上。
@Entry
@Component
struct PropPage {
@State text: string = 'ArkUI'
@State user: User = new User('Jane', new Car('Benz E'))
build() {
Column({ space: 10 }) {
Text(this.text).onClick(() => {
this.text = 'Flutter '
})
Text(`name:${this.user.name},car=${this.user.car?.brand}`)
.onClick(()=> {
this.user = new User('parent User')
})
Divider().height(20)
PropChild({ title: this.text, user: this.user })
}.width('100%')
}
}
@Component
struct PropChild {
/**
* 修改父组件的状态变量可以同步刷新子组件, 而修改子组件的状态变量不会同步刷新父组件
*/
@Prop title: string = 'default'
@Prop user: User
build() {
Column({ space: 10 }) {
Text(this.title).onClick(() => {
this.title = 'JetpackCompose'
})
Text(`name:${this.user.name},car=${this.user.car?.brand}`)
Button('user=new User("Shine")').onClick((event: ClickEvent) => {
this.user = new User('Shine')
})
Button('user.car = new Car("Audi A6")').onClick((event: ClickEvent) => {
this.user.car = new Car('Audi A6')
})
Button('user.car.brand="BWM 530"').onClick((event: ClickEvent) => {
if (this.user.car) {
this.user.car.brand = 'BWM 530'
}
})
}
}
}
@Prop 装饰的状态变量, ArkUI 同样观察不到 二级属性的变化,并且子组件状态变量的变化不会引起父组件 UI 的刷新, 但父组件状态变量的变化可以引起子组件的刷新。
@Link 父子双向同步
子组件中被@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定.
@Link装饰器不能在@Entry装饰的自定义组件中使用。
@Link 装饰原始类型
@Entry
@Component
struct LinkBasicPage {
@State text: string = 'ArkUI'
build() {
Column({ space: 10 }) {
Text('父组件:' + this.text).onClick(() => {
this.text = 'Flutter '
})
LinkBasicChild({ title: $text })
}.width('100%')
}
}
@Component
struct LinkBasicChild {
// @Link装饰的状态变量禁止初始化
// @Link title: string = ''
@Link title: string
build() {
Column({ space: 10 }) {
Text('子组件:' + this.title).onClick(() => {
this.title = 'JetpackCompose'
})
}
}
}
父子组件的状态变量的变化,可以相互引起 UI 刷新
@Link 装饰 class
@Entry
@Component
struct LinkClassPage {
@State user: User = new User('Jane', new Car('Benz E'))
build() {
Column({ space: 10 }) {
Text(`父组件 name=${this.user.name},car=${this.user.car?.brand}`)
Button('user=new User("Mike")').onClick((event: ClickEvent) => {
this.user = new User('Mike-P')
})
Divider()
LinkClassChild({ user: $user })
}.width('100%')
}
}
@Component
struct LinkClassChild {
// 禁止本地初始化
@Link user: User
build() {
Column({ space: 10 }) {
Text(`子组件 name=${this.user.name},car=${this.user.car?.brand}`)
Button('user=new User("Shine")').onClick((event: ClickEvent) => {
this.user = new User('Shine')
})
Button('user.car = new Car("Audi A6")').onClick((event: ClickEvent) => {
this.user.car = new Car('Audi A6')
})
Button('user.car.brand="BWM 530"').onClick((event: ClickEvent) => {
if (this.user.car) {
this.user.car.brand = 'BWM 530'
}
})
}
}
}
@Link 装饰的状态变量, ArkUI 同样观察不到 二级属性的变化。一级属性可以引起父子组件UI 的刷新。
@Link 装饰数组
@Entry
@Component
struct LinkArrayPage {
@State list: string[] = ['Android', 'iOS', 'Harmony']
build() {
Column({ space: 10 }) {
ForEach(this.list, (item: string) => {
Text(item)
})
Button('new Array').onClick((event: ClickEvent) => {
this.list = new Array<string>('Benz', 'BMW', 'Audi')
})
Button('Add').onClick((event: ClickEvent) => {
this.list.push('parent add')
})
Button('remove').onClick((event: ClickEvent) => {
if (this.list.length > 0) {
this.list.splice(0, 1)
}
})
Button('update').onClick((event: ClickEvent) => {
if (this.list.length > 0) {
this.list[0] = 'parent update'
}
})
Divider()
LinkArrayChild({ childList: $list })
}.width('100%')
}
}
@Component
struct LinkArrayChild {
@Link childList: string[]
build() {
Column({ space: 10 }) {
ForEach(this.childList, (item: string) => {
Text(item)
})
Button('new Array').onClick((event: ClickEvent) => {
this.childList = new Array<string>('Kotlin', 'Swift', 'ArkTS')
})
Button('Add').onClick((event: ClickEvent) => {
this.childList.push('child Add')
})
Button('remove').onClick((event: ClickEvent) => {
if (this.childList.length > 0) {
this.childList.splice(0, 1)
}
})
Button('update').onClick((event: ClickEvent) => {
if (this.childList.length > 0) {
this.childList[0] = 'child update'
}
})
}
}
}
@Provide/@Consume 父与后代组件双向同步
@Provide和@Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递,@Provide和@Consume摆脱参数传递机制的束缚,实现跨层级传递。
@Provide/@Consume 装饰原始类型
@Entry
@Component
struct ProvideConsumeBasicPage {
@Provide count: number = 1
build() {
Column({ space: 10 }) {
Text(this.count.toString()).onClick(() => {
this.count++
})
ProvideChild()
}
.width('100%')
}
}
@Component
struct ProvideChild {
@Consume count: number
build() {
Column({ space: 10 }) {
Text(this.count.toString()).onClick(() => {
this.count += 2
})
ProvideChildSecond()
}
.backgroundColor(Color.Brown)
.padding(30)
}
}
@Component
struct ProvideChildSecond {
@Consume count: number
build() {
Column() {
Text(this.count.toString()).onClick(() => {
this.count += 3
})
}
.backgroundColor(Color.Pink)
.padding(30)
}
}
@Provide/@Consume 装饰 class
@Entry
@Component
struct ProvideConsumeClassPage {
@Provide user: User = new User('Apple')
build() {
Column({ space: 10 }) {
Text('父组件:' + this.user.name)
Button('user.name="parent"').onClick((event: ClickEvent) => {
this.user.name = 'parent'
})
Divider()
ProvideClassChild()
}.width('100%')
}
}
@Component
struct ProvideClassChild {
@Consume user: User
build() {
Column({ space: 10 }) {
Text('孩子组件:' + this.user.name)
Divider()
ProvideClassChildSecond()
}
}
}
@Component
struct ProvideClassChildSecond {
@Consume user: User
build() {
Column({ space: 10 }) {
Text('孙子组件:' + this.user.name)
Divider()
ProvideClassChildThird()
}
}
}
@Component
struct ProvideClassChildThird {
@Consume user: User
build() {
Column({ space: 10 }) {
Text('曾孙子:' + this.user.name)
Button('user=new User("曾孙子")').onClick((event: ClickEvent) => {
this.user = new User('曾孙子')
})
Button('user.name="曾孙name:sx"').onClick((event: ClickEvent) => {
this.user.name="曾孙name:sx"
})
Button('user.car.brand = "曾孙.name.car: Benz S"').onClick((event: ClickEvent) => {
if (this.user.car) {
this.user.car.brand = '曾孙.name.car: Benz S'
}
})
}
}
}
@Oberved/@ObjectLink 嵌套类对象属性变化
上述 @State, @Prop,@Link 和 @Provide/@Consume 装饰器只能观察 class 一级属性赋值,无法观察二级属性变化,但在实际开发过程中,class 的属性类型仍是 class 类型是很常用见的,因此就需要使用 @Observed/@ObjectLink 来实现了。
@ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步:
- 被@Observed装饰的类,可以被观察到属性的变化;
- 子组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被@Observed装饰。
- 单独使用@Observed是没有任何作用的,需要搭配@ObjectLink或者@Prop使用。
- @ObjectLink 装饰的变量必须为被@Observed装饰的class实例
- @ObjectLink 装饰的变量不允许重新赋值。
@ObjectLink 装饰 class
@Entry
@Component
struct ObjectLinkBasicPage {
@State user: User = new User('Jack', new Car('BMW 3 serial'))
build() {
Column({ space: 10 }) {
Button(`user.car.brand = 'BMW'`).onClick((event: ClickEvent) => {
if (this.user.car) {
this.user.car.brand = 'Benz E class'
}
})
Divider()
CarItem({ car: this.user.car })
}.width('100%')
}
}
@Component
struct CarItem {
// Car 需要被 @Observed 装饰
@ObjectLink car: Car
build() {
Column() {
Text('car: ' + this.car.brand)
Button('car=new Car("audi")').onClick((event: ClickEvent) => {
// 报错,@ObjectLink装饰的状态变量不允许被赋值, 可以对它的属性赋值
// this.car = new Car('audi')
this.car.brand = 'audi'
})
}
.width('90%')
.padding(20)
.backgroundColor(Color.Pink)
}
}
@ObjectLink 装饰数组中数组项
@Entry
@Component
struct ObjectLinkBasicPage {
@State userArray: User[] = [
new User('Jack', new Car('BMW 3 serial')),
new User('Mike', new Car('Audi A4L'))
]
build() {
Column({ space: 10 }) {
Button(`userArray[0].car.brand = 'Benz S class'`).onClick((event: ClickEvent) => {
if (this.userArray.length > 0) {
let user: User = this.userArray[0]
if (user.car) {
user.car.brand = 'Benz S class'
}
}
})
ForEach(this.userArray, (item: User) => {
UserItem({ user: item })
})
}.width('100%')
}
}
@Component
struct UserItem {
// User 需要被 @Observed 装饰
@ObjectLink user: User
build() {
CarItem({ car: this.user.car })
}
}
@Component
struct CarItem {
// Car 需要被 @Observed 装饰
@ObjectLink car: Car
build() {
Column() {
Text('car: ' + this.car.brand)
}
.width('90%')
.padding(20)
.backgroundColor(Color.Pink)
}
}
ViewModel 存储数据
@Entry
@Component
struct ViewModelPage {
@State viewModel: UserViewModel = new UserViewModel([
new User('jack', new Car('保时捷')),
new User('mack', new Car('奔驰'))
])
build() {
Scroll() {
Column() {
Flex({ wrap: FlexWrap.Wrap }) {
Button('new Array')
.margin(6)
.onClick((event: ClickEvent) => {
this.viewModel.userList = [new User('new kotlin')]
})
Button('add')
.margin(6)
.onClick((event: ClickEvent) => {
this.viewModel.userList.push(new User('kotlin'))
})
Button('remove')
.margin(6)
.onClick((event: ClickEvent) => {
if (this.viewModel.userList.length > 0) {
this.viewModel.userList.splice(0, 1)
}
})
Button('viewModel.userList[0]=new User("peter")')
.margin(6)
.onClick((event: ClickEvent) => {
if (this.viewModel.userList.length > 0) {
this.viewModel.userList[0] = new User('peter', new Car('马自达'))
}
})
Button('viewModel.userList[0].name="张三"')
.margin(6)
.onClick((event: ClickEvent) => {
if (this.viewModel.userList.length > 0) {
this.viewModel.userList[0].name = '张三'
}
})
Button('viewModel.userList[0].car.brand="XIAOMI SU7 MAX"')
.margin(6)
.onClick((event: ClickEvent) => {
if (this.viewModel.userList.length > 0) {
let car = this.viewModel.userList[0].car
if (car) {
car.brand = 'XIAOMI SU7 MAX'
}
}
})
}
ChildUser({ userList: this.viewModel.userList })
}
}
}
}
@Component
struct ChildUser {
// ObservableArray 需要被 @Observed 装饰
@ObjectLink userList: ObservableArray<User>
build() {
Column() {
ForEach(this.userList, (user: User) => {
UserItem({ user: user })
Divider()
})
}.padding(20)
}
}
@Component
struct UserItem {
// User 需要被 @Observed 装饰
@ObjectLink user: User
build() {
Column() {
Text('user.name: ' + this.user.name)
CarItem({ car: this.user.car })
}
}
}
@Component
struct CarItem {
// Car 需要被 @Observed 装饰
@ObjectLink car: Car | undefined
build() {
Text('user.car: ' + this.car?.brand)
}
}
@Observed
export class ObservableArray<T> extends Array<T> {
constructor(args: T[]) {
if (args instanceof Array) {
super(...args);
} else {
super(args)
}
}
}
@ObservedV2装饰器和@Trace装饰器:类属性变化观测
从 API12 开始,更推荐使用@ObservedV2装饰器和@Trace装饰器装饰类以及类中的属性,来管理状态。
@ObservedV2
class Father {
@Trace name: string = "Tom";
}
class Son extends Father {
}
@Entry
@Component
struct Index {
son: Son = new Son();
build() {
Column() {
// 当点击改变name时,Text组件会刷新
Text(`${this.son.name}`)
.onClick(() => {
this.son.name = "Jack";
})
}
}
}