Harmony ArkTS 状态管理

组件的状态

@State

@State 装饰的变量,即为状态变量,当状态发生改变时,对应的 UI 也会发生改变,和该状态变量不相关的 UI 不会发生重新渲染,从而实现页面渲染的按需更新。它是私有的,只能从组件内部访问,在声明时必须指定其类型和本地初始化。

举个简单的例子,这里点击 Button,改变状态变量,进而改变 Text 的显示。

ts 复制代码
@Entry
@Component
struct Index {
  @State count: number = 1;

  build() {
    Column() {
      Text(this.count.toString())
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
      Button('increase', { type: ButtonType.Capsule }).onClick(() => {
        this.count++
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

但是,并不是所有的更改都会引起 UI 刷新,只有可以被框架观察到的修改才会引起 UI 刷新,那什么样的修改才能被观察到呢?

@State 装饰的数据类型是 string,number,boolean,array,class,Object 时,都能被观察到,其中包含自身的赋值变化和其属性的赋值变化,以及数组本身的增删改变化。

ts 复制代码
export class MyCounter {
  value: number = 1

  constructor(value: number) {
    this.value = value
  }
}
ts 复制代码
@Entry
@Component
struct Index {
  @State counter: MyCounter = new MyCounter(1)

  build() {
    Column() {
      Text(this.counter.value.toString())
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
      Button('increase', { type: ButtonType.Capsule }).onClick(() => {
        this.counter.value++
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

对于 class 或 Object,嵌套属性的赋值观察不到

ts 复制代码
export class MyCounter {
  son: MyCounterSon;

  constructor(son: MyCounterSon) {
    this.son = son
  }
}

export class MyCounterSon {
  value: number = 1

  constructor(value: number) {
    this.value = value
  }
}
ts 复制代码
@Entry
@Component
struct Index {
  @State counter: MyCounter = new MyCounter(new MyCounterSon(2))

  build() {
    Column() {
      Text(this.counter.son.value.toString())
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
      Button('increase', { type: ButtonType.Capsule }).onClick(() => {
        this.counter.son.value++
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

this.counter.son.value++ 是嵌套属性的赋值,它无法被观察到,所以点击按钮是不会更新 UI 的。

对于 array,数组项中属性的赋值观察不到。

ts 复制代码
@State counter: MyCounter[] = [
  new MyCounter(new MyCounterSon(1)),
  new MyCounter(new MyCounterSon(2))
]

同样,执行自增,虽然他实际的值会变,但是它也是不能被观察到的,自然就不会引起 UI 的刷新。

ts 复制代码
this.counter[0].son.value++

当装饰的变量类型为 Map,Date,Set 时,可以观察到其整体的赋值,同时也可以通过相应的接口更新其值,比如 Map 的 set,Date 的 setDate,Set 的 add 等。

ts 复制代码
@Entry
@Component
struct Index {
  @State countMap: Map<string, number> = new Map([
    ['key1', 1],
    ['key2', 2]
  ])

  build() {
    Column() {
      Text(this.countMap.get('key1')?.toString())
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
      Button('increase', { type: ButtonType.Capsule }).onClick(() => {
        this.countMap.set('key1', 8)
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

@State 支持联合类型和 undefined 和 null

ts 复制代码
@State count: number | undefined = 1

@Prop

@Prop 装饰的变量和父组件建立单向的同步关系,它允许在本地修改,但修改后的变化不会同步回父组件,不能在 @Entry 装饰的自定义组件中使用。

ts 复制代码
@Component
export struct SonComponent {
  @Prop msg: string = ''

  build() {
    Text(this.msg).fontSize(50)
  }
}
ts 复制代码
@Entry
@Component
struct Index {
  @State parentMsg: string = 'hello huawei'

  build() {
    Column() {
      SonComponent({ msg: this.parentMsg })
      Button('change', { type: ButtonType.Capsule }).onClick(() => {
        this.parentMsg = 'be far ahead'
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

对于 Object 或 class 复杂类型,只能观察到第一层的属性变化,不能观察到第二层的属性变化。

ts 复制代码
export class Parent {
  name: string
  child: Child

  constructor(name: string, child: Child) {
    this.name = name;
    this.child = child;
  }
}

export class Child {
  name: string

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

创建一个子组件

ts 复制代码
@Component
export struct SonComponent {
  @Prop child: Child

  build() {
    Column() {
      Text(`SonComponent -> ${this.child.name}`).fontSize(20)
    }
  }
}

将父组件的状态变量传递到子组件

ts 复制代码
@Entry
@Component
struct Index {
  @State parentData: Parent = new Parent('parent_name', new Child('son_name'))

  build() {
    Column() {
      Text()
      Text(`ParentComponent -> ${this.parentData.child.name}`).fontSize(20)
      SonComponent({ child: this.parentData.child })
      Button('Change', { type: ButtonType.Capsule }).onClick(() => {
        // 无法被观察到
        this.parentData.child.name = 'huawei'
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

执行后发现,点击按钮没有引起 UI 更新,这是第二层属性的变化,无论是父组件还是子组件都无法观察到。由于只能观察到第一层属性的赋值,所以我们可以这样更新值。

ts 复制代码
this.parent.child = new Child('huawei')

如果不想这么处理,就希望能跨层级属性赋值的话,就应该使用 @Observed 和 @ObjectLink 了。

如果需要在嵌套对象或数组的场景中进行双向数据同步,就可以使用 @ObjectLink 和 @Observed,被 @Observed 装饰就可以被观察到属性的变化。子组件中 @ObjectLink 装饰的状态变量用于接收 @Observed 装饰的类实例,沿用并修改上面的例子,使其点击按钮后修改的值能被观察到。

ts 复制代码
@Observed
export class Parent {
  name: string
  child: Child

  constructor(name: string, child: Child) {
    this.name = name;
    this.child = child;
  }
}

@Observed
export class Child {
  name: string

  constructor(name: string) {
    this.name = name;
  }
}
ts 复制代码
@Component
export struct SonComponent {
  @ObjectLink child: Child

  build() {
    Column() {
      Text(`SonComponent -> ${this.child.name}`).fontSize(20)
    }
  }
}
ts 复制代码
@Entry
@Component
struct Index {
  @State parentData: Parent = new Parent('parent_name', new Child('son_name'))

  build() {
    Column() {
      Text()
      Text(`ParentComponent -> ${this.parentData.child.name}`).fontSize(20)
      SonComponent({ child: this.parentData.child })
      Button('Change', { type: ButtonType.Capsule }).onClick(() => {
        this.parentData.child.name = 'huawei'
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

@Link 装饰的变量和父组件建立双向的同步关系,子组件可以修改值,修改后的变化也会同步回父组件,同样,@Link 装饰器不能在 @Entry 装饰的自定义组件中使用。

ts 复制代码
@Component
export struct SonComponent {
  @Link parentData: number[]

  build() {
    Column() {
      Text(`SonComponent: ${this.parentData}`).fontSize(20)
      Button('change', { type: ButtonType.Capsule }).onClick(() => {
        this.parentData = [7, 8, 9]
      })
    }
  }
}
ts 复制代码
@Entry
@Component
struct Index {
  @State parentData: number[] = [1, 2, 3]

  build() {
    Column() {
      SonComponent({ parentData: this.parentData })
      Text(`ParentComponent: ${this.parentData}`).fontSize(20)
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

点击子组件的 Button,父组件的 Text 也会跟着刷新。不同的是,@Link 装饰的变量是禁止在本地初始化的,会报错。

@Provide 和 @Consume

@Prop 和 @Link 只能用于父与子组件之间的数据同步,而 @Provide 和 @Consume 可以实现跨层级,与后代组件的双向数据同步。在祖先组件中使用 @Provide 将状态变量提供给后代组件,在后代组件中使用 @Consume 去消费这个状态变量。

ts 复制代码
@Component
export struct SonComponent {
  @Consume parentData: number[]

  build() {
    Column() {
      Text(`SonComponent: ${this.parentData}`).fontSize(20)
      Button('change', { type: ButtonType.Capsule }).onClick(() => {
        this.parentData = [7, 8, 9]
      })
    }
  }
}
ts 复制代码
@Entry
@Component
struct Index {
  @Provide parentData: number[] = [1, 2, 3]

  build() {
    Column() {
      //无需传递变量
      SonComponent()
      Text(`ParentComponent: ${this.parentData}`).fontSize(20)
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

@Consume 装饰的变量禁止本地初始化,@Provide 装饰的状态变量对其所有后代组件都可用,我们不需要在组件之间传递变量,可以通过相同的变量名或别名绑定,例如:

ts 复制代码
@Provide parentData: number[] = [1, 2, 3]
@Consume parentData: number[]
ts 复制代码
@Provide('hello') parentData: number[] = [1, 2, 3]
@Consume('hello') ChildData: number[]

应用的状态

LocalStorage

LocalStorage 是页面级的 UI 状态存储,通过 @Entry 装饰器接收的参数可以在页面内共享同一个 LocalStorage 实例,它有两个相关的装饰器 @LocalStorageProp 和 @LocalStorageLink,这俩装饰器与 LocalStorage 中给定属性分别是单向和双向的同步关系。

被 @Component 装饰的组件最多可以访问一个 LocalStorage 实例和 AppStorage,未被 @Entry 装饰的组件不可被独立分配 LocalStorage 实例,只能接受父组件通过 @Entry 传递来的 LocalStorage 实例。一个 LocalStorage 实例在组件树上可以被分配给多个组件。

ts 复制代码
let para: Record<string, number> = { 'Key1': 888 };
let storage: LocalStorage = new LocalStorage(para);
storage.setOrCreate('Key2', 'hello');


@Entry(storage)
@Component
struct Index {
  @LocalStorageLink('Key2') data: string = '';

  build() {
    Column() {
      Text()
      Text(`LocalStorage: ${this.data}`).fontSize(20)
      Button('Change', { type: ButtonType.Capsule }).onClick(() => {
        this.data = 'huawei'
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

AppStorage

AppStorage 是应用全局的 UI 状态存储,在应用启动时创建,它也有两个相关的装饰器 @StorageProp 和 @StorageLink,这俩装饰器与 AppStorage 中给定属性分别是单向和双向的同步关系。

ts 复制代码
AppStorage.setOrCreate('Key', 'hello');


@Entry
@Component
struct Index {
  @StorageProp('Key') data: string = '';

  build() {
    Column() {
      Text()
      Text(`AppStorage: ${this.data}`).fontSize(20)
      Button('Change', { type: ButtonType.Capsule }).onClick(() => {
        this.data = 'huawei'
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

PersistentStorage

如果想在应用退出后再次启动依然能保存对应的 UI 状态,就可以使用 PersistentStorage。PersistentStorage 将选定的 AppStorage 属性保留在设备磁盘上。

UI 和业务逻辑不直接访问 PersistentStorage 中的属性,所有属性访问都是对 AppStorage 的访问,AppStorage 中的更改会自动同步到 PersistentStorage。

ts 复制代码
PersistentStorage.persistProp('Key', new User('8', 'Jack'));
//通过 AppStorage 获取值
AppStorage.get<User>('key')

@Entry
@Component
struct Index {
  @StorageLink('Key') data: User = new User('0', '0');

  build() {
    Column() {
      Text()
      Text(`PersistentStorage: ${JSON.stringify(this.data)}`).fontSize(20)
      Button('Change', { type: ButtonType.Capsule }).onClick(() => {
        this.data = new User('10', 'Tom')
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

其他状态

@Watch

@Watch 用于监听状态变量的变化,当状态变量变化时,@Watch 的回调方法将被调用。需要注意地是,在第一次初始化的时候,@Watch 装饰的方法不会被调用,只有在后续状态改变时,才会回调。

ts 复制代码
@State @Watch('isConnected') connect: boolean = false

/**
 *
 * @param changedPropertyName:被 watch 的属性名,此处就是 connect,当多个状态变量绑定同一个 @Watch 方
 * 法时,就可以根据这个属性名去做不同的逻辑处理。
 */
isConnected(changedPropertyName: string) {

}

只有值改变了,@Watch 方法才会被回调,ArkUI 使用的是严格相等(===),举个例子:

ts 复制代码
@Entry
@Component
struct Index {
  @State @Watch('contentChange') content: string = ''

  contentChange(changedPropertyName: string) {
    console.log('changedPropertyName: ' + this.content)
  }

  build() {
    Column() {
      Button('Change', { type: ButtonType.Capsule }).onClick(() => {
        this.content = 'hello harmony'
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

这段代码,无论我点击多少次按钮,contentChange 方法只会被调用一次,因为赋的值没变。

组件冻结

先来看段代码

ts 复制代码
AppStorage.setOrCreate('Info', 'huawei')
ts 复制代码
@Entry
@Component
struct Index {
  @StorageLink('Info') @Watch('contentChange') info: string = ''

  contentChange(changedPropertyName: string) {
    console.log('changedPropertyName: ' + changedPropertyName)
  }

  build() {
    Column() {
      Button('Jump', { type: ButtonType.Capsule }).onClick(() => {
        router.pushUrl({ url: 'pages/MyPage' })
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

点击 Jump Button,跳转到另一个页面。

ts 复制代码
@Entry
@Component
struct MyPage {
  @StorageLink('Info') info: string = ''

  build() {
    Column() {
      Text(this.info).fontSize(20)
      Button('change value', { type: ButtonType.Capsule }).onClick(() => {
        this.info = 'google'
      })
      Button('back', { type: ButtonType.Capsule }).onClick(() => {
        router.back()
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

在 MyPage 页面更新 info 的值,即使 Index 页面处于不可见状态,也会收到相应的更新回调。如果想让页面在非活跃状态下不响应更新,就可以使用冻结功能 freezeWhenInactive。

less 复制代码
@Entry
@Component({ freezeWhenInactive: true })
struct Index {
  ......
}

这样,在 MyPage 页面更新值,Index 页面就不会收到更新通知了,只有当点击 back 按钮返回 Index 页面时,Index 页面才会收到更新回调。

相关推荐
Random_index3 小时前
#Uniapp篇:支持纯血鸿蒙&发布&适配&UIUI
uni-app·harmonyos
鸿蒙自习室6 小时前
鸿蒙多线程开发——线程间数据通信对象02
ui·harmonyos·鸿蒙
SuperHeroWu78 小时前
【HarmonyOS】鸿蒙应用接入微博分享
华为·harmonyos·鸿蒙·微博·微博分享·微博sdk集成·sdk集成
zhangjr057511 小时前
【HarmonyOS Next】鸿蒙实用装饰器一览(一)
前端·harmonyos·arkts
诗歌难吟46418 小时前
初识ArkUI
harmonyos
SameX18 小时前
HarmonyOS Next 设备安全特性深度剖析学习
harmonyos
郭梧悠19 小时前
HarmonyOS(57) UI性能优化
ui·性能优化·harmonyos
郝晨妤1 天前
鸿蒙原生应用开发元服务 元服务是什么?和App的关系?(保姆级步骤)
android·ios·华为od·华为·华为云·harmonyos·鸿蒙
Peace*1 天前
HarmonyOs鸿蒙开发实战(16)=>沉浸式效果第一种方案一窗口全屏布局方案
harmonyos·鸿蒙·鸿蒙系统
howard20051 天前
鸿蒙实战:页面跳转传参
harmonyos·跳转·router·传参