HarmonyOS——状态管理(V1稳定&不全乎版)
LocalStorage是ArkTS为构建页面级别状态变量提供存储的内存内的“数据库”,而先前的状态变量基本都是在组件内或者组件之间的。其中一个@Entry装饰的@component最多只能访问一个LocalStorage实例,未被装饰的组件不可独立分配LocalStorage,只能接收从父组件中传递来的实例LocalStorage中的所有属性都是可变的LocalStorage根据@compone
一、前言
在我们进行界面的开发中,常常需要与用户进行一些交互,以达到更好的操作体验。
而要想达到这种效果,UI界面的某些属性就不能是一成不变的,而应该是一个状态变量,而一个项目中可能有成千上万的状态变量,我们就需要对其进行分类和管理,就是状态管理。
在声明式UI编程框架中,UI是程序状态的运行结果,就像是一个函数,你给它一个固定的值,它会返回给你一个固定的结果。但倘若你给的值在变化,但框架返回的UI也会跟着变化,就达到了我们需要的动态的一个效果。

上面的示例中,用户与应用程序的交互(按钮的点击)触发了文本状态变更,状态变更引起了UI渲染,UI从“Hello World”变更为“Hello ArkUI”。
二、管理组件拥有的状态
1. @State装饰器——组件内部状态
特点:
- 在声明时必须指定其类型和本地初始化。初始化也可选择使用命名参数机制从父组件完成初始化。
- 可以与子组件中的状态变量建立数据传递
- 生命周期与组件生命周期一致
- 无法观测到复杂结构的状态变化,例如二维数组或者对象的属性中包含对象,而相应的解决方法在后文中会提到。
初始化

2. @Prop装饰器——父子单向同步 @Link装饰器——父子双向同步
特点
- 子组件中@Prop装饰的变量在本地的改动并不会同步回父组件,而@Link装饰的变量则可以;但在父组件@State装饰的变量的修改则回同步至子组件中@Prop和@Link装饰的变量,分别为单向绑定和双向绑定
- @Link和@Prop都不能在@Entry装饰的组件中使用
- @Link装饰的变量不允许本地初始化 而@Prop则可以
@Prop的初始化

@Link的初始化

- 使用示例
@Entry
@Component
struct Parent {
@State sourceNumber: number = 0;
build() {
Column() {
Text(`父组件的sourceNumber:` + this.sourceNumber)
Child({ sourceNumber1: this.sourceNumber ,sourceNumber2:this.sourceNumber})
Button('父组件更改sourceNumber')
.onClick(() => {
this.sourceNumber++;
})
}
.width('100%')
.height('100%')
}
}
@Component
struct Child {
@Link sourceNumber1: number;
@Prop sourceNumber2:number;
build() {
Column() {
Text(`@Link子组件的sourceNumber:` + this.sourceNumber1.toString())
Text(`@Prop子组件的sourceNumber:` + this.sourceNumber2.toString())
Button('@Link子组件更改sourceNumber')
.onClick(() => {
this.sourceNumber1++;
})
Button('@Prop子组件更改sourceNumber')
.onClick(() => {
this.sourceNumber2++;
})
}
}
}

点击第三个按钮:
点击第二个按钮:
点击第一个按钮
3. @Observed装饰器和@ObjectLink装饰器:嵌套类对象属性变化
用途
- 上文所述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这时就需要使用@Observed/@ObjectLink装饰器。
特点
- 被@Observed装饰的Class可以观察到其属性的变化
- @Observed装饰的变量不能用于@Entry装饰的组件中,而且不允许初始化;装饰的变量类型必须为@Observed装饰的class
特别说明
@ObjectLink装饰的变量不能被赋值,如果要使用赋值操作,请使用@Prop。
初始化

装饰器说明
| @Observed类装饰器 | 说明 |
|---|---|
| 装饰器参数 | 无 |
| 类装饰器 | 装饰class。需要放在class的定义前,使用new创建类对象。 |
| @ObjectLink变量装饰器 | 说明 |
|---|---|
| 装饰器参数 | 无 |
| 允许装饰的变量类型 | 必须为被@Observed装饰的class实例,必须指定类型。不支持简单类型,可以使用@Prop。支持继承Date、Array的class实例,API11及以上支持继承Map、Set的class实例。示例见观察变化。API11及以上支持@Observed装饰类和undefined或null组成的联合类型,比如ClassA |
| 被装饰变量的初始值 | 不允许 |
使用方式
假设我们现在需要使用ForEach显示出一个number类型的二维数组中每一个值,并且当我们改变其中的值时,UI随我们的改变刷新
- 因为@Observed为类装饰器,首先我们需要包装一下number这个类型,然后再用@Observed装饰。
@Observed
class Num{
a:number
constructor(a:number) {
this.a = a
}
change(num:number){
this.a = num
}
}
此时我们number类型的二维数组就变成了Num类型的二位数组
@State mes:Num[][]=[[new Num(0),new Num(0),new Num(0)],[new Num(0),new Num(0),new Num(0)]]
- 因为@ObjectLink只能接受@Observed装饰的class的实例,我们最好设计自定义组件来单独渲染每一个数组项
@Component
struct Child{
@Link mess:Num[][]
build() {
Column(){
//Arra({num:this.mess[0]})
ForEach(this.mess,(item:Num[],index:number)=>{
ForEach(item,(it:Num)=>{
Arra({num:it})
})
})
}
}
}
@Component
struct Arra{
@ObjectLink num:Num
build(){
Column(){
Text(this.num.a.toString())
.fontSize(25)
}
}
}
在这里我先用Child组件接收父组件传递来的源数据,再在Child组件中用ForEach循环渲染每一个Arra子组件
-
初始状态如下

-
现在我们修改数组中的第一项,即[0][0]项

可以看到对应项发生了我们想要的变化,对应子组件进行了重新渲染。 -
若我们使用@Link或@Prop装饰器,则会因观察不到相应变化而不会触发UI的重新渲染,源码如下,大家有兴趣可以自己试一下。
@Observed
class Num{
a:number
constructor(a:number) {
this.a = a
}
change(num:number){
this.a = num
}
}
@Component
struct Child{
@Link mess:Num[][]
build() {
Column(){
//Arra({num:this.mess[0]})
ForEach(this.mess,(item:Num[],index:number)=>{
ForEach(item,(it:Num)=>{
Arra({num:it})
})
})
}
}
}
@Component
struct Arra{
@ObjectLink num:Num
build(){
Column(){
Text(this.num.a.toString())
.fontSize(25)
}
}
}
@Entry
@Component
struct Index {
@State i: number =1
@State mes:Num[][]=[[new Num(0),new Num(0),new Num(0)],[new Num(0),new Num(0),new Num(0)]]
@State mess:Array<Num> = this.mes[0]
build() {
Column() {
Row(){
Button("修改")
.onClick(()=>{
let a = new Num(1000);
this.mes[0][0].change(1000)
})
}
Child({mess:this.mes})
}
.height('100%')
.width('100%')
}
}
三、管理应用拥有的状态
LocalStorage:页面级UI状态存储
概述
- LocalStorage是ArkTS为构建页面级别状态变量提供存储的内存内的“数据库”,而先前的状态变量基本都是在组件内或者组件之间的。
- 其中一个@Entry装饰的@component最多只能访问一个LocalStorage实例,未被装饰的组件不可独立分配LocalStorage,只能接收从父组件中传递来的实例
- LocalStorage中的所有属性都是可变的
- LocalStorage根据@component装饰子组件同步类型不同,提供了 @LocalStorageProp 和 @LocalStorageLink两个装饰器
这两个装饰器的同步方式与上文中@Porp与@Link一致,在此仅对@LocalStorageLink进行简单介绍
特点
- LocalStorage一旦被创建,命名属性的类型不可更改,后续使用set方法时传递的参数必须保证类型一致
- LocalStorage为页面级储存,可以通过在UIAbility中创建相应LocalStorage实例,实现多个视图(页面)共用一个LocalStorage实例
初始化

使用规则
我们通过@LocalStorageLink来建立UI与LocalStorage之间的联系
使用@LocalStorageProp(key)/@LocalStorageLink(key)装饰组件内的变量,key标识了LocalStorage的属性。
当组件初始化时 @LocalStorageLink(key)会根据对应的key来绑定LocalStorage中对应的属性。但key不一定存在,所以本地的初始化就十分有必要了,当key不存在时,就进行本地初始化

创建示例
//在这里我们储存一个number类型的页面级变量
//对应键值为“PropA”
para:Record<string, number> = { 'PropA': 47 };
//通过para创建new出LocalStorage实例
storage: LocalStorage = new LocalStorage(this.para);
示例:多视图共享同一LocalStorage实例
- 我们首先需要再在UIAbility中创建LocalStorage示例,并将其传入当前Stage模型
export default class EntryAbility extends UIAbility {
//创建LocalStorage示例
para:Record<string,number> = {"PropA":20}
storage:LocalStorage = new LocalStorage(this.para)
onWindowStageCreate(windowStage: window.WindowStage): void {
//使用loadContent方法将LocalStorage实例传入Stage模型
windowStage.loadContent('pages/LocalStorage_1', this.storage);
}
}
- 在所需页面中使用getShared方法获得对应键值的实例,若为未找到键值则返回undefined
let storage = LocalStorage.getShared()
@Entry(storage)
@Component
struct Page {
@LocalStorageLink('PropA') propA: number = 2;
}
此时@LocalStorageLink装饰的propA便于UIAbility中的LocalStorage实例相绑定
第二个页面同理,使用getShared方法来获得实例并通过装饰器与其绑定。
- 我们再使用router类实现页面间的跳转,便于我们查看两个页面是否为同一LocalStorage
- 页面1
// index.ets
import { router } from '@kit.ArkUI';
// 通过getShared接口获取stage共享的LocalStorage实例
let storage = LocalStorage.getShared()
@Entry(storage)
@Component
struct Index {
// can access LocalStorage instance using
// @LocalStorageLink/Prop decorated variables
@LocalStorageLink('PropA') propA: number = 0;
build() {
Row() {
Column() {
Text(`${this.propA}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
Button("To Page")
.onClick(() => {
this.getUIContext().getRouter().pushUrl({
url: 'pages/LocalStorage_2'
})
})
}
.width('100%')
}
.height('100%')
}
}
- 页面2
// Page.ets
import { router } from '@kit.ArkUI';
let storage = LocalStorage.getShared()
@Entry(storage)
@Component
struct Page {
@LocalStorageLink('PropA') propA: number = 2;
build() {
Row() {
Column() {
Text(`${this.propA}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
Button("Change propA")
.onClick(() => {
this.propA = 100;
})
Button("Back Index")
.onClick(() => {
this.getUIContext().getRouter().back()
})
}
.width('100%')
}
}
}
效果如下所示

这样我们就实现了多个页面共享同一LocalStorage实例
四、@Watch:状态变量更改通知
概述
- @Watch用于对状态变量的监听,当状态变量改变时,@Watch相绑定的回调函数将被调用。
值得注意的是@Watch在这里使用的是严格的“===”判断,若类型改变也会触发回调函数
装饰器说明
| @Watch补充变量装饰器 | 说明 |
|---|---|
| 装饰器参数 | 必填。常量字符串,字符串需要有引号。是(string) => void自定义成员函数的方法的引用。 |
| 可装饰的自定义组件变量 | 可监听所有装饰器装饰的状态变量。不允许监听常规变量。 |
| 装饰器的顺序 | 建议@State、@Prop、@Link等装饰器在@Watch装饰器之前。 |
使用示例
假设我们现在要对@state装饰的变量k进行监听@state k:number = 0;
以下为监听回调定义,我们需要通过@Watch装饰器来绑定监听回调
onCountUpdated(): void {
console.log("状态值改变 组件重新渲染!")
}
绑定语法如下
@State @Watch('onCountUpdated') k:number = 0
当k值改变时便会在日志中打印 :“状态值改变 组件重新渲染!”
五、小结
在状态管理中,我对其中AppStorage,@Provide装饰器和@Consume装饰器的理解还不是很清晰,故在此文没有提及,等我理解透彻后定会卷土重来!
更多推荐
所有评论(0)