一、前言

在我们进行界面的开发中,常常需要与用户进行一些交互,以达到更好的操作体验。

而要想达到这种效果,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随我们的改变刷新


  1. 因为@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)]]
  1. 因为@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子组件

  1. 初始状态如下
    在这里插入图片描述

  2. 现在我们修改数组中的第一项,即[0][0]项
    在这里插入图片描述
    可以看到对应项发生了我们想要的变化,对应子组件进行了重新渲染。

  3. 若我们使用@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实例

  1. 我们首先需要再在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);
  }
 }
  1. 在所需页面中使用getShared方法获得对应键值的实例,若为未找到键值则返回undefined
let storage = LocalStorage.getShared()

@Entry(storage)
@Component
struct Page {
  @LocalStorageLink('PropA') propA: number = 2;
}

此时@LocalStorageLink装饰的propA便于UIAbility中的LocalStorage实例相绑定

第二个页面同理,使用getShared方法来获得实例并通过装饰器与其绑定。

  1. 我们再使用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装饰器的理解还不是很清晰,故在此文没有提及,等我理解透彻后定会卷土重来!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐