【鸿蒙HarmonyOS】全网最全面的状态管理!

6.状态管理(V1)

1.概述

1.基本概念

前文的描述中,我们构建的页面多为静态界面。如果希望构建一个动态的、有交互的界面,就需要引入“状态”的概念。

图1 效果图

上面的示例中,用户与应用程序的交互触发了文本状态变更,状态变更引起了UI渲染,UI从“Hello World”变更为“Hello ArkUI”。

在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。

自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。 下图展示了State和View(UI)之间的关系。

  • View(UI):UI渲染,指将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面。
  • State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。

在阅读状态管理文档前,开发者需要对UI范式基本语法有基本的了解。建议提前阅读:基本语法概述声明式UI描述自定义组件-创建自定义组件

名词介绍

  • 状态变量:被状态装饰器装饰的变量,状态变量值的改变会引起UI的渲染更新。示例:@State num: number = 1,其中,@State是状态装饰器,num是状态变量。
  • 常规变量:没有被状态装饰器装饰的变量,通常应用于辅助计算。它的改变永远不会引起UI的刷新。以下示例中increaseBy变量为常规变量。
  • 数据源/同步源:状态变量的原始来源,可以同步给不同的状态数据。通常意义为父组件传给子组件的数据。以下示例中数据源为count: 1。
  • 命名参数机制:父组件通过指定参数传递给子组件的状态变量,为父子传递同步参数的主要手段。示例:CompA({ aProp: this.aProp })。
  • 从父组件初始化:父组件使用命名参数机制,将指定参数传递给子组件。子组件初始化的默认值在有父组件传值的情况下,会被覆盖。示例:
@Component
  struct MyComponent {
    @State count: number = 0;
    private increaseBy: number = 1;


    build() {
    }
  }


@Entry
  @Component
  struct Parent {
    build() {
      Column() {
        // 从父组件初始化,覆盖本地定义的默认值
        MyComponent({ count: 1, increaseBy: 2 })
      }
    }
  }
  • 初始化子组件:父组件中状态变量可以传递给子组件,初始化子组件对应的状态变量。示例同上。
  • 本地初始化:在变量声明的时候赋值,作为变量的默认值。示例:@State count: number = 0。

说明

当前状态管理的功能仅支持在UI主线程使用,不能在子线程、worker、taskpool中使用。

一句话数据驱动视图,视图通过组件的事件以及方法改变数据,从而引发页面的重新渲染。

2. 状态管理 V1 概述

装饰器总览

ArkUI状态管理V1提供了多种装饰器,通过使用这些装饰器,状态变量不仅可以观察在组件内的改变,还可以在不同组件层级间传递,比如父子组件、跨组件层级,也可以观察全局范围内的变化。根据状态变量的影响范围,将所有的装饰器可以大致分为

  • 管理组件内状态的装饰器:组件级别的状态管理,可以观察同一个组件树上(即同一个页面内)组件内或不同组件层级的变量变化。
  • 管理应用级状态的装饰器:应用级别的状态管理,可以观察不同页面,甚至不同UIAbility的状态变化,是应用内全局的状态管理。

从数据的传递形式和同步类型层面看,装饰器也可分为:

  • 只读的单向传递;
  • 可变更的双向传递。

图示如下,具体装饰器的介绍,可详见管理组件拥有的状态管理应用拥有的状态。开发者可以灵活地利用这些能力来实现数据和UI的联动。

上图中,Components部分的装饰器为组件级别的状态管理,Application部分为应用的状态管理。开发者可以通过@StorageLink/@LocalStorageLink实现应用和组件状态的双向同步,通过@StorageProp/@LocalStorageProp实现应用和组件状态的单向同步。

管理组件拥有的状态,即图中Components级别的状态管理:

  • @State:@State装饰的变量拥有其所属组件的状态,可以作为其子组件单向和双向同步的数据源。当其数值改变时,会引起相关组件的渲染刷新。
  • @Prop:@Prop装饰的变量可以和父组件建立单向同步关系,@Prop装饰的变量是可变的,但修改不会同步回父组件。
  • @Link:@Link装饰的变量可以和父组件建立双向同步关系,子组件中@Link装饰变量的修改会同步给父组件中建立双向数据绑定的数据源,父组件的更新也会同步给@Link装饰的变量。
  • @Provide/@Consume:@Provide/@Consume装饰的变量用于跨组件层级(多层组件)同步状态变量,可以不需要通过参数命名机制传递,通过alias(别名)或者属性名绑定。
  • @Observed:@Observed装饰class,需要观察多层嵌套场景的class需要被@Observed装饰。单独使用@Observed没有任何作用,需要和@ObjectLink、@Prop联用。
  • @ObjectLink:@ObjectLink装饰的变量接收@Observed装饰的class的实例,应用于观察多层嵌套场景,和父组件的数据源构建双向同步。

说明

@Observed/@ObjectLink可以观察嵌套场景,其他的状态变量仅能观察第一层,详情见各个装饰器章节的“观察变化和行为表现”小节。

管理应用拥有的状态,即图中Application级别的状态管理:

  • AppStorage是应用程序中的一个特殊的单例LocalStorage对象,是应用级的数据库,和进程绑定,通过@StorageProp@StorageLink装饰器可以和组件联动。
  • AppStorage是应用状态的“中枢”,将需要与组件(UI)交互的数据存入AppStorage,比如持久化数据PersistentStorage和环境变量Environment。UI再通过AppStorage提供的装饰器或者API接口,访问这些数据。
  • 框架还提供了LocalStorage,AppStorage是LocalStorage特殊的单例。LocalStorage是应用程序声明的应用状态的内存“数据库”,通常用于页面级的状态共享,通过@LocalStorageProp@LocalStorageLink装饰器可以和UI联动。

其他状态管理V1功能

@Watch用于监听状态变量的变化。

$$运算符:给内置组件提供TS变量的引用,使得TS变量和内置组件的内部状态保持同步。

2.管理组件拥有的状态

1.@State-组件内状态

@State 装饰器是管理组件内部状态的
@State装饰的变量,或称为状态变量
但是,并不是状态变量的所有更改都会引起UI的刷新,只有可以被框架观察到的修改才会引起UI刷新。

观察变化注意点:
●当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
●当装饰的数据类型为Object或数组时
○可以观察到自身的赋值的变化
○可以观察到对象属性赋值的变化,即Object.keys(observedObject)返回的所有属性
■注意:嵌套属性的赋值观察不到
○可以观察到数组本身的赋值和添加、删除、更新数组项的变化
■注意:数组项中属性的赋值观察不到


 

2.@Prop-父子单向同步

@Prop 装饰的变量可以和父组件建立单向的同步关系

@Prop 装饰的变量是可变的,但是变化不会同步回其父组件

注意:

  1. 修改父组件数据,会同步更新子组件
  2. 修改子组件@Prop 修饰的数据,子组件 UI 更新,更新后的数据不会同步给父组件
  3. 通过回调函数的方式修改父组件的数据,然后触发@Prop数据的更新

3.@Link-父子双向同步

使用@Link 可以实现父组件和子组件的双向同步:

@Link修饰的状态变量也能监听到浅层的数据变化

4.@Provide与@Consume-后代组件双向同步

将数据传递给后代,和后代的数据进行双向同步

// 写法 1:通过相同的变量名绑定
@Provide a: number = 0;
@Consume a: number;

// 写法 2通过相同的变量别名绑定
@Provide b: number = 0;
@Consume('b') c: number;
interface Person {
  name: string
  age: number
}

@Entry
@Component
  // 顶级组件
struct RootComponent {
  @Provide
  p: Person = {
    name: 'jack',
    age: 18
  }
  @Provide
  food: string = '西兰花炒蛋'

  build() {
    Column() {
      Text('顶级组件')
        .fontSize(30)
        .fontWeight(900)
      Text(JSON.stringify(this.p))
      Text(this.food)

      ParentComponent()
    }
    .padding(10)
    .height('100%')
    .backgroundColor('#ccc')
    .width('100%')
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 100 })
  }
}


@Component
  // 二级组件
struct ParentComponent {
  // 编写 UI
  build() {
    Column({ space: 20 }) {
      Text('我是二级组件')
        .fontSize(22)
        .fontWeight(900)

      SonComponent()
    }
    .backgroundColor('#a6c398')
    .alignItems(HorizontalAlign.Center)
    .width('90%')
    .margin({ top: 50 })
    .padding(10)
    .borderRadius(10)

  }
}

@Component
  // 内层组件
struct SonComponent {
  // 相同变量名
  @Consume p: Person
  @Consume('food') f: string

  // 编写 UI
  build() {
    Column({ space: 20 }) {
      Text('我是内层组件')
        .fontSize(20)
        .fontWeight(900)
      Text(JSON.stringify(this.p))
      Text(this.f)
      Button('修改')
        .onClick(() => {
          this.p.name = 'rose'
          this.p.age = 99
          this.f += '!'
        })

    }
    .backgroundColor('#bf94e4')
    .alignItems(HorizontalAlign.Center)
    .width('90%')
    .margin({ top: 50 })
    .padding(10)
    .borderRadius(10)

  }
}
5.@Observed 与 ObjectLink

在线文档

问题:@State装饰器仅能观察到第一层的变化,对于多层嵌套的情况(比如对象数组等),他们的第二层属性变化是无法观察到的
解决:@Observed&@ObjectLink可以解决上述问题
@Observed&@ObjectLink作用:用于父子组件场景下,在涉及嵌套对象或数组中进行双向数据同步
●使用new创建被@Observed装饰的类,可以被观察到属性的变化
注意:ObjectLink修饰符不能用在Entry修饰的组件中
 

//【固定】1. @Observed 装饰类
@Observed
class ClassA{
  // 略
}

//2. 父组件
@Component 
struct ParentCom{
  // 【非固定写法】根据需求来定义数据
  @State 变量名: ClassA[] = [ new ClassA(),new ClassA(), ]
  @State 变量名:ClassA = new ClassA()  
  @State 变量名: ClassA[][] = [[new ClassA(),new ClassA()],[new ClassA(),new ClassA()] ]
  
}


// 【固定】3. 子组件
@Component 
struct ChildCom{
  @ObjectLink 变量名: ClassA
}

使用步骤:

  • interface 改为 类 class 并使用 @Observed 修饰
  • 通过 new 的方式完成数据的创建(通过 new 的方式来监测数据的改变)
  • 状态修饰符改为 @ObjectLink
    • 在父组件修改数据:不需要 splice
    • 在子组件修改数据
interface Person {
  id: number
  name: string
  age: number
}


@Entry
@Component
struct ObservedAndLink {
  @State personList: Person[] = [
    {
      id: 1,
      name: '张三',
      age: 18
    },
    {
      id: 2,
      name: '李四',
      age: 19
    },
    {
      id: 3,
      name: '王五',
      age: 20
    }
  ]

  build() {
    Column({ space: 20 }) {
      Text('父组件')
        .fontSize(30)
      List({ space: 10 }) {
        ForEach(this.personList, (item: Person, index: number) => {
          ItemCom({
            info: item,
            addAge: () => {
              item.age++
              this.personList.splice(index, 1, item)
            }
          })
        })
      }

    }
    .backgroundColor('#cbe69b')
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

@Component
struct ItemCom {
  @Prop info: Person
  addAge = () => {

  }

  build() {
    ListItem() {
      Row({ space: 10 }) {
        Text('姓名:' + this.info.name)
        Text('年龄:' + this.info.age)
        Blank()
        Button('修改数据')
          .onClick(() => {
            this.addAge()
          })
      }
      .backgroundColor(Color.Pink)
      .padding(10)
      .width('100%')
    }
  }
}

@Observed
class Person {
  id: number
  name: string
  age: number

  constructor(id: number, name: string, age: number) {
    this.id = id
    this.name = name
    this.age = age
  }
}


@Entry
@Component
struct ObservedAndLink {
  @State personList: Person[] = [
    new Person(1, '张三', 18),
    new Person(2, '李四', 18),
    new Person(3, '王五', 18),
  ]

  build() {
    Column({ space: 20 }) {
      Text('父组件')
        .fontSize(30)
      List({ space: 10 }) {
        ForEach(this.personList, (item: Person, index: number) => {
          ItemCom({
            info: item,
            addAge: () => {
              item.age++
            }
          })
        })
      }

    }
    .backgroundColor('#cbe69b')
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

@Component
struct ItemCom {
  @ObjectLink info: Person
  addAge = () => {

  }

  build() {
    ListItem() {
      Row({ space: 10 }) {
        Text('姓名:' + this.info.name)
        Text('年龄:' + this.info.age)
        Blank()
        Button('修改数据')
          .onClick(() => {
            this.addAge()
            // 这种写法也可以
            // this.info.age++
          })
      }
      .backgroundColor(Color.Pink)
      .padding(10)
      .width('100%')
    }
  }
}

3.管理应用拥有的状态

TIP

关于应用状态相关的内容需要使用模拟器或真机调试

到目前为止咱们学习的各种装饰器可以实现在一个组件树上共享状态。如果开发者要实现应用级的,或者多个页面的状态数据共享,就需要用到应用级别的状态管理的概念。ArkTS根据不同特性,提供了多种【应用状态管理】的能力:

  • LocalStorage:页面级UI状态共享(内存-非持久化状态,退出消失)
  • AppStorage:应用内状态-多 UIAbility 共享(内存-非持久化状态,退出消失)
  • PersistentStorage:全局持久化状态(磁盘-持久化状态,退出应用 数据同样存在)
  • Environment:应用程序运行的设备的环境参数(可读不可写)
    • 暗黑模式
    • 语言模式
    • 网络环境
1. LocalStorage

UIAbility内的状态管理

LocalStorage 是页面级的UI状态存储,通过 @Entry 装饰器接收的参数可以在页面内共享同一个 LocalStorage 实例。 LocalStorage 也可以在 UIAbility 内,页面间共享状态。

1.用法

import { router } from '@kit.ArkUI'

// class UserInfo {
//   name: string = ''
//   age: number = 0
// }
export interface UserInfo{
  name: string
  age: number
}

let userData: Record<string, UserInfo> = {
  'user': {
    name: 'jack',
    age: 18
  }
}
// ✨✨1. 实例化LocalStorage
export const storage = new LocalStorage(userData)

//✨✨ 2. 将LocalStorage实例设置给@Entry的参数
@Entry(storage)
@Component
struct Index {
  //✨✨ 3. 页面使用LocalStorage中的数据
  // 3.1 使用LocalStorageLink进行双向同步
  @LocalStorageLink('user')
  user: UserInfo = {
    name: 'rose',
    age: 18
  }
  // 3.2 使用LocalStorageProp进行单向同步
  // @LocalStorageProp('user')
  // user: UserInfo = {
  //   name: 'rose',
  //   age: 18
  // }

  build() {
    Column({ space: 15 }) {
      Text('我是首页:')
        .fontSize(40)
      // ✨✨✨4. UI使用LocalStorage的数据
      Text(JSON.stringify(this.user)).fontSize(20)

      Button('点击+1')
        .onClick(() => {
          this.user.age++
        })
      ChildA()
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

@Component
struct ChildA {
  @LocalStorageLink('user')
  user: UserInfo = {
    name: '',
    age: 0
  }

  build() {
    Column() {
      Text('我是子组件A')
        .fontSize(30)
      Button(this.user.name)
        .onClick(() => {
          this.user.name += '!'
        })
    }
    .backgroundColor(Color.Orange)
    .width('100%')

  }
}
2.AppStorage

模块页面数据共享

LocalStorage是针对UIAbility的状态共享- 一个UIAbility有多个页面,这些页面都可以通过 LocalStorage 共享数据

一个应用可能有若干个UIAbility,如果要在多个 UIAbility 共享数据,就可以使用 AppStorage

class User {
  name: string = ''
  age: number = 0
}

// ✨✨1. 给AppStorage设置初始数据
AppStorage.setOrCreate<User>('user', { name: 'jack', age: 18 })

@Entry
@Component
struct AppStorageUse01 {
  //✨✨ 2. 组件内获取AppStorage的数据
  @StorageLink('user')
  user: User = {
    name: '',
    age: 0
  }

  build() {
    Column({ space: 15 }) {
      Text(JSON.stringify(this.user))
        .fontSize(20)
      //✨✨ 3. 使用AppStorage数据
      Button('点击修改名字')
        .fontSize(30)
        .onClick(() => {
          this.user.name += '!'
        })
     Button('点击修改年龄')
        .fontSize(30)
        .onClick(() => {
          this.user.age++
        })


      Divider()
      ChildA()
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

@Component
struct ChildA {
  @StorageProp('user')
  user: User = {
    name: '',
    age: 0
  }

  build() {
    Column({ space: 15 }) {
      Text('子组件 A:')
        .fontColor(Color.White)
      Text(JSON.stringify(this.user))
     Button('点击修改名字')
        .fontColor(Color.White)
        .onClick(() => {
          this.user.name += '!'
        })

      Button('点击修改年龄')
        .fontColor(Color.White)
        .onClick(() => {
          this.user.age++
        })
    }
    .width('100%')
    .padding(10)
    .backgroundColor(Color.Orange)
  }
}

如用不需要持久化我们可以直接省略第一步

@StorageLink('isShow')

isShow: boolean = true

3.PersistentStorage

状态持久化

模拟器或者真机中查看效果-> 查看路径如下

LocalStorage和AppStorage都是运行时的内存,在应用退出后就没有了,如果要在应用退出后再次启动AppStorage依然能保存选定的结果,这就需要用到PersistentStorage。

用法:

PersistentStorage.PersistProp('属性名', 值)

后续直接通过 AppStorage 的 Api 来获取并修改即可,AppStorage 的修改会自动同步到PersistentStorage中

保存简单类型

number, string, boolean, enum 等简单类型都支持

核心步骤:

  1. 初始化PersistentStorage
  2. 通过 AppStorage 获取并修改数据
  3. 重启应用,检测结果
PersistentStorage.persistProp<string>('info','感觉自己闷闷哒')

@Entry
@Component
struct PersistentStoragePage01 {
  @StorageLink('info')
  info:string=''

  build() {
    Row() {
      Column() {
        Text(this.info)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(()=>{
            this.info+='!'
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

保存复杂类型

接下来看看复杂类型

支持的复杂类型:

  • 可以被JSON.stringify()和JSON.parse()重构的对象。例如Date, Map, Set等内置类型则不支持,以及对象的属性方法不支持持久化。

划重点:自己定义的 class、interface 基本都是支持的

不允许的类型和值有:

  • 不支持嵌套对象(对象数组,对象的属性是对象等)。因为目前框架无法检测AppStorage中嵌套对象(包括数组)值的变化,所以无法写回到PersistentStorage中。
  • 不支持 undefined 和 null
interface FoodInfo{
  name:string
  price:number
}
PersistentStorage.persistProp('foods',[
  {name:'西兰花炒蛋',price:10},
  {name:'猪脚饭',price:15},
  {name:'剁椒鱼头',price:14},
])
1
@Entry
@Component
struct PersistentStoragePage02 {
  @StorageLink('foods')
  foods:FoodInfo[]=[]

  build() {
    Row() {
      Column() {
        Text(JSON.stringify(this.foods))
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
        Button('点击添加新的菜')
          .onClick(() => {
            this.foods.push({
              name:'红烧肉',
              price:20
            })
          })

      }
      .width('100%')
    }
    .height('100%')
  }
}

说在最后:

  • 持久化变量最好是小于2kb的数据,如果开发者需要存储大量的数据,建议使用数据库api

注意点

PersistentStorage和UI实例相关联,持久化操作需要在UI实例初始化成功后(即loadContent传入的回调被调用时)才可以被调用,早于该时机调用会导致持久化失败

export interface MkUser {
  token: string
  nickname: string
  avatar: string
  account: string
}


class Auth {
  KEY: string = 'user'

  initUser(){
    // 设置键为 user 的数据持久化
    PersistentStorage.persistProp<MkUser>(this.KEY,{} as MkUser )
  }

  getUser() {
    return AppStorage.get<MkUser>(this.KEY) || {} as MkUser
  }

  saveUser(user: MkUser) {
    AppStorage.setOrCreate<MkUser>(this.KEY, user)
  }

  removeUser() {
    AppStorage.setOrCreate<MkUser>(this.KEY, {} as MkUser)
  }
}

// 导出实例化对象
export const auth = new Auth()
// 要在onWindowStageCreate之后调用
windowStage.loadContent('pages/Index', (err) => {
    if (err.code) {
      hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
      return;
    }
    // ...省略
    hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
    // 持久化存储用户信息
    auth.initUser()
  });
}
4.Environment

设备环境查询

开发者如果需要应用程序运行的设备的环境参数,以此来作出不同的场景判断,比如多语言,暗黑模式等,需要用到Environment设备环境查询。

使用步骤:

  1. Environment.envProp('languageCode', 默认值); // 将设备信息存入 AppStorage 中,如果读取不到使用默认值
  2. 通过 AppStorage 获取 并使用即可

注:只能读取,无法修改

数据类型

描述

accessibilityEnabled

boolean

获取无障碍屏幕读取是否启用。

colorMode

ColorMode

色彩模型类型:选项为ColorMode.LIGHT: 浅色,ColorMode.DARK: 深色。

fontScale

number

字体大小比例,范围: [0.85, 1.45]。

fontWeightScale

number

字体粗细程度,范围: [0.6, 1.6]。

layoutDirection

LayoutDirection

布局方向类型:包括LayoutDirection.LTR: 从左到右,LayoutDirection.RTL: 从右到左。

languageCode

string

当前系统语言值,取值必须为小写字母, 例如zh。

Environment.envProp("accessibilityEnabled", 'aaaaaaaa')
Environment.envProp("fontScale", 'bbbb')
Environment.envProp("languageCode", "cccc")
Environment.envProp("colorMode", "ddddd")
@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  aboutToAppear(): void {
    console.log('测试','获取无障碍屏幕读取是否启用', AppStorage.get("accessibilityEnabled"))
    console.log("测试",'字体大小比例', AppStorage.get("fontScale"))
    console.log('测试','当前系统语言值', AppStorage.get("languageCode"))
    console.log('测试','色彩模型类型', AppStorage.get("colorMode"))

  }

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}

4.其他状态管理

1.@Watch装饰器:状态变量更改通知

如果开发者需要关注某个状态变量的值改变后,触发一段逻辑的执行,可以使用 @Watch 为状态变量设置回调函数。

注意:

Watch无法单独使用,必须配合状态装饰器,比如@State、@Prop、@Link

@State、@Prop、@Link 等装饰器在 @Watch 装饰之前

@State / @Prop / @Link 其中之一
@Watch('方法名')
info:string=''

方法名(){
  // 数据改变之后会触发
}
/*
 * 1. 定义一个状态变量
 * 2. 在这个状态变量上使用@Watch来修饰
 * 3. 定义一个函数来配合@Watch的使用,至于这个函数的方法体写什么逻辑不固定
 * */

@Entry
@Component
struct Index {

  @State
  @Watch('loadData')
  count:number = 100

  @State
  @Watch('loadData')
  msg:string = 'hello'


  // source参数表示是哪个状态变量触发的这个函数
  // 进来可以用在判断不同的状态变量引起的回调,就处理不同的逻辑
  loadData(source:string){
    AlertDialog.show({message:this.count.toString() + source})
  }

  build() {
    Column() {

      Button('count='+this.count)
        .onClick(()=>{
          // this.count++
          this.msg = 'hi'
        })

    }
    .height('100%')
    .width('100%')
    .backgroundColor(Color.Pink)

  }
}
2.$$语法:内置组件双向同步

$$运算符为系统内置组件提供TS变量的引用,使得TS变量和系统内置组件的内部状态保持同步。

内部状态具体指什么取决于组件。例如,TextInput组件的text参数。

说明

$$还用于@Builder装饰器的按引用传递参数,开发者需要注意两种用法的区别。

使用规则

  • 当前$$支持基础类型变量,以及@State@Link@Prop装饰的变量。
  • 当前$$支持的组件:

组件

支持的参数/属性

起始API版本

Checkbox

select

10

CheckboxGroup

selectAll

10

DatePicker

selected

10

TimePicker

selected

10

MenuItem

selected

10

Panel

mode

10

Radio

checked

10

Rating

rating

10

Search

value

10

SideBarContainer

showSideBar

10

Slider

value

10

Stepper

index

10

Swiper

index

10

Tabs

index

10

TextArea

text

10

TextInput

text

10

TextPicker

selected、value

10

Toggle

isOn

10

AlphabetIndexer

selected

10

Select

selected、value

10

BindSheet

isShow

10

BindContentCover

isShow

10

Refresh

refreshing

8

GridItem

selected

10

ListItem

selected

10

  • $$绑定的变量变化时,会触发UI的同步刷新。

使用示例

TextInput方法的text参数为例:

// xxx.ets
@Entry
  @Component
  struct TextInputExample {
    @State text: string = ''
    controller: TextInputController = new TextInputController()


    build() {
      Column({ space: 20 }) {
        Text(this.text)
        TextInput({ text: $$this.text, placeholder: 'input your word...', controller: this.controller })
          .placeholderColor(Color.Grey)
          .placeholderFont({ size: 14, weight: 400 })
          .caretColor(Color.Blue)
          .width(300)
      }.width('100%').height('100%').justifyContent(FlexAlign.Center)
    }
  }

7.状态管理(V2)

1.概述

为了增强状态管理V1版本的部分能力,例如深度观察、属性级更新等,ArkUI推出状态管理V2供开发者使用。

说明

当前状态管理V2能力相较于状态管理V1能力仍有GAP,请开发者慎重选择。

状态管理V1现状以及V2优点

状态管理V1使用代理观察数据,当创建一个状态变量时,同时也创建了一个数据代理观察者。该观察者可感知代理变化,但无法感知实际数据变化,因此在使用上有如下限制

  • 状态变量不能独立于UI存在,同一个数据被多个视图代理时,在其中一个视图的更改不会通知其他视图更新。
  • 只能感知对象属性第一层的变化,无法做到深度观测和深度监听。
  • 在更改对象中属性以及更改数组中元素的场景下存在冗余更新的问题。
  • 装饰器间配合使用限制多,不易用。组件中没有明确状态变量的输入与输出,不利于组件化。

状态管理V2将观察能力增强到数据本身,数据本身就是可观察的,更改数据会触发相应的视图的更新。相较于状态管理V1,状态管理V2有如下优点:

  • 状态变量独立于UI,更改数据会触发相应视图的更新。
  • 支持对象的深度观测和深度监听,且深度观测机制不影响观测性能。
  • 支持对象中属性级精准更新及数组中元素的最小化更新。
  • 装饰器易用性高、拓展性强,在组件中明确输入与输出,有利于组件化。

v1 与 v2 的本质区别

v1 在每个状态变量被创建时就给它创建了一个代理对象观察数据

(所以每个 UI 都会有一个代理对象观察者,并且 这个 个对象都是独立的)

(@State使用混乱外部可以随意传值)

(无法精准监听对象某一个属性的变化,容易造成性能浪费)

V2 将所有需要响应式的数据放在一起统一使用一个代理对象观察

(这样状态变量便会独立于 UI 之外,每当数据变化所对应的 UI 视图便会更新)

(使用@Local 替代@State 为组件内部状态,无法从外部初始化)

(@ObservedV2 与 @Trace 替代@Observed @ObjectLink 实现数据的精准监听)

装饰器总览

状态管理(V2)提供了一套全新的装饰器。

  • @ObservedV2:@ObservedV2装饰器装饰class,使得被装饰的class具有深度监听的能力。@ObservedV2和@Trace配合使用可以使class中的属性具有深度观测的能力。
  • @Trace:@Trace装饰器装饰被@ObservedV2装饰的class中的属性,被装饰的属性具有深度观测的能力。
  • @ComponentV2:使用@ComponentV2装饰的struct中能使用新的装饰器。例如:@Local、@Param、@Event、@Once、@Monitor、@Provider、@Consumer。
  • @Local:@Local装饰的变量为组件内部状态,无法从外部初始化。
  • @Param:@Param装饰的变量作为组件的输入,可以接受从外部传入初始化并同步。
  • @Once:@Once装饰的变量仅初始化时同步一次,需要与@Param一起使用。
  • @Event:@Event装饰方法类型,作为组件输出,可以通过该方法影响父组件中变量。
  • @Monitor:@Monitor装饰器用于@ComponentV2装饰的自定义组件或@ObservedV2装饰的类中,能够对状态变量进行深度监听。
  • @Provider和@Consumer:用于跨组件层级双向同步。
  • @Computed:计算属性,在被计算的值变化的时候,只会计算一次。主要应用于解决UI多次重用该属性从而重复计算导致的性能问题。
  • !!语法:双向绑定语法糖。

2.管理组件的状态

@ComponentV2:自定义组件

是什么?和@Component装饰器一样,@ComponentV2装饰器用于装饰自定义组件

作用:在@ComponentV2装饰的自定义组件中,开发者仅可以使用全新的状态变量装饰器,包括@Local、@Param、@Once、@Event、@Provider、@Consumer等。

基本用法:

注意:

  1. @ComponentV2装饰器从API version 12开始支持。
  2. 无法同时使用@ComponentV2与@Component装饰同一个struct结构
@Local:组件内状态

是什么?@Local表示组件内部的状态,使得自定义组件内部的变量具有观测变化的能力

作用:状态管理V1使用@State装饰器定义状态变量。但由于@State装饰器能够从外部初始化,因此@State无法准确表达组件内部状态不能被外面修改的语义。

@Local装饰的变量无法从外部初始化,因此必须在组件内部进行初始化。

语法:

注意:

  1. @Local装饰器只能在@ComponentV2装饰的自定义组件中使用
  2. @Local装饰的变量表示组件内部状态,不允许从外部传入初始化。
  3. 当装饰的变量类型为boolean、string、number时,可以观察到对变量赋值的变化。
  4. 当装饰的变量类型为对象时,仅可以观察到对对象整体赋值的变化,无法直接观察到对成员属性赋值的变化

对对象成员属性的观察依赖@ObservedV2和@Trace装饰器

@Entry
@ComponentV2
struct Index {
  build() {
    Column() {
      ChildCom()
    }
    .height('100%')
    .width('100%')
  }
}

interface iPerson {
  name: string;
  toy: iToy
}

interface iToy {
  name: string;
}

@ComponentV2
struct ChildCom {
  @Local count: number = 100
  @Local person: iPerson = { name: '张三', toy: { name: '玩具' } }

  build() {
    Column() {

      Text(this.count.toString())
      Text(this.person.name)
      Text(this.person.toy.name)

      Button('修改count的值')
        .onClick(()=>{
          this.count++
        })

      Button('修改person变量')
        .onClick(()=>{
          this.person={name:'张三2',toy:{name:'玩具2'}}
        })
      Button('修改person.name变量')
        .onClick(()=>{
          // 不会更新UI
          this.person.name = '张三3'
        })

    }
    .height('100%')
    .width('100%')
  }
}
@Observed与@Trace:类属性变化观测

作用:@ObservedV2装饰器与@Trace装饰器用于装饰类以及类中的属性,使得被装饰的类和属性具有深度观测的能力

注意:

  • @ObservedV2装饰器与@Trace装饰器需要配合使用,单独使用@ObservedV2装饰器或@Trace装饰器没有任何作用
  • 未被@Trace装饰的属性用在UI中无法感知到变化,也无法触发UI刷新。
  • @ObservedV2的类实例目前不支持使用JSON.stringify进行序列化。
  • 被@Trace装饰器装饰的属性property变化时,仅会通知property关联的组件进行刷新
  • 在嵌套类中,嵌套类中的属性property被@Trace装饰且嵌套类被@ObservedV2装饰时,才具有触发UI刷新的能力。
@Entry
@ComponentV2
struct Index {
  build() {
    Column() {
      ChildCom()
    }
    .height('100%')
    .width('100%')
  }
}
//1.声明观察类

@ObservedV2
class Person {
  @Trace name: string
  @Trace toy: iToy

  constructor(name: string, toy: iToy) {
    this.name = name
    this.toy = toy
  }
}

@ObservedV2
class iToy {
  @Trace name: string;

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

@ComponentV2
struct ChildCom {
  // 2.使用类
  @Local person: Person = new Person('张三', new iToy('玩具'))
  build() {
    Column() {
      // Text(JSON.stringify(this.person))
      Text(this.person.name)
      Text(this.person.toy.name)

      Button('修改person变量')
        .onClick(() => {
          this.person = new Person('张三2', new iToy('玩具2'))
        })

      Button('修改person.name变量')
        .onClick(() => {
          // 不会更新UI
          this.person.name = '张三3'
          // this.person.toy = { name: '玩具3' }
          this.person.toy.name = '玩具3'
        })

    }
    .height('100%')
    .width('100%')
  }
}
@Param:组件外输入数据

是什么:@Param表示组件从外部传入的状态,使得父子组件之间的数据能够进行同步

注意:

  1. @Param不仅可以接受组件外部输入,还可以接受@Local的同步变化,@Param装饰的变量变化时,会刷新该变量关联的组件。
  2. @Param装饰的变量支持本地初始化,但是不允许在组件内部直接修改变量本身。
  3. @Param装饰的变量在子组件中无法进行修改。但当装饰的变量类型为对象时,在子组件中修改对象中属性是允许的。
  4. 对于复杂类型如类对象,@Param会接受数据源的引用。在组件内可以修改类对象中的属性,该修改会同步到数据源。
@Entry
@ComponentV2
struct Index {
  @Local count: number = 200
  @Local person: Person = new Person('李四', new iToy('奔驰'))

  build() {
    Column() {
      Button('修改count值').onClick(() => {
        this.count++
      })

      Text(this.person.name)
      Text(this.person.toy.name)

      ChildCom({ 
        //1.父组件传值
      count: this.count,
      person: this.person
      })
    }
    .height('100%')
    .width('100%')
  }
}

@ObservedV2
class Person {
  @Trace name: string
  @Trace toy: iToy

  constructor(name: string, toy: iToy) {
    this.name = name
    this.toy = toy
  }
}

@ObservedV2
class iToy {
  @Trace name: string;

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

@ComponentV2
struct ChildCom {
  // 子组件接收
  @Param count: number = 100
  @Param person: Person = new Person('张三', new iToy('玩具'))

  build() {
    Column() {
      Text(this.count.toString())
      Text(this.person.name)
      Text(this.person.toy.name)


      Button('修改person.name变量')
        .onClick(() => {
          // 不会更新UI
          this.person.name = '张三3'
          // this.person.toy = { name: '玩具3' }
          this.person.toy.name = '玩具3'
        })

    }
    .height('100%')
    .width('100%')
  }
}
@Event:规范方法传入

@Event主要配合@Param实现数据的双向同步
由于@Param装饰的变量在本地无法更改,使用@Event装饰器装饰回调方法并调用,可以实现更改数据源的变量,再通过@Local的同步机制,将修改同步回@Param,以此达到主动更新@Param装饰变量的效果。
@Param标志着组件的输入,表明该变量受父组件影响,而@Event标志着组件的输出,可以通过该方法影响父组件。
语法:

@Entry
@ComponentV2
struct Index {
  @Local count: number = 200

  build() {
    Column() {
      Text(this.count.toString())

      ChildCom({
        count: this.count,
        // ✨✨注意:如果changeCount没有使用@Event修饰,这里会报错
        changeCount: () => {
          this.count++
        }
      })
    }
    .height('100%')
    .width('100%')
  }
}


@ComponentV2
struct ChildCom {
  @Param count: number = 100
  @Event changeCount: () => void = () => {
  }

  build() {
    Column() {
      Text(this.count.toString())
        .onClick(()=>{
          this.changeCount()
        })

    }
    .height('100%')
    .width('100%')
  }
}
@Once:初始化同步一次

@Once装饰器仅在变量初始化时接受外部传入值进行初始化,当后续数据源更改时,不会将修改同步给子组件

注意:

  • @Once必须搭配@Param使用,单独使用或搭配其他装饰器使用都是不允许的。
  • @Once与@Param搭配使用时,可以在本地修改@Param变量的值,但不会影响到父组件的状态变量(单独@Param修饰的变量是不允许在本地修改变量值的
@Entry
@ComponentV2
struct Index {
  @Local count: number = 200

  build() {
    Column() {
      Button('修改count值').onClick(() => {
        // 由于ChildCom中的count使用了@Once修饰,因此修改变量值,不会引起ChildCom组件中的count改变
        this.count++
      })
      Text(this.count.toString())

      ChildCom({
        count: this.count
      })
    }
    .height('100%')
    .width('100%')
  }
}


@ComponentV2
struct ChildCom {
  @Param @Once count: number = 100

  build() {
    Column() {
      Text(this.count.toString())
        .onClick(()=>{
          // ✨✨由于使用了@Once修饰 ,可以本地修改 @Param变量的值
          this.count++
        })
    }
    .height('100%')
    .width('100%')
  }
}
@Monitor:监听状态变量变化

@Monitor装饰器支持在@ComponentV2装饰的自定义组件中使用,未被状态变量装饰器@Local、@Param、@Provider、@Consumer、@Computed装饰的变量无法被@Monitor监听到变化。
功能类似@Watch
语法:

@Entry
@ComponentV2
struct Index {
  @Local name: string = "张三";
  @Local age: number = 24;

  @Monitor("name", "age")
  change(monitor: IMonitor) {
    monitor.dirty.forEach((item) => {
      console.log(item, JSON.stringify(monitor.value(item)))
    })
  }

  build() {
    Column() {
      Button("修改name")
        .onClick(() => {
          this.name = "李四";
          this.age++
        })

      Button("修改age")
        .onClick(() => {
          this.age++
        })
    }
  }
}
@Computed:计算属性

@Computed装饰器:计算属性,在被计算的值变化的时候,只会计算一次。主要应用于解决UI多次重用该属性从而重复计算导致的性能问题。
状态变量的变化可以触发其关联@Computed的重新计算。
语法:

@Entry
@ComponentV2
struct Index {
  @Local firstName: string = 'Li';
  @Local lastName: string = 'Hua';
  age: number = 20; // 无法触发Computed

  @Computed
  get fullName() {
    console.info("---------Computed----------");
    return this.firstName + ' ' + this.lastName + this.age;
  }

  build() {
    Column() {
      Text(this.fullName)
      Text(this.fullName)
      Button('changed lastName').onClick(() => {
        this.lastName += 'a';
      })

      Button('changed age').onClick(() => {
        this.age++;  // 无法触发Computed
      })
    }
  }
}

鸿蒙中的状态管理V2和V1有哪些区别?

鸿蒙中状态管理V2版本有哪些装饰器?

你用过状态管理V2吗?

回答思路都是列出V1和V2版本的装饰器对比

V1: @State V2 @Local

@Prop @Link -> @Param @Once

@Observed @ObjectLink -> @ObservedV2 @Trace

@Watch -> @Monitor

@Provide + @Comsume -> @Provider + @Comsumer

V2单独:@Once @Computed @Event

v1中数据共享,有AppStorage和localStroage ,其中AppStroage持久化使用的是PersistenStorage完成的

V2中只有AppStroageV2可以结合PersisitenceV2完成

3.管理应用的状态

1.AppStorageV2: 应用全局UI状态存储

要模拟器或真机才能有用
AppStorageV2是提供状态变量在应用级全局共享的能力,开发者可以通过connect绑定同一个key,进行跨ability的数据共享。
注意:不支持基本类型,需要使用class结合@ObservedV2和@Trace来定义数据
语法:

@ObservedV2
class myNumber {
@Trace  age:number
  constructor(age:number) {
    this.age = age
  }
}

@Entry
@ComponentV2
struct Index {
  @Local count: number = 100
  // connect(数据类型,key,默认数据)
  @Local appStorage:myNumber = AppStorageV2.connect(myNumber,'key',()=>new myNumber(28))!

  build() {
    Column() {
      ChildCom()
      Text('第一行' + this.count)
        .onClick(() => {
          this.count++
        })

      Text('第二行'+this.appStorage.age)
        .onClick(() => {
          this.appStorage.age++
        })
    }
    .height('100%')
    .width('100%')
    .backgroundColor(Color.Pink)

  }
}
Button('get')
.onClick(()=>{
  const app  = AppStorageV2.connect(myNumber,'key',()=>new myNumber(28))!
 console.log( app.age)
})
2.PersistenceV2: 持久化储存UI状态

要模拟器或真机才能有用

PersistenceV2提供状态变量持久化能力,开发者可以通过connect绑定同一个key,在状态变量变换和应用冷启动时,实现持久化能力。

基本用法:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值