harmony 鸿蒙V1->V2迁移指导

  • 2025-06-16
  • 浏览 (3)

V1->V2迁移指导

概述

ArkUI状态管理的主要职责是:负责将可观察数据的变化自动同步到UI界面,实现数据驱动的UI刷新,使开发者能更加够专注于UI界面的实现和设计。

在状态管理框架的演进过程中,先后推出了状态管理V1和V2两个版本。V1强调组件层级的状态管理,而V2则增强了对数据对象的深度观察与管理能力,不再局限于组件层级。通过V2,开发者能够更灵活地控制数据和状态,实现更高效的UI刷新。具体V1和V2的区别可以参见状态管理概述

V1V2使用指引

  1. V2是V1的增强版本,正在持续迭代优化来为开发者提供更多功能和灵活性。
  2. 对于新开发的应用,建议直接使用V2版本范式来进行开发。
  3. 对于已经使用V1的应用,如果V1的功能和性能已能满足需求,则不必立即切换到V2。
  4. 对于需要在现阶段混用V1和V2的场景,请参阅混用文档。编译器、工具链、DevEco Studio对某些不推荐的误用和混用场景会进行校验,虽然开发者可能可以通过特殊手段绕过这些校验,但还是强烈建议开发者遵循混用文档的指导,避免因双重代理等问题给应用带来不确定性。

迁移指南的目的

  1. 对希望将现有V1应用迁移到V2的开发者,提供系统化的模板和指导,帮助完成V1到V2的迁移。
  2. 对希望逐步将V1应用过渡到V2的开发者,提供参考,结合本迁移文档与混用文档,可以帮助开发者实现逐步改造。
  3. 尚未开始开发应用但已熟悉V1状态管理规则的开发者,可以参考本迁移文档及V2各个装饰器和接口的文档,开始使用V2进行应用开发。

V1V2能力对比及迁移简表

V1装饰器名 V2装饰器名 说明
\@Observed \@ObservedV2 表明当前对象为可观察对象。但两者能力并不相同。
\@Observed可观察第一层的属性,需要搭配\@ObjectLink使用才能生效。
\@ObservedV2本身无观察能力,仅代表当前class可被观察,如果要观察其属性,需要搭配\@Trace使用。
\@Track \@Trace V1装饰器\@Track为精确观察,不使用则无法做到类属性的精准观察。
V2\@Trace装饰的属性可以被精确跟踪观察。
\@Component \@ComponentV2 \@Component为搭配V1状态变量使用的自定义组件装饰器。
@ComponentV2为搭配V2状态变量使用的自定义组件装饰器。
\@State 无外部初始化:@Local
外部初始化一次:\@Param\@Once
\@State和\@Local类似都是数据源的概念,在不需要外部传入初始化时,可直接迁移。如果需要外部传入初始化,则可以迁移为\@Param\@Once,详情见@State->@Local
\@Prop \@Param \@Prop和\@Param类似都是自定义组件参数的概念。当输入参数为复杂类型时,\@Prop为深拷贝,\@Param为引用。
\@Link \@Param\@Event \@Link是框架自己封装实现的双向同步,对于V2开发者可以通过@Param@Event自己实现双向同步。
\@ObjectLink \@Param 直接兼容,\@ObjectLink需要被@Observed装饰的class的实例初始化,\@Param没有此限制。
\@Provide \@Provider 兼容。
\@Consume \@Consumer 兼容。
\@Watch \@Monitor \@Watch用于监听V1状态变量的变化,具有监听状态变量本身和其第一层属性变化的能力。状态变量可观察到的变化会触发其\@Watch监听事件。
\@Monitor用于监听V2状态变量的变化,搭配\@Trace使用,可有深层监听的能力。状态变量在一次事件中多次变化时,仅会以最终的结果判断是否触发\@Monitor监听事件。
LocalStorage 全局\@ObservedV2\@Trace 兼容。
AppStorage AppStorageV2 兼容。
Environment 调用Ability接口获取系统环境变量 Environment获取环境变量能力和AppStorage耦合。在V2中可直接调用Ability接口获取系统环境变量。
PersistentStorage PersistenceV2 PersistentStorage持久化能力和AppStorage耦合,PersistenceV2持久化能力可独立使用。

各装饰器迁移示例

@State->@Local

迁移规则

在V1中,\@State装饰器用于装饰组件内部的状态变量,在V2中提供了\@Local作为其替代能力,但两者在观察能力和初始化规则上存在明显差异。针对不同的使用场景,迁移策略如下:

  • 简单类型:对于简单类型的变量,可以直接将\@State替换为\@Local。
  • 复杂类型:V1中的@State可以观察复杂对象的第一层属性变化,而V2中的\@Local只能观察对象自身的变化。如果需要追踪对象内部的属性变化,可以结合使用\@ObservedV2和\@Trace。
  • 外部初始化:V1中,\@State支持从外部传递初始值,但在V2中,\@Local禁止外部初始化。若需要从外部传递初始值,可以使用\@Param和\@Once装饰器来实现类似的效果。

示例

简单类型

对于简单类型变量,V1的@State可以直接替换为V2的@Local。

V1:

@Entry
@Component
struct Child {
  @State val: number = 10;
  build(){
    Text(this.val.toString())
  }
}

V2迁移策略:直接替换。

@Entry
@ComponentV2
struct Child {
  @Local val: number = 10;
  build(){
    Text(this.val.toString())
  }
}

复杂类型

V1的@State能够观察复杂对象的第一层属性变化,但V2的@Local无法观察对象内部变化。为了解决这个问题,需要在类上添加@ObservedV2,并在需要观察的属性上添加@Trace。这样,框架就能追踪对象内部的属性变化。

V1:

class Child {
  value: number = 10;
}

@Component
@Entry
struct example {
  @State child: Child = new Child();
  build(){
    Column() {
      Text(this.child.value.toString())
      // @State可以观察第一层变化
      Button('value+1')
        .onClick(() => {
          this.child.value++;
        })
    }
  }
}

V2迁移策略:使用@ObservedV2和@Trace。

@ObservedV2
class Child {
  @Trace public value: number = 10;
}

@ComponentV2
@Entry
struct example {
  @Local child: Child = new Child();
  build(){
    Column() {
      Text(this.child.value.toString())
      // @Local只能观察自身,需要给Child加上@ObservedV2和@Trace
      Button('value+1')
        .onClick(() => {
          this.child.value++;
        })
    }
  }
}

外部初始化状态变量

V1的@State变量可以从外部初始化,V2的@Local禁止外部初始化。为实现类似功能,需要用@Param和@Once代替@State,允许外部传入初始值,并确保该值只初始化时同步一次。

V1实现:

@Component
struct Child {
  @State value: number = 0;
  build() {
    Text(this.value.toString())
  }
}

@Entry
@Component
struct Parent {
  build() {
    Column(){
      // @State可以从外部初始化
      Child({ value: 30 })
    }
  }
}

V2迁移策略:使用@Param和@Once。

@ComponentV2
struct Child {
  @Param @Once value: number = 0;
  build() {
    Text(this.value.toString())
  }
}

@Entry
@ComponentV2
struct Parent {
  build() {
    Column(){
      // @Local禁止从外部初始化,可以用@Param和@Once替代实现
      Child({ value: 30 })
    }
  }
}

@Link -> @Param/@Event

迁移规则

在V1中,@Link允许父组件和子组件之间进行双向数据绑定。迁移到V2时,可以用@Param和@Event模拟双向同步。@Param实现父到子的单向传递,子组件再通过@Event回调函数触发父组件的状态更新。

示例

V1实现:

@Component
struct Child {
  // @Link可以双向同步数据
  @Link val: number;
  build() {
    Column(){
      Text("child: " + this.val.toString())
      Button("+1")
        .onClick(() => {
          this.val++;
        })
    }
  }
}

@Entry
@Component
struct Parent {
  @State myVal: number = 10;
  build() {
    Column(){
      Text("parent: " + this.myVal.toString())
      Child({val: this.myVal})
    }
  }
}

V2迁移策略:使用@Param和@Event。

@ComponentV2
struct Child {
  // @Param搭配@Event回调实现数据双向同步
  @Param val: number  = 0;
  @Event addOne: () => void;
  build() {
    Column(){
      Text("child: " + this.val.toString())
      Button("+1")
        .onClick(()=> { 
          this.addOne();
        })
    }
  }
}

@Entry
@ComponentV2
struct Parent {
  @Local myVal: number = 10
  build() {
    Column() {
      Text("parent: " + this.myVal.toString())
      Child({ val: this.myVal, addOne: () => this.myVal++})
    }
  }
}

@Prop -> @Param

迁移规则

在V1中,@Prop装饰器用于从父组件传递参数给子组件,这些参数在子组件中可以被直接修改。在V2中,@Param取代了@Prop的作用,但@Param是只读的,子组件不能直接修改参数的值。因此,根据场景的不同,有几种迁移策略:

  • 简单类型:对于简单类型的参数,可以直接将@Prop替换为@Param。
  • 复杂类型:如果传递的是复杂对象且需要严格的单向数据绑定,可以对对象进行深拷贝,防止子组件修改父组件的数据。
  • 子组件修改变量:如果子组件需要修改传入的参数,可以使用@Once来允许子组件对在本地修改该变量。但需要注意,如果使用了\@Once,则代表当前子组件只会被初始化一次,后续并没有父组件到子组件的同步能力。

示例

简单类型

对于简单类型变量,V1的@Prop可以直接替换为V2的@Param。

V1实现:

@Component
struct Child {
  @Prop value: number;
  build() {
    Text(this.value.toString())
  }
}

@Entry
@Component
struct Parent {
  build() {
    Column(){
      Child({ value: 30 })
    }
  }
}

V2迁移策略:直接替换。

@ComponentV2
struct Child {
  @Param value: number = 0;
  build() {
    Text(this.value.toString())
  }
}

@Entry
@ComponentV2
struct Parent {
  build() {
    Column(){
      Child({ value: 30 })
    }
  }
}

复杂类型的单向数据传递

在V2中,传递复杂类型时,如果希望实现严格的单向数据绑定,防止子组件修改父组件的数据,需要在使用@Param传递复杂对象时进行深拷贝以避免传递对象的引用。

V1实现:

class Fruit {
  apple: number = 5;
  orange: number = 10;
}

@Component
struct Child {
  // @Prop传递Fruit类,当子类修改属性,父类不受影响
  @Prop fruit: Fruit;
  build() {
    Column() {
      Text("child apple: "+ this.fruit.apple.toString())
      Text("child orange: "+ this.fruit.orange.toString())
      Button("apple+1")
        .onClick(() => {
          this.fruit.apple++;
        })
      Button("orange+1")
        .onClick(() => {
          this.fruit.orange++;
        })
    }
  }
}

@Entry
@Component
struct Parent {
  @State parentFruit: Fruit = new Fruit();
  build() {
    Column(){
      Text("parent apple: "+this.parentFruit.apple.toString())
      Text("parent orange: "+this.parentFruit.orange.toString())
      Child({ fruit: this.parentFruit })
    }
  }
}

V2迁移策略:使用深拷贝。

@ObservedV2
class Fruit{
  @Trace apple: number = 5;
  @Trace orange: number = 10;
  // 实现深拷贝,子组件不会修改父组件的数据
  clone(): Fruit {
    let newFruit: Fruit = new Fruit();
    newFruit.apple = this.apple;
    newFruit.orange = this.orange;
    return newFruit;
  }
}

@ComponentV2
struct Child {
  @Param fruit: Fruit = new Fruit();
  build() {
    Column() {
      Text("child")
      Text(this.fruit.apple.toString())
      Text(this.fruit.orange.toString())
      Button("apple+1")
        .onClick( ()=> {
          this.fruit.apple++;
        })
      Button("orange+1")
        .onClick(() => {
          this.fruit.orange++;
        })
    }
  }
}

@Entry
@ComponentV2
struct Parent {
  @Local parentFruit: Fruit = new Fruit();
  build() {
    Column(){
      Text("parent")
      Text(this.parentFruit.apple.toString())
      Text(this.parentFruit.orange.toString())
      Child({ fruit: this.parentFruit.clone()})
    }
  }
}

子组件修改变量

在V1中,子组件可以修改@Prop的变量,然而在V2中,@Param是只读的。如果子组件需要修改传入的值,可以使用@Param和@Once允许子组件在本地修改。

V1实现:

@Component
struct Child {
  // @Prop可以直接修改变量值
  @Prop value: number;
  build() {
    Column(){
      Text(this.value.toString())
      Button("+1")
        .onClick(()=> {
          this.value++;
        })
    }
  }
}

@Entry
@Component
struct Parent {
  build() {
    Column(){
      Child({ value: 30 })
    }
  }
}

V2迁移策略:使用@Param和@Once。

@ComponentV2
struct Child {
  // @Param搭配@Once使用,可以在本地修改@Param变量
  @Param @Once value: number = 0;
  build() {
    Column(){
      Text(this.value.toString())
      Button("+1")
        .onClick(() => {
          this.value++;
        })
    }
  }
}

@Entry
@ComponentV2
struct Parent {
  build() {
    Column(){
      Child({ value: 30 })
    }
  }
}

在V1中,子组件可以修改\@Prop的变量,且只会在本地更新,不会同步回父组件。父组件数据源更新时,会通知子组件更新,并覆写子组件本地\@Prop的值。

V1: - 改变子组件ChildlocalValue,不会同步回父组件Parent。 - 父组件更新value,通知子组件Child更新,并覆写本地子组件localValue的值。

@Component
struct Child {
  @Prop localValue: number = 0;

  build() {
    Column() {
      Text(`${this.localValue}`).fontSize(25)
      Button('Child +100')
        .onClick(() => {
          // 改变localValue不会传递给父组件Parent
          this.localValue += 100;
        })
    }
  }
}

@Entry
@Component
struct Parent {
  @State value: number = 10;
  build() {
    Column() {
      Button('Parent +1')
        .onClick(() => {
          // 改变value的值,通知子组件Child value更新
          this.value += 1;
        })
      Child({ localValue: this.value })
    }
  }
}

V2中,\@Param本地不可写,和\@Once搭配使用只会同步一次。如果要实现子组件本地可写,且父组件后续更新还是能通知子组件,可以借助\@Monitor来实现这一效果。

V2实现: - 父组件Parent更新通知子组件value的刷新,并回调\@Monitor修饰的onValueChange回调方法,onValueChange将更新后的值赋值给localValue。 - 子组件Child改变localValue的值,不会同步给父组件Parent。 - 父组件Parent中再次改变value,将会继续通知给子组件,并覆写子组件本地localValue的值。

@ComponentV2
struct Child {
  @Local localValue: number = 0;
  @Param value: number = 0;
  @Monitor('value')
  onValueChange(mon: IMonitor) {
    console.info(`value has been changed from ${mon.value()?.before} to ${mon.value()?.now}`);
    // 父组件value变化时,通知子组件value更新,回调Monitor函数,将更新的值覆写给本地的localValue
    this.localValue = this.value;
  }

  build() {
    Column() {
      Text(`${this.localValue}`).fontSize(25)
      Button('Child +100')
        .onClick(() => {
          // 改变localValue不会传递给父组件Parent
          this.localValue += 100;
        })
    }
  }
}

@Entry
@ComponentV2
struct Parent {
  @Local value: number = 10;
  build() {
    Column() {
      Button('Parent +1')
        .onClick(() => {
          // 改变value的值,通知子组件Child value更新
          this.value += 1;
        })
      Child({ value: this.value })
    }
  }
}

@ObjectLink/@Observed/@Track -> @ObservedV2/@Trace

迁移规则

在V1中,@Observed与@ObjectLink装饰器用于观察类对象及其嵌套属性的变化,但V1只能直接观察对象的第一层属性。对于嵌套对象的属性,必须通过自定义组件和@ObjectLink实现观察。此外,V1中提供了@Track装饰器来实现对属性级别变化的精确控制。

在V2中,@ObservedV2与@Trace结合使用,可以高效地实现类对象及其嵌套属性的深度观察,省去了对自定义组件的依赖,简化了开发流程。同时,@Trace装饰器还具备精确更新的能力,替代了V1中的@Track,从而实现更高效的UI刷新控制。根据不同的场景,有以下迁移策略:

  • 嵌套对象的属性观察:V1中需要通过自定义组件和@ObjectLink观察嵌套属性,V2中则可以使用@ObservedV2和@Trace直接观察嵌套对象,简化了代码结构。
  • 类属性的精确更新:V1中的@Track可以用V2中的@Trace取代,@Trace可以同时观察和精确更新属性变化,使代码更简洁高效。

示例

嵌套对象属性观察方法

在V1中,无法直接观察嵌套对象的属性变化,只能观察到第一层属性的变化。必须通过创建自定义组件并使用@ObjectLink来实现对嵌套属性的观察。V2中使用@ObservedV2和@Trace,可以直接对嵌套对象的属性进行深度观察,减少复杂度。

V1实现:

@Observed
class Address {
  city: string;

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

@Observed
class User {
  name: string;
  address: Address;

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

@Component
struct AddressView {
  // 子组件中@ObjectLink装饰的address从父组件初始化,接收被@Observed装饰的Address实例
  @ObjectLink address: Address;

  build() {
    Column() {
      Text(`City: ${this.address.city}`)
      Button("city +a")
        .onClick(() => {
          this.address.city += "a";
        })
    }
  }
}

@Entry
@Component
struct UserProfile {
  @State user: User = new User("Alice", new Address("New York"));

  build() {
    Column() {
      Text(`Name: ${this.user.name}`)
      // 无法直接观察嵌套对象的属性变化,例如this.user.address.city
      // 只能观察到对象第一层属性变化,所以需要将嵌套的对象Address抽取到自定义组件AddressView
      AddressView({ address: this.user.address })
    }
  }
}

V2迁移策略:使用@ObservedV2和@Trace。

@ObservedV2
class Address {
  @Trace city: string;

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

@ObservedV2
class User {
  @Trace name: string;
  @Trace address: Address;

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

@Entry
@ComponentV2
struct UserProfile {
  @Local user: User = new User("Alice", new Address("New York"));

  build() {
    Column() {
      Text(`Name: ${this.user.name}`)
      // 通过@ObservedV2和@Trace可以直接观察嵌套属性
      Text(`City: ${this.user.address.city}`)
      Button("city +a")
        .onClick(() => {
          this.user.address.city += "a";
        })
    }
  }
}

类属性变化观测

在V1中,@Observed用于观察类实例及其属性的变化,@Track则用于对属性级别的变化优化,使得只有被@Track装饰的属性触发UI更新。在V2中,@Trace结合了观察和更新属性级别变化的能力,搭配@ObservedV2实现高效的UI更新。

V1实现:

@Observed
class User {
  @Track name: string;
  @Track age: number;

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

@Entry
@Component
struct UserProfile {
  @State user: User = new User('Alice', 30);

  build() {
    Column() {
      Text(`Name: ${this.user.name}`)
      Text(`Age: ${this.user.age}`)
      Button("increase age")
        .onClick(() => {
          this.user.age++;
        })
    }
  }
}

V2迁移策略:使用@ObservedV2和@Trace。

@ObservedV2
class User {
  @Trace name: string;
  @Trace age: number;

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

@Entry
@ComponentV2
struct UserProfile {
  @Local user: User = new User('Alice', 30);

  build() {
    Column() {
      Text(`Name: ${this.user.name}`)
      Text(`Age: ${this.user.age}`)
      Button("Increase age")
        .onClick(() => {
          this.user.age++;
        })
    }
  }
}

@Provide/@Consume -> @Provider/@Consumer

迁移规则

V1的@Provide/@Consume和V2@Provider/@Consumer定位和作用大体类似,基本可以实现丝滑替换,但是有以下细微差距,开发者可根据自己代码实现来参考是否需要调整: 在V1中,@Provide和@Consume用于父子组件之间的数据共享,可以通过alias(别名)或属性名匹配,同时@Consume必须依赖父组件的@Provide,不允许本地初始化。而V2中,@Provider和@Consumer增强了这些特性,使数据共享更加灵活。根据不同的场景,有以下迁移策略:

  • V1中\@Provide/\@Consume在没有指定alias的情况下,可以直接使用。V2中\@Provider/\@Consumer是标准装饰器,且参数可选,所以不管有无指定alias后面需要必须跟随“()”。

  • alias和属性名匹配规则:V1中,@Provide和@Consume可以通过alias或属性名匹配;V2中,alias是唯一的匹配key,指定alias后只能通过alias匹配。

  • 本地初始化支持:V1中,@Consume不允许本地初始化,必须依赖父组件;V2中,@Consumer支持本地初始化,当找不到对应的@Provider时使用本地默认值。

  • 从父组件初始化:V1中,@Provide可以直接从父组件初始化;V2中,@Provider不支持外部初始化,需用@Param和@Once接受初始值并赋给 @Provider。

  • 重载支持:V1中,@Provide默认不支持重载,需设置 allowOverride;V2中,@Provider默认支持重载,@Consumer会向上查找最近的@Provider。

    示例

    alias和属性名匹配规则

在V1中,@Provide和@Consume的匹配既可以通过alias,也可以通过属性名。在V2中,alias成为唯一的key,如果在@Consumer中制定了alias,只能通过alias而非属性名进行匹配。

V1实现:

@Component
struct Child {
  // alias和属性名都为key,alias和属性名都可以匹配
  @Consume('text') childMessage: string;
  @Consume message: string;
  build(){
    Column(){
      Text(this.childMessage)
      Text(this.message) // Text是Hello World
    }
  }
}

@Entry
@Component
struct Parent {
  @Provide('text') message: string = "Hello World";
  build(){
    Column(){
      Child()
    }
  }
}

V2迁移策略:确保alias一致,没有指定alias的情况下,依赖属性名进行匹配。

@ComponentV2
struct Child {
  // alias是唯一匹配的key,有alias情况下无法通过属性名匹配
  @Consumer('text') childMessage: string = "default";
  @Consumer() message: string = "default";
  build(){
    Column(){
      Text(this.childMessage)
      Text(this.message) // Text是default
    }
  }
}

@Entry
@ComponentV2
struct Parent {
  @Provider('text') message: string = "Hello World";
  build(){
    Column(){
      Child()
    }
  }
}

V1的@Consume不支持本地初始化,V2支持

V1中,@Consume不允许本地初始化变量,必须依赖父组件的@Provide,否则会抛出异常。迁移到V2后,@Consumer允许本地初始化,当找不到对应的@Provider,会使用本地默认值。

V1实现:

@Component
struct Child {
  // @Consume禁止本地初始化,当找不到对应的@Provide时抛出异常
  @Consume message: string;
  build(){
    Text(this.message)
  }
}

@Entry
@Component
struct Parent {
  @Provide message: string = "Hello World";
  build(){
    Column(){
      Child()
    }
  }
}

V2迁移策略:@Consumer可以本地初始化。

@ComponentV2
struct Child {
  // @Consumer允许本地初始化,当找不到@Provider的时候使用本地默认值
  @Consumer() message: string = "Hello World";
  build(){
    Text(this.message)
  }
}

@Entry
@ComponentV2
struct Parent {
  build(){
    Column(){
      Child()
    }
  }
}

V1的@Provide可以从父组件初始化,V2不支持

在V1中,@Provide允许从父组件初始化,可以直接通过组件参数传递初始值。在V2中,@Provider禁止从外部初始化。为实现相同功能,可以在子组件中使用@Param @Once接受初始值,然后将其赋值给@Provider变量。

V1实现:

@Entry
@Component
struct Parent {
  @State parentValue: number = 42;
  build() {
    Column() {
      // @Provide可以从父组件初始化
      Child({ childValue: this.parentValue })
    }
  }
}

@Component
struct Child {
  @Provide childValue: number = 0;
  build(){
    Column(){
      Text(this.childValue.toString())
    }
  }
}

V2迁移策略:使用@Param接受初始值,再赋值给@Provider。

@Entry
@ComponentV2
struct Parent {
  @Local parentValue: number = 42;
  build() {
    Column() {
      // @Provider禁止从父组件初始化,替代方案为先用@Param接受,再赋值给@Provider
      Child({ initialValue: this.parentValue })
    }
  }
}

@ComponentV2
struct Child {
  @Param @Once initialValue: number = 0;
  @Provider() childValue: number = this.initialValue;
  build() {
    Column(){
      Text(this.childValue.toString())
    }
  }
}

V1的@Provide默认不支持重载,V2默认支持

在V1中,@Provide默认不支持重载,无法覆盖上层组件的同名@Provide。若需支持重载,必须设置allowOverride。在V2中,@Provider默认支持重载,@Consumer会向上查找最近的@Provider,无需额外设置。

V1实现:

@Entry
@Component
struct GrandParent {
  @Provide("reviewVotes") reviewVotes: number = 40;
  build() {
    Column(){
      Parent()
    }
  }
}

@Component
struct Parent {
  // @Provide默认不支持重载,支持重载需设置allowOverride函数
  @Provide({ allowOverride: "reviewVotes" }) reviewVotes: number = 20;
  build() {
    Child()
  }
}

@Component
struct Child {
  @Consume("reviewVotes") reviewVotes: number;
  build() {
    Text(this.reviewVotes.toString()) // Text显示20
  }
}

V2迁移策略:去掉allowOverride。

@Entry
@ComponentV2
struct GrandParent {
  @Provider("reviewVotes") reviewVotes: number = 40;
  build() {
    Column(){
      Parent()
    }
  }
}

@ComponentV2
struct Parent {
  // @Provider默认支持重载,@Consumer向上查找最近的@Provider
  @Provider() reviewVotes: number = 20;
  build() {
    Child()
  }
}

@ComponentV2
struct Child {
  @Consumer() reviewVotes: number = 0;
  build() {
    Text(this.reviewVotes.toString()) // Text显示20
  }
}

@Watch -> @Monitor

迁移规则

在V1中,\@Watch用于监听状态变量的变化,并在变量变化时触发指定回调函数。在V2中,\@Monitor替代了\@Watch,可以更灵活地监听变量的变化,并获取变量变化前后的值。具体的迁移策略如下:

  • 单变量监听:对于简单的场景,可以直接用@Monitor替换@Watch,效果一致。

  • 多变量监听:V1的@Watch无法获取变化前的值。在V2中,\@Monitor支持同时监听多个变量,并可以访问变量变化前后的状态。

    示例

    单变量监听

对于简单案例,V1的@Watch可以直接替换为替换为V2的@Monitor。

V1实现:

@Entry
@Component
struct watchExample {
  @State @Watch('onAppleChange') apple: number = 0;
  onAppleChange(): void {
    console.log("apple count changed to "+this.apple);
  }

  build() {
    Column(){
      Text(`apple count: ${this.apple}`)
      Button("add apple")
        .onClick(() => {
          this.apple++;
        })
    }
  }
}

V2迁移策略:直接替换。

@Entry
@ComponentV2
struct monitorExample {
  @Local apple: number = 0;
  @Monitor('apple')
  onFruitChange(monitor: IMonitor) {
    console.log(`apple changed from ${monitor.value()?.before} to ${monitor.value()?.now}`);
  }

  build() {
    Column(){
      Text(`apple count: ${this.apple}`)
      Button("add apple")
        .onClick(()=> {
          this.apple++;
        })
    }
  }
}

多变量监听

在V1中,每个@Watch回调函数只能监听一个变量,且无法获取变化前的值。迁移到V2后,可以使用一个@Monitor同时监听多个变量以及获取监听变量的变化前后的值。

V1实现:

@Entry
@Component
struct watchExample {
  @State @Watch('onAppleChange') apple: number = 0;
  @State @Watch('onOrangeChange') orange: number = 0;
  // @Watch 回调,只能监听单个变量,不能获取变化前的值
  onAppleChange(): void {
    console.log("apple count changed to "+this.apple);
  }
  onOrangeChange(): void {
    console.log("orange count changed to "+this.orange);
  }

  build() {
    Column(){
      Text(`apple count: ${this.apple}`)
      Text(`orange count: ${this.orange}`)
      Button("add apple")
        .onClick(() => {
          this.apple++;
        })
      Button("add orange")
        .onClick(() => {
          this.orange++;
        })
    }
  }
}

V2迁移策略:同时监听多个变量,以及获取变化前的值。

@Entry
@ComponentV2
struct monitorExample {
  @Local apple: number = 0;
  @Local orange: number = 0;

  // @Monitor回调,支持监听多个变量,可以获取变化前的值
  @Monitor('apple','orange')
  onFruitChange(monitor: IMonitor) {
    monitor.dirty.forEach((name: string) => {
      console.log(`${name} changed from ${monitor.value(name)?.before} to ${monitor.value(name)?.now}`);
    });
  }

  build() {
    Column() {
      Text(`apple count: ${this.apple}`)
      Text(`orange count: ${this.orange}`)
      Button("add apple")
        .onClick(() => {
          this.apple++;
        })
      Button("add orange")
        .onClick(() => {
          this.orange++;
        })
    }
  }
}

@Computed

迁移规则

V1中并没有提供计算属性的概念,所以对于UI中的冗余计算,并没有办法可以减少重复计算。V2针对该场景,提供了@Computed装饰器,可以帮助开发者减少重复计算。

V1: 在下面的例子中,每次改变lastName都会触发Text组件的刷新,每次Text组件的刷新,都需要重复计算this.lastName + ' ' + this.firstName

@Entry
@Component
struct Index {
  @State firstName: string = 'Li';
  @State lastName: string = 'Hua';

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

    }
  }
}

V2: 使用V2中的\@Computed,每次改变lastName仅会触发一次计算。

@Entry
@ComponentV2
struct Index {
  @Local firstName: string = 'Li';
  @Local lastName: string = 'Hua';

  @Computed
  get fullName() {
    return this.firstName + ' ' + this.lastName;
  }

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

LocalStorage->全局@ObservedV2/@Trace

迁移规则

LocalStorage的目的是为了实现页面间的状态变量共享。之所以提供这个能力,是因为V1状态变量和View层耦合,无法由开发者自主地实现页面间状态变量的共享。 对于状态管理V2,状态变量的观察能力内嵌到数据本身,不再和View层耦合,所以对于状态管理V2,不再需要类似LocalStorage的能力,可以使用全局@ObservedV2/@Trace,由开发者自己import和export,自己实现状态变量的页面间共享。

示例

基本场景

V1: 通过windowStage.loadContent和this.getUIContext().getSharedLocalStorage接口实现页面间的状态变量共享。

// EntryAbility.ets
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  para:Record<string, number> = { 'count': 47 };
  storage: LocalStorage = new LocalStorage(this.para);

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Page1', this.storage);
  }
}

在下面的示例中,使用\@LocalStorageLink,可以使得开发者本地的修改同步回LocalStorage中。

// Page1.ets
// 预览器上不支持获取页面共享的LocalStorage实例。
@Entry({ useSharedStorage: true })
@Component
struct Page1 {
  @LocalStorageLink('count') count: number = 0;
  pageStack: NavPathStack = new NavPathStack();
  build() {
    Navigation(this.pageStack) {
      Column() {
        Text(`${this.count}`)
          .fontSize(50)
          .onClick(() => {
            this.count++;
          })
        Button('push to Page2')
          .onClick(() => {
            this.pageStack.pushPathByName('Page2', null);
          })
      }
    }
  }
}
// Page2.ets
@Builder
export function Page2Builder() {
  Page2()
}

// Page2组件获得了父亲Page1组件的LocalStorage实例
@Component
struct Page2 {
  @LocalStorageLink('count') count: number = 0;
  pathStack: NavPathStack = new NavPathStack();

  build() {
    NavDestination() {
      Column() {
        Text(`${this.count}`)
          .fontSize(50)
          .onClick(() => {
            this.count++;
          })
        Button('change')
          .fontSize(50)
          .onClick(() => {
            const storage = this.getUIContext().getSharedLocalStorage();
            if (storage) {
              storage.set('count', 20);
            }
          })
      }
    }
    .onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
    })
  }
}

使用Navigation时,需要添加配置系统路由表文件src/main/resources/base/profile/route_map.json,并替换pageSourceFile为Page2页面的路径,并且在module.json5中添加:”routerMap”: “$profile:route_map”。

{
  "routerMap": [
    {
      "name": "Page2",
      "pageSourceFile": "src/main/ets/pages/Page2.ets",
      "buildFunction": "Page2Builder",
      "data": {
        "description" : "LocalStorage example"
      }
    }
  ]
}

V2: - 声明\@ObservedV2装饰的MyStorage类,并import需要使用的页面中。 - 声明被\@Trace的属性作为页面间共享的可观察的数据。

// storage.ets
@ObservedV2
export class MyStorage {
  static singleton_: MyStorage;
  static instance() {
    if(!MyStorage.singleton_) {
      MyStorage.singleton_ = new MyStorage();
    };
    return MyStorage.singleton_;
  }
  @Trace count: number = 47;
}
// Page1.ets
import { MyStorage } from './storage';

@Entry
@ComponentV2
struct Page1 {
  storage: MyStorage = MyStorage.instance();
  pageStack: NavPathStack = new NavPathStack();
  build() {
    Navigation(this.pageStack) {
      Column() {
        Text(`${this.storage.count}`)
          .fontSize(50)
          .onClick(() => {
            this.storage.count++;
          })
        Button('push to Page2')
          .onClick(() => {
            this.pageStack.pushPathByName('Page2', null);
          })
      }
    }
  }
}
// Page2.ets
import { MyStorage } from './storage';

@Builder
export function Page2Builder() {
  Page2()
}

@ComponentV2
struct Page2 {
  storage: MyStorage = MyStorage.instance();
  pathStack: NavPathStack = new NavPathStack();
  build() {
    NavDestination() {
      Column() {
        Text(`${this.storage.count}`)
          .fontSize(50)
          .onClick(() => {
            this.storage.count++;
          })
      }
    }
    .onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
    })
  }
}

使用Navigation时,需要添加配置系统路由表文件src/main/resources/base/profile/route_map.json,并替换pageSourceFile为Page2页面的路径,并且在module.json5中添加:”routerMap”: “$profile:route_map”。

{
  "routerMap": [
    {
      "name": "Page2",
      "pageSourceFile": "src/main/ets/pages/Page2.ets",
      "buildFunction": "Page2Builder",
      "data": {
        "description" : "LocalStorage example"
      }
    }
  ]
}

如果开发者需要实现类似于\@LocalStorageProp的效果,希望本地的修改不要同步回LocalStorage中,如以下示例: - 在Page1中改变count值,因为count是\@LocalStorageProp装饰的,所以其改变只会在本地生效,并不会同步回LocalStorage。 - 点击push to Page2,跳转到Page2中。因为在Page1中改变count值并不会同步会LocalStorage,所以在Page2中Text组件依旧显示原本的值47。 - 点击change Storage Count,调用LocalStorage的setOrCreate,改变count对应的值,并通知所有绑定该key的变量。

// Page1.ets
export let storage: LocalStorage = new LocalStorage();
storage.setOrCreate('count', 47);

@Entry(storage)
@Component
struct Page1 {
  @LocalStorageProp('count') count: number = 0;
  pageStack: NavPathStack = new NavPathStack();
  build() {
    Navigation(this.pageStack) {
      Column() {
        Text(`${this.count}`)
          .fontSize(50)
          .onClick(() => {
            this.count++;
          })
        Button('change Storage Count')
          .onClick(() => {
            storage.setOrCreate('count', storage.get<number>('count') as number + 100);
          })
        Button('push to Page2')
          .onClick(() => {
            this.pageStack.pushPathByName('Page2', null);
          })
      }
    }
  }
}
// Page2.ets
import { storage } from './Page1'
@Builder
export function Page2Builder() {
  Page2()
}

// Page2组件获得了父亲Page1组件的LocalStorage实例
@Component
struct Page2 {
  @LocalStorageProp('count') count: number = 0;
  pathStack: NavPathStack = new NavPathStack();
  build() {
    NavDestination() {
      Column() {
        Text(`${this.count}`)
          .fontSize(50)
          .onClick(() => {
            this.count++;
          })
        Button('change Storage Count')
          .onClick(() => {
            storage.setOrCreate('count', storage.get<number>('count') as number + 100);
          })
      }
    }
    .onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
    })
  }
}

在V2中,可以借助\@Local和\@Monitor实现类似的效果。 - \@Local装饰的count变量为组件本地的值,其改变不会同步回storage。 - \@Monitor监听storage.count的变化,当storage.count改变时,在\@Monitor的回调里改变本地\@Local的值。

// Page1.ets
import { MyStorage } from './storage';

@Entry
@ComponentV2
struct Page1 {
  storage: MyStorage = MyStorage.instance();
  pageStack: NavPathStack = new NavPathStack();
  @Local count: number = this.storage.count;

  @Monitor('storage.count')
  onCountChange(mon: IMonitor) {
    console.log(`Page1 ${mon.value()?.before} to ${mon.value()?.now}`);
    this.count = this.storage.count;
  }
  build() {
    Navigation(this.pageStack) {
      Column() {
        Text(`${this.count}`)
          .fontSize(50)
          .onClick(() => {
            this.count++;
          })
        Button('change Storage Count')
          .onClick(() => {
            this.storage.count += 100;
          })
        Button('push to Page2')
          .onClick(() => {
            this.pageStack.pushPathByName('Page2', null);
          })
      }
    }
  }
}
// Page2.ets
import { MyStorage } from './storage';

@Builder
export function Page2Builder() {
  Page2()
}

@ComponentV2
struct Page2 {
  storage: MyStorage = MyStorage.instance();
  pathStack: NavPathStack = new NavPathStack();
  @Local count: number = this.storage.count;

  @Monitor('storage.count')
  onCountChange(mon: IMonitor) {
    console.log(`Page2 ${mon.value()?.before} to ${mon.value()?.now}`);
    this.count = this.storage.count;
  }
  build() {
    NavDestination() {
      Column() {
        Text(`${this.count}`)
          .fontSize(50)
          .onClick(() => {
            this.count++;
          })
        Button('change Storage Count')
          .onClick(() => {
            this.storage.count += 100;
          })
      }
    }
    .onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
    })
  }
}

自定义组件接收LocalStorage实例场景

为了配合Navigation的场景,LocalStorage支持作为自定义组件的入参,传递给以当前自定义组件为根节点的所有子自定义组件。 对于该场景,V2可以采用多个全局\@ObservedV2/\@Trace实例来替代。

V1:

let localStorageA: LocalStorage = new LocalStorage();
localStorageA.setOrCreate('PropA', 'PropA');

let localStorageB: LocalStorage = new LocalStorage();
localStorageB.setOrCreate('PropB', 'PropB');

let localStorageC: LocalStorage = new LocalStorage();
localStorageC.setOrCreate('PropC', 'PropC');

@Entry
@Component
struct MyNavigationTestStack {
  @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack();

  @Builder
  PageMap(name: string) {
    if (name === 'pageOne') {
      // 传递不同的LocalStorage实例
      pageOneStack({}, localStorageA)
    } else if (name === 'pageTwo') {
      pageTwoStack({}, localStorageB)
    } else if (name === 'pageThree') {
      pageThreeStack({}, localStorageC)
    }
  }

  build() {
    Column({ space: 5 }) {
      Navigation(this.pageInfo) {
        Column() {
          Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
            .width('80%')
            .height(40)
            .margin(20)
            .onClick(() => {
              this.pageInfo.pushPath({ name: 'pageOne' }); //将name指定的NavDestination页面信息入栈
            })
        }
      }.title('NavIndex')
      .navDestination(this.PageMap)
      .mode(NavigationMode.Stack)
      .borderWidth(1)
    }
  }
}

@Component
struct pageOneStack {
  @Consume('pageInfo') pageInfo: NavPathStack;
  @LocalStorageLink('PropA') PropA: string = 'Hello World';

  build() {
    NavDestination() {
      Column() {
        // 显示'PropA'
        NavigationContentMsgStack()
        // 显示'PropA'
        Text(`${this.PropA}`)
        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
          .width('80%')
          .height(40)
          .margin(20)
          .onClick(() => {
            this.pageInfo.pushPathByName('pageTwo', null);
          })
      }.width('100%').height('100%')
    }.title('pageOne')
    .onBackPressed(() => {
      this.pageInfo.pop();
      return true;
    })
  }
}

@Component
struct pageTwoStack {
  @Consume('pageInfo') pageInfo: NavPathStack;
  @LocalStorageLink('PropB') PropB: string = 'Hello World';

  build() {
    NavDestination() {
      Column() {
        // 显示'Hello',当前LocalStorage实例localStorageB没有PropA对应的值,使用本地默认值'Hello'
        NavigationContentMsgStack()
        // 显示'PropB'
        Text(`${this.PropB}`)
        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
          .width('80%')
          .height(40)
          .margin(20)
          .onClick(() => {
            this.pageInfo.pushPathByName('pageThree', null);
          })

      }.width('100%').height('100%')
    }.title('pageTwo')
    .onBackPressed(() => {
      this.pageInfo.pop();
      return true;
    })
  }
}

@Component
struct pageThreeStack {
  @Consume('pageInfo') pageInfo: NavPathStack;
  @LocalStorageLink('PropC') PropC: string = 'pageThreeStack';

  build() {
    NavDestination() {
      Column() {
        // 显示'Hello',当前LocalStorage实例localStorageC没有PropA对应的值,使用本地默认值'Hello'
        NavigationContentMsgStack()
        // 显示'PropC'
        Text(`${this.PropC}`)
        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
          .width('80%')
          .height(40)
          .margin(20)
          .onClick(() => {
            this.pageInfo.pushPathByName('pageOne', null);
          })

      }.width('100%').height('100%')
    }.title('pageThree')
    .onBackPressed(() => {
      this.pageInfo.pop();
      return true;
    })
  }
}

@Component
struct NavigationContentMsgStack {
  @LocalStorageLink('PropA') PropA: string = 'Hello';

  build() {
    Column() {
      Text(`${this.PropA}`)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
    }
  }
}

V2:

声明\@ObservedV2装饰的class代替LocalStorage。其中LocalStorage的key可以用\@Trace装饰的属性代替。

// storage.ets
@ObservedV2
export class MyStorageA {
  @Trace propA: string = 'Hello';
  constructor(propA?: string) {
      this.propA = propA? propA : this.propA;
  }
}

@ObservedV2
export class MyStorageB extends MyStorageA {
  @Trace propB: string = 'Hello';
  constructor(propB: string) {
    super();
    this.propB = propB;
  }
}

@ObservedV2
export class MyStorageC extends MyStorageA {
  @Trace propC: string = 'Hello';
  constructor(propC: string) {
    super();
    this.propC = propC;
  }
}

pageOneStackpageTwoStackpageThreeStack组件内分别创建MyStorageAMyStorageBMyStorageC的实例,并通过\@Param传递给其子组件NavigationContentMsgStack,从而实现类似LocalStorage实例在子组件树上共享的能力。

// Index.ets
import { MyStorageA, MyStorageB, MyStorageC } from './storage';

@Entry
@ComponentV2
struct MyNavigationTestStack {
   pageInfo: NavPathStack = new NavPathStack();

  @Builder
  PageMap(name: string) {
    if (name === 'pageOne') {
      pageOneStack()
    } else if (name === 'pageTwo') {
      pageTwoStack()
    } else if (name === 'pageThree') {
      pageThreeStack()
    }
  }

  build() {
    Column({ space: 5 }) {
      Navigation(this.pageInfo) {
        Column() {
          Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
            .width('80%')
            .height(40)
            .margin(20)
            .onClick(() => {
              this.pageInfo.pushPath({ name: 'pageOne' }); //将name指定的NavDestination页面信息入栈
            })
        }
      }.title('NavIndex')
      .navDestination(this.PageMap)
      .mode(NavigationMode.Stack)
      .borderWidth(1)
    }
  }
}

@ComponentV2
struct pageOneStack {
  pageInfo: NavPathStack = new NavPathStack();
  @Local storageA: MyStorageA = new MyStorageA('PropA');

  build() {
    NavDestination() {
      Column() {
        // 显示'PropA'
        NavigationContentMsgStack({storage: this.storageA})
        // 显示'PropA'
        Text(`${this.storageA.propA}`)
        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
          .width('80%')
          .height(40)
          .margin(20)
          .onClick(() => {
            this.pageInfo.pushPathByName('pageTwo', null);
          })
      }.width('100%').height('100%')
    }.title('pageOne')
    .onBackPressed(() => {
      this.pageInfo.pop();
      return true;
    })
    .onReady((context: NavDestinationContext) => {
      this.pageInfo = context.pathStack;
    })
  }
}

@ComponentV2
struct pageTwoStack {
  pageInfo: NavPathStack = new NavPathStack();
  @Local storageB: MyStorageB = new MyStorageB('PropB');

  build() {
    NavDestination() {
      Column() {
        // 显示'Hello'
        NavigationContentMsgStack({ storage: this.storageB })
        // 显示'PropB'
        Text(`${this.storageB.propB}`)
        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
          .width('80%')
          .height(40)
          .margin(20)
          .onClick(() => {
            this.pageInfo.pushPathByName('pageThree', null);
          })

      }.width('100%').height('100%')
    }.title('pageTwo')
    .onBackPressed(() => {
      this.pageInfo.pop();
      return true;
    })
    .onReady((context: NavDestinationContext) => {
      this.pageInfo = context.pathStack;
    })
  }
}

@ComponentV2
struct pageThreeStack {
  pageInfo: NavPathStack = new NavPathStack();
  @Local storageC: MyStorageC = new MyStorageC("PropC");

  build() {
    NavDestination() {
      Column() {
        // 显示'Hello'
        NavigationContentMsgStack({ storage: this.storageC })
        // 显示'PropC'
        Text(`${this.storageC.propC}`)
        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
          .width('80%')
          .height(40)
          .margin(20)
          .onClick(() => {
            this.pageInfo.pushPathByName('pageOne', null);
          })

      }.width('100%').height('100%')
    }.title('pageThree')
    .onBackPressed(() => {
      this.pageInfo.pop();
      return true;
    })
    .onReady((context: NavDestinationContext) => {
      this.pageInfo = context.pathStack;
    })
  }
}

@ComponentV2
struct NavigationContentMsgStack {
  @Require@Param storage: MyStorageA;

  build() {
    Column() {
      Text(`${this.storage.propA}`)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
    }
  }
}

AppStorage->AppStorageV2

上一小节中,对于全局的@ObserveV2/@Trace的改造并不适合跨Ability的数据共享,该场景可以使用AppStorageV2来替换。

V1: AppStorage是和应用进程绑定了,可以跨Ability实现数据共享。 在下面的示例中,使用\@StorageLink,可以使得开发者本地的修改同步回AppStorage中。

// EntryAbility Index.ets
import { common, Want } from '@kit.AbilityKit';
@Entry
@Component
struct Index {
  @StorageLink('count') count: number = 0;
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;

  build() {
    Column() {
      Text(`EntryAbility count: ${this.count}`)
        .fontSize(50)
        .onClick(() => {
          this.count++;
        })
      Button('Jump to EntryAbility1').onClick(() => {
        let wantInfo: Want = {
          bundleName: 'com.example.myapplication', // 替换成AppScope/app.json5里的bundleName
          abilityName: 'EntryAbility1'
        };
        this.context.startAbility(wantInfo);
      })
    }
  }
}
// EntryAbility1 Index1.ets
import { common, Want } from '@kit.AbilityKit';
@Entry
@Component
struct Index1 {
  @StorageLink('count') count: number = 0;
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;

  build() {
    Column() {
      Text(`EntryAbility1 count: ${this.count}`)
        .fontSize(50)
        .onClick(() => {
          this.count++;
        })
      Button('Jump to EntryAbility').onClick(() => {
        let wantInfo: Want = {
          bundleName: 'com.example.myapplication', // 替换成AppScope/app.json5里的bundleName
          abilityName: 'EntryAbility'
        };
        this.context.startAbility(wantInfo);
      })
    }
  }
}

V2: 可以使用AppStorageV2实现跨Ability共享。 如下面示例:

import { common, Want } from '@kit.AbilityKit';
import { AppStorageV2 } from '@kit.ArkUI';

@ObservedV2
export class MyStorage {
  @Trace count: number = 0
}

@Entry
@ComponentV2
struct Index {
  @Local storage: MyStorage = AppStorageV2.connect(MyStorage, 'storage', () => new MyStorage())!;
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;

  build() {
    Column() {
      Text(`EntryAbility1 count: ${this.storage.count}`)
        .fontSize(50)
        .onClick(() => {
          this.storage.count++;
        })
      Button('Jump to EntryAbility1').onClick(() => {
        let wantInfo: Want = {
          bundleName: 'com.example.myapplication', // 替换成AppScope/app.json5里的bundleName
          abilityName: 'EntryAbility1'
        };
        this.context.startAbility(wantInfo);
      })
    }
  }
}

import { common, Want } from '@kit.AbilityKit';
import { AppStorageV2 } from '@kit.ArkUI';

@ObservedV2
export class MyStorage {
  @Trace count: number = 0
}

@Entry
@ComponentV2
struct Index1 {
  @Local storage: MyStorage = AppStorageV2.connect(MyStorage, 'storage', () => new MyStorage())!;
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;

    build() {
      Column() {
        Text(`EntryAbility1 count: ${this.storage.count}`)
          .fontSize(50)
          .onClick(() => {
            this.storage.count++;
          })
        Button('Jump to EntryAbility').onClick(() => {
          let wantInfo: Want = {
            bundleName: 'com.example.myapplication', // 替换成AppScope/app.json5里的bundleName
            abilityName: 'EntryAbility'
          };
          this.context.startAbility(wantInfo);
        })
      }
    }
}

如果开发者需要实现类似于\@StorageProp的效果,希望本地的修改不要同步回AppStorage中,而AppStorage的变化又可以通知给使用\@StorageProp装饰器的组件,可以参考以下示例对比。

V1:

// EntryAbility Index.ets
import { common, Want } from '@kit.AbilityKit';
@Entry
@Component
struct Index {
  @StorageProp('count') count: number = 0;
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;

  build() {
    Column() {
      Text(`EntryAbility count: ${this.count}`)
        .fontSize(25)
        .onClick(() => {
          this.count++;
        })
      Button('change Storage Count')
        .onClick(() => {
          AppStorage.setOrCreate('count', AppStorage.get<number>('count') as number + 100);
        })
      Button('Jump to EntryAbility1').onClick(() => {
        let wantInfo: Want = {
          bundleName: 'com.example.myapplication', // 替换成AppScope/app.json5里的bundleName
          abilityName: 'EntryAbility1'
        };
        this.context.startAbility(wantInfo);
      })
    }
  }
}
// EntryAbility1 Index1.ets
import { common, Want } from '@kit.AbilityKit';
@Entry
@Component
struct Index1 {
  @StorageProp('count') count: number = 0;
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;

  build() {
    Column() {
      Text(`EntryAbility1 count: ${this.count}`)
        .fontSize(50)
        .onClick(() => {
          this.count++;
        })
      Button('change Storage Count')
        .onClick(() => {
          AppStorage.setOrCreate('count', AppStorage.get<number>('count') as number + 100);
        })
      Button('Jump to EntryAbility').onClick(() => {
        let wantInfo: Want = {
          bundleName: 'com.example.myapplication', // 替换成AppScope/app.json5里的bundleName
          abilityName: 'EntryAbility'
        };
        this.context.startAbility(wantInfo);
      })
    }
  }
}

V2: 开发者可以借助\@Monitor和\@Local来实现类似的效果,示例如下。

import { common, Want } from '@kit.AbilityKit';
import { AppStorageV2 } from '@kit.ArkUI';

@ObservedV2
export class MyStorage {
  @Trace count: number = 0;
}

@Entry
@ComponentV2
struct Index {
  @Local storage: MyStorage = AppStorageV2.connect(MyStorage, 'storage', () => new MyStorage())!;
  @Local count: number = this.storage.count;
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;

  @Monitor('storage.count')
  onCountChange(mon: IMonitor) {
    console.log(`Index1 ${mon.value()?.before} to ${mon.value()?.now}`);
    this.count = this.storage.count;
  }
  build() {
    Column() {
      Text(`EntryAbility1 count: ${this.count}`)
        .fontSize(25)
        .onClick(() => {
          this.count++;
        })
      Button('change Storage Count')
        .onClick(() => {
          this.storage.count += 100;
        })
      Button('Jump to EntryAbility1').onClick(() => {
        let wantInfo: Want = {
          bundleName: 'com.example.myapplication', // 替换成AppScope/app.json5里的bundleName
          abilityName: 'EntryAbility1'
        };
        this.context.startAbility(wantInfo);
      })
    }
  }
}
import { common, Want } from '@kit.AbilityKit';
import { AppStorageV2 } from '@kit.ArkUI';

@ObservedV2
export class MyStorage {
  @Trace count: number = 0;
}

@Entry
@ComponentV2
struct Index1 {
  @Local storage: MyStorage = AppStorageV2.connect(MyStorage, 'storage', () => new MyStorage())!;
  @Local count: number = this.storage.count;
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;

  @Monitor('storage.count')
  onCountChange(mon: IMonitor) {
    console.log(`Index1 ${mon.value()?.before} to ${mon.value()?.now}`);
    this.count = this.storage.count;
  }

  build() {
    Column() {
      Text(`EntryAbility1 count: ${this.count}`)
        .fontSize(25)
        .onClick(() => {
          this.count++;
        })
      Button('change Storage Count')
        .onClick(() => {
          this.storage.count += 100;
        })
      Button('Jump to EntryAbility').onClick(() => {
        let wantInfo: Want = {
          bundleName: 'com.example.myapplication', // 替换成AppScope/app.json5里的bundleName
          abilityName: 'EntryAbility'
        };
        this.context.startAbility(wantInfo);
      })
    }
  }
}

Environment->调用Ability接口直接获取系统环境变量

V1中,开发者可以通过Environment来获取环境变量,但Environment获取的结果无法直接使用,需要配合AppStorage才能得到对应环境变量的值。 在切换V2的过程中,开发者无需再通过Environment来获取环境变量,可以直接通过UIAbilityContext的config属性获取系统环境变量。 V1: 以languageCode为例。

// 将设备languageCode存入AppStorage中
Environment.envProp('languageCode', 'en');

@Entry
@Component
struct Index {
  @StorageProp('languageCode') languageCode: string = 'en';
  build() {
    Row() {
      Column() {
        // 输出当前设备的languageCode
        Text(this.languageCode)
      }
    }
  }
}

V2: 封装Env类型来传递多个系统环境变量。

// Env.ets
import { ConfigurationConstant } from '@kit.AbilityKit';

export class Env {
  language: string|undefined;
  colorMode: ConfigurationConstant.ColorMode|undefined;
  fontSizeScale: number|undefined;
  fontWeightScale: number|undefined;
}

export let env: Env = new Env();

onCreate里获得需要的系统环境变量:

// EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { env } from '../pages/Env';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    env.language = this.context.config.language;
    env.colorMode = this.context.config.colorMode;
    env.fontSizeScale = this.context.config.fontSizeScale;
    env.fontWeightScale = this.context.config.fontWeightScale;
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index');
  }
}

在页面中获得当前Env的值。

// Index.ets
import { env } from '../pages/Env';

@Entry
@ComponentV2
struct Index {
  build() {
    Row() {
      Column() {
        // 输出当前设备的环境变量
        Text(`languageCode: ${env.language}`).fontSize(20)
        Text(`colorMode: ${env.colorMode}`).fontSize(20)
        Text(`fontSizeScale: ${env.fontSizeScale}`).fontSize(20)
        Text(`fontWeightScale: ${env.fontWeightScale}`).fontSize(20)
      }
    }
  }
}

PersistentStorage->PersistenceV2

V1中PersistentStorage提供了持久化UI数据的能力,而V2则提供了更加方便使用的PersistenceV2接口来替代它。 - PersistentStorage持久化的触发时机依赖AppStorage的观察能力,且与AppStorage耦合,开发者无法自主选择写入或读取持久化数据的时机。 - PersistentStorage使用序列化和反序列化,并没有传入类型,所以在持久化后,会丢失其类型,且对象的属性方法不能持久化。

对于PersistenceV2: - 与PersistenceV2关联的\@ObservedV2对象,其\@Trace属性的变化,会触发整个关联对象的自动持久化。 - 开发者也可以调用PersistenceV2.savePersistenceV2.globalConnect接口来手动触发持久化写入和读取。

V1:

class data {
  name: string = 'ZhangSan';
  id: number = 0;
}

PersistentStorage.persistProp('numProp', 47);
PersistentStorage.persistProp('dataProp', new data());

@Entry
@Component
struct Index {
  @StorageLink('numProp') numProp: number = 48;
  @StorageLink('dataProp') dataProp: data = new data();

  build() {
    Column() {
      // 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果
      Text(`numProp: ${this.numProp}`)
        .onClick(() => {
          this.numProp += 1;
        })
        .fontSize(30)

      // 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果
      Text(`dataProp.name: ${this.dataProp.name}`)
        .onClick(() => {
          this.dataProp.name += 'a';
        })
        .fontSize(30)
      // 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果
      Text(`dataProp.id: ${this.dataProp.id}`)
        .onClick(() => {
          this.dataProp.id += 1;
        })
        .fontSize(30)

    }
    .width('100%')
  }
}

V2:

下面的案例展示了: - 将PersistentStorage的持久化数据迁移到V2的PersistenceV2中去,其中V2对被@Trace标记的数据可以自动持久化,对于非@Trace数据需要开发者自己手动调用save进行持久化。 - 示例中的move函数和需要显示的组件放在了一个ets中,开发者可以定义自己的move函数,并放入合适的位置进行统一迁移操作。

// 迁移到globalConnect
import { PersistenceV2, Type } from '@kit.ArkUI';

// 接受序列化失败的回调
PersistenceV2.notifyOnError((key: string, reason: string, msg: string) => {
  console.error(`error key: ${key}, reason: ${reason}, message: ${msg}`);
});

class Data {
  name: string = 'ZhangSan';
  id: number = 0;
}

@ObservedV2
class V2Data {
  @Trace name: string = '';
  @Trace Id: number = 1;
}

@ObservedV2
export class Sample {
  // 对于复杂对象需要@Type修饰,确保序列化成功
  @Type(V2Data)
  @Trace num: number = 1;
  @Trace V2: V2Data = new V2Data();
}

// 用于判断是否完成数据迁移的辅助数据
@ObservedV2
class StorageState {
  @Trace isCompleteMoving: boolean = false;
}

function move() {
  let movingState = PersistenceV2.globalConnect({type: StorageState, defaultCreator: () => new StorageState()})!;
  if (!movingState.isCompleteMoving) {
    PersistentStorage.persistProp('numProp', 47);
    PersistentStorage.persistProp('dataProp', new Data());
    let num = AppStorage.get<number>('numProp')!;
    let V1Data = AppStorage.get<Data>('dataProp')!;
    PersistentStorage.deleteProp('numProp');
    PersistentStorage.deleteProp('dataProp');

    // V2创建对应数据
    let migrate = PersistenceV2.globalConnect({type: Sample, key: 'connect2', defaultCreator: () => new Sample()})!;  // 使用默认构造函数也可以
    // 赋值数据,@Trace修饰的会自动保存,对于非@Trace对象,也可以调用save保存,如:PersistenceV2.save('connect2'); 
    migrate.num = num;
    migrate.V2.name = V1Data.name;
    migrate.V2.Id = V1Data.id;

    // 将迁移标志设置为true
    movingState.isCompleteMoving = true;
  }
}

move();

@Entry
@ComponentV2
struct Page1 {
  @Local refresh: number = 0;
  // 使用key:connect2存入数据
  @Local p: Sample = PersistenceV2.globalConnect({type: Sample, key:'connect2', defaultCreator:() => new Sample()})!;

  build() {
    Column({space: 5}) {
      // 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果
      Text(`numProp: ${this.p.num}`)
        .onClick(() => {
          this.p.num += 1;
        })
        .fontSize(30)

      // 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果
      Text(`dataProp.name: ${this.p.V2.name}`)
        .onClick(() => {
          this.p.V2.name += 'a';
        })
        .fontSize(30)
      // 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果
      Text(`dataProp.id: ${this.p.V2.Id}`)
        .onClick(() => {
          this.p.V2.Id += 1;
        })
        .fontSize(30)
    }
    .width('100%')
  }
}

存量迁移场景

对于已经使用V1开发的大型应用,一般不太可能做到一次性的从V1迁移到V2,而是分批次和分组件的部分迁移,这就必然会带来V1和V2的混用。

这种场景,一般是父组件是状态管理V1,而迁移的子组件为状态管理V2。为了模拟这种场景,我们举出下面的示例: - 父组件是\@Component,数据源是\@LocalStorageLink。 - 子组件是\@ComponentV2,使用\@Param接受数据源的数据。

这种情况,我们可以通过以下策略进行迁移: - 声明一个\@ObservedV2装饰的class来封装V1的数据。 - 在\@Component和\@ComponentV2之间,定义一个桥接的\@Component自定义组件。 - 在桥接层: - V1->V2的数据同步,可通过\@Watch的监听触发\@ObservedV2装饰的class的属性的赋值。 - V2->V1的数据同步,可通过在\@ObservedV2装饰的class里声明Monitor,通过LocalStorage的API反向通知给V1状态变量。

具体示例如下:

let storage: LocalStorage = new LocalStorage();

@ObservedV2
class V1StorageData {
  @Trace title: string = 'V1OldComponent'
  @Monitor('title')
  onStrChange(monitor: IMonitor) {
    monitor.dirty.forEach((path: string) => {
      console.log(`${path} changed from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`)
      if (path === 'title') {
        storage.setOrCreate('title', this.title);
      }
    })
  }
}
let v1Data: V1StorageData = new V1StorageData();

@Entry(storage)
@Component
struct V1OldComponent {
  @LocalStorageLink('title') title: string = 'V1OldComponent';

  build() {
    Column() {
      Text(`V1OldComponent: ${this.title}`)
        .fontSize(20)
        .onClick(() => {
          this.title = 'new value from V1OldComponent';
        })
      Bridge()
    }
  }
}


@Component
struct Bridge {
  @LocalStorageLink('title')@Watch('titleWatch') title: string = 'Bridge';
  titleWatch() {
    v1Data.title = this.title;
  }

  build() {
    NewV2Component()
  }
}
@ComponentV2
struct NewV2Component {
  build() {
    Column() {
      Text(`NewV2Component: ${v1Data.title}`)
        .fontSize(20)
        .onClick(() => {
          v1Data.title = 'NewV2Component';
        })
    }
  }
}

其他迁移场景

滑动组件

List

开发者可以通过ChildrenMainSize来设置List的子组件在主轴方向的大小信息。

V1:

在状态管理V1中,可以通过\@State装饰观察其api调用。

具体示例如下:

@Entry
@Component
struct ListExample {
  private arr: Array<number> = new Array(10).fill(0);
  private scroller: ListScroller = new ListScroller();
  @State listSpace: number = 10;
  @State listChildrenSize: ChildrenMainSize = new ChildrenMainSize(100);

  build() {
    Column() {
      Button('change Default').onClick(() => {
        this.listChildrenSize.childDefaultSize += 10;
      })

      Button('splice 5').onClick(() => {
        this.listChildrenSize.splice(0, 5, [100, 100, 100, 100, 100]);
      })

      Button('update 5').onClick(() => {
        this.listChildrenSize.update(0, 200);
      })

      List({ space: this.listSpace, scroller: this.scroller }) {
        ForEach(this.arr, (item: number) => {
          ListItem() {
            Text(`item-` + item)
          }.backgroundColor(Color.Pink)
        })
      }
      .childrenMainSize(this.listChildrenSize) // 10
    }
  }
}

V2:

但在状态管理V2中,\@Local只能观察本身的变化,无法观察第一层的变化,又因为ChildrenMainSize定义在框架中,开发者无法使用\@Trace来标注ChildrenMainSize的属性,此时可以使用makeObserved替代。

具体示例如下:

import { UIUtils } from '@kit.ArkUI';

@Entry
@ComponentV2
struct ListExample {
  private arr: Array<number> = new Array(10).fill(0);
  private scroller: ListScroller = new ListScroller();
  listSpace: number = 10;
  // 使用makeObserved的能力来观测ChildrenMainSize
  listChildrenSize: ChildrenMainSize = UIUtils.makeObserved(new ChildrenMainSize(100));

  build() {
    Column() {
      Button('change Default').onClick(() => {
        this.listChildrenSize.childDefaultSize += 10;
      })

      Button('splice 5').onClick(() => {
        this.listChildrenSize.splice(0, 5, [100, 100, 100, 100, 100]);
      })

      Button('update 5').onClick(() => {
        this.listChildrenSize.update(0, 200);
      })

      List({ space: this.listSpace, scroller: this.scroller }) {
        ForEach(this.arr, (item: number) => {
          ListItem() {
            Text(`item-` + item)
          }.backgroundColor(Color.Pink)
        })
      }
      .childrenMainSize(this.listChildrenSize) // 10
    }
  }
}

WaterFlow

开发者可以通过WaterFlowSections来设置WaterFlow瀑布流分组信息。

需要注意的是,数组arr的长度需要与WaterFlowSections的中所有SectionOptions的itemsCount的总和保持一致,否则WaterFlow无法处理,导致UI不刷新。

以下两个示例请按照’push option’ -> ‘splice option’ -> ‘update option’的顺序进行点击。

V1:

在状态管理V1中,可以通过\@State装饰观察其api调用。

具体示例如下:

@Entry
@Component
struct WaterFlowSample {
  @State colors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Blue, Color.Pink];
  @State sections: WaterFlowSections = new WaterFlowSections();
  scroller: Scroller = new Scroller();
  @State private arr: Array<number> = new Array(9).fill(0);
  oneColumnSection: SectionOptions = {
    itemsCount: 4,
    crossCount: 1,
    columnsGap: '5vp',
    rowsGap: 10,
  };
  twoColumnSection: SectionOptions = {
    itemsCount: 2,
    crossCount: 2,
  };
  lastSection: SectionOptions = {
    itemsCount: 3,
    crossCount: 3,
  };

  aboutToAppear(): void {
    let sectionOptions: SectionOptions[] = [this.oneColumnSection, this.twoColumnSection, this.lastSection];
    this.sections.splice(0, 0, sectionOptions);
  }

  build() {
    Column() {
      Text(`${this.arr.length}`)

      Button('push option').onClick(() => {
        let section: SectionOptions = {
          itemsCount: 1,
          crossCount: 1,
        };
        this.sections.push(section);
        this.arr.push(100);
      })

      Button('splice option').onClick(() => {
        let section: SectionOptions = {
          itemsCount: 8,
          crossCount: 2,
        };
        this.sections.splice(0, this.arr.length, [section]);
        this.arr = new Array(8).fill(10);
      })

      Button('update option').onClick(() => {
        let section: SectionOptions = {
          itemsCount: 8,
          crossCount: 2,
        };
        this.sections.update(1, section);
        this.arr = new Array(16).fill(1);
      })

      WaterFlow({ scroller: this.scroller, sections: this.sections }) {
        ForEach(this.arr, (item: number) => {
          FlowItem() {
            Text(`${item}`)
              .border({ width: 1 })
              .backgroundColor(this.colors[item % 6])
              .height(30)
              .width(50)
          }
        })
      }
    }
  }
}

V2:

但在状态管理V2中,\@Local只能观察本身的变化,无法观察第一层的变化,又因为WaterFlowSections定义在框架中,开发者无法使用\@Trace来标注WaterFlowSections的属性,此时可以使用makeObserved替代。

具体示例如下:

import { UIUtils } from '@kit.ArkUI';

@Entry
@ComponentV2
struct WaterFlowSample {
  colors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Blue, Color.Pink];
  // 使用makeObserved的能力来观测WaterFlowSections
  sections: WaterFlowSections = UIUtils.makeObserved(new WaterFlowSections());
  scroller: Scroller = new Scroller();
  @Local private arr: Array<number> = new Array(9).fill(0);
  oneColumnSection: SectionOptions = {
    itemsCount: 4,
    crossCount: 1,
    columnsGap: '5vp',
    rowsGap: 10,
  };
  twoColumnSection: SectionOptions = {
    itemsCount: 2,
    crossCount: 2,
  };
  lastSection: SectionOptions = {
    itemsCount: 3,
    crossCount: 3,
  };

  aboutToAppear(): void {
    let sectionOptions: SectionOptions[] = [this.oneColumnSection, this.twoColumnSection, this.lastSection];
    this.sections.splice(0, 0, sectionOptions);
  }

  build() {
    Column() {
      Text(`${this.arr.length}`)

      Button('push option').onClick(() => {
        let section: SectionOptions = {
          itemsCount: 1,
          crossCount: 1,
        };
        this.sections.push(section);
        this.arr.push(100);
      })

      Button('splice option').onClick(() => {
        let section: SectionOptions = {
          itemsCount: 8,
          crossCount: 2,
        };
        this.sections.splice(0, this.arr.length, [section]);
        this.arr = new Array(8).fill(10);
      })

      Button('update option').onClick(() => {
        let section: SectionOptions = {
          itemsCount: 8,
          crossCount: 2,
        };
        this.sections.update(1, section);
        this.arr = new Array(16).fill(1);
      })

      WaterFlow({ scroller: this.scroller, sections: this.sections }) {
        ForEach(this.arr, (item: number) => {
          FlowItem() {
            Text(`${item}`)
              .border({ width: 1 })
              .backgroundColor(this.colors[item % 6])
              .height(30)
              .width(50)
          }
        })
      }
    }
  }
}

Modifier

attributeModifier

开发者可以通过attributeModifier动态设置组件的属性方法。

V1:

在状态管理V1中,可以通过\@State装饰观察其变化。

具体示例如下:

class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
  isDark: boolean = false;

  applyNormalAttribute(instance: ButtonAttribute): void {
    if (this.isDark) {
      instance.backgroundColor(Color.Black);
    } else {
      instance.backgroundColor(Color.Red);
    }
  }
}

@Entry
@Component
struct AttributeDemo {
  @State modifier: MyButtonModifier = new MyButtonModifier();

  build() {
    Row() {
      Column() {
        Button('Button')
          .attributeModifier(this.modifier)
          .onClick(() => {
            this.modifier.isDark = !this.modifier.isDark;
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

V2:

但在状态管理V2中,\@Local只能观察本身的变化,无法观察第一层的变化,如果要观察attributeModifier的属性变化,可以使用makeObserved替代。

具体示例如下:

import { UIUtils } from '@kit.ArkUI';

class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
  isDark: boolean = false;

  applyNormalAttribute(instance: ButtonAttribute): void {
    if (this.isDark) {
      instance.backgroundColor(Color.Black);
    } else {
      instance.backgroundColor(Color.Red);
    }
  }
}

@Entry
@ComponentV2
struct AttributeDemo {
  // 使用makeObserved的能力观测attributeModifier的属性this.modifier
  modifier: MyButtonModifier = UIUtils.makeObserved(new MyButtonModifier());

  build() {
    Row() {
      Column() {
        Button('Button')
          .attributeModifier(this.modifier)
          .onClick(() => {
            this.modifier.isDark = !this.modifier.isDark;
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

CommonModifier

动态设置组件的属性类。以CommonModifier为例。

V1:

在状态管理V1中,可以通过\@State装饰观察其变化。

具体实例如下:

import { CommonModifier } from '@ohos.arkui.modifier';

class MyModifier extends CommonModifier {
  applyNormalAttribute(instance: CommonAttribute): void {
    super.applyNormalAttribute?.(instance);
  }

  public setGroup1(): void {
    this.borderStyle(BorderStyle.Dotted);
    this.borderWidth(8);
  }

  public setGroup2(): void {
    this.borderStyle(BorderStyle.Dashed);
    this.borderWidth(8);
  }
}

@Component
struct MyImage1 {
  @Link modifier: CommonModifier;

  build() {
    // 此处'app.media.app_icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
    Image($r('app.media.app_icon'))
      .attributeModifier(this.modifier as MyModifier)
  }
}

@Entry
@Component
struct Index {
  @State myModifier: CommonModifier = new MyModifier().width(100).height(100).margin(10);
  index: number = 0;

  build() {
    Column() {
      Button($r('app.string.EntryAbility_label'))
        .margin(10)
        .onClick(() => {
          console.log('Modifier', 'onClick');
          this.index++;
          if (this.index % 2 === 1) {
            (this.myModifier as MyModifier).setGroup1();
            console.log('Modifier', 'setGroup1');
          } else {
            (this.myModifier as MyModifier).setGroup2();
            console.log('Modifier', 'setGroup2');
          }
        })

      MyImage1({ modifier: this.myModifier })
    }
    .width('100%')
  }
}

V2:

但在状态管理V2中,\@Local只能观察本身的变化,无法观察第一层的变化,又因为CommonModifier在框架内是通过其属性触发刷新,此时可以使用makeObserved替代。

具体示例如下:

import { UIUtils } from '@kit.ArkUI';
import { CommonModifier } from '@ohos.arkui.modifier';

class MyModifier extends CommonModifier {
  applyNormalAttribute(instance: CommonAttribute): void {
    super.applyNormalAttribute?.(instance);
  }

  public setGroup1(): void {
    this.borderStyle(BorderStyle.Dotted);
    this.borderWidth(8);
  }

  public setGroup2(): void {
    this.borderStyle(BorderStyle.Dashed);
    this.borderWidth(8);
  }
}

@ComponentV2
struct MyImage1 {
  @Param @Require modifier: CommonModifier;

  build() {
    // 此处'app.media.app_icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
    Image($r('app.media.app_icon'))
      .attributeModifier(this.modifier as MyModifier)
  }
}

@Entry
@ComponentV2
struct Index {
  // 使用makeObserved的能力来观测CommonModifier
  @Local myModifier: CommonModifier = UIUtils.makeObserved(new MyModifier().width(100).height(100).margin(10));
  index: number = 0;

  build() {
    Column() {
      Button($r('app.string.EntryAbility_label'))
        .margin(10)
        .onClick(() => {
          console.log('Modifier', 'onClick');
          this.index++;
          if (this.index % 2 === 1) {
            (this.myModifier as MyModifier).setGroup1();
            console.log('Modifier', 'setGroup1');
          } else {
            (this.myModifier as MyModifier).setGroup2();
            console.log('Modifier', 'setGroup2');
          }
        })

      MyImage1({ modifier: this.myModifier })
    }
    .width('100%')
  }
}

组件Modifier

动态设置组件的属性类。以Text组件为例。

V1:

在状态管理V1中,可以通过\@State装饰观察其变化。

具体示例如下:

import { TextModifier } from '@ohos.arkui.modifier';

class MyModifier extends TextModifier {
  applyNormalAttribute(instance: TextModifier): void {
    super.applyNormalAttribute?.(instance);
  }

  public setGroup1(): void {
    this.fontSize(50);
    this.fontColor(Color.Pink);
  }

  public setGroup2(): void {
    this.fontSize(50);
    this.fontColor(Color.Gray);
  }
}

@Component
struct MyImage1 {
  @Link modifier: TextModifier;
  index: number = 0;

  build() {
    Column() {
      Text('Test')
        .attributeModifier(this.modifier as MyModifier)

      Button($r('app.string.EntryAbility_label'))
        .margin(10)
        .onClick(() => {
          console.log('Modifier', 'onClick');
          this.index++;
          if (this.index % 2 === 1) {
            (this.modifier as MyModifier).setGroup1();
            console.log('Modifier', 'setGroup1');
          } else {
            (this.modifier as MyModifier).setGroup2();
            console.log('Modifier', 'setGroup2');
          }
        })
    }
  }
}

@Entry
@Component
struct Index {
  @State myModifier: TextModifier = new MyModifier().width(100).height(100).margin(10);
  index: number = 0;

  build() {
    Column() {
      MyImage1({ modifier: this.myModifier })

      Button('replace whole')
        .margin(10)
        .onClick(() => {
          this.myModifier = new MyModifier().backgroundColor(Color.Orange);
        })
    }
    .width('100%')
  }
}

V2:

但在状态管理V2中,\@Local只能观察本身的变化,无法观察第一层的变化,此时可以使用makeObserved替代。

具体示例如下:

import { UIUtils } from '@kit.ArkUI';
import { TextModifier } from '@ohos.arkui.modifier';

class MyModifier extends TextModifier {
  applyNormalAttribute(instance: TextModifier): void {
    super.applyNormalAttribute?.(instance);
  }

  public setGroup1(): void {
    this.fontSize(50);
    this.fontColor(Color.Pink);
  }

  public setGroup2(): void {
    this.fontSize(50);
    this.fontColor(Color.Gray);
  }
}

@ComponentV2
struct MyImage1 {
  @Param @Require modifier: TextModifier;
  index: number = 0;

  build() {
    Column() {
      Text('Test')
        .attributeModifier(this.modifier as MyModifier)

      Button($r('app.string.EntryAbility_label'))
        .margin(10)
        .onClick(() => {
          console.log('Modifier', 'onClick');
          this.index++;
          if (this.index % 2 === 1) {
            (this.modifier as MyModifier).setGroup1();
            console.log('Modifier', 'setGroup1');
          } else {
            (this.modifier as MyModifier).setGroup2();
            console.log('Modifier', 'setGroup2');
          }
        })
    }
  }
}

@Entry
@ComponentV2
struct Index {
  // 使用makeObserved的能力观测TextModifier
  @Local myModifier: TextModifier = UIUtils.makeObserved(new MyModifier().width(100).height(100).margin(10));
  index: number = 0;

  build() {
    Column() {
      MyImage1({ modifier: this.myModifier })

      Button('replace whole')
        .margin(10)
        .onClick(() => {
          this.myModifier = UIUtils.makeObserved(new MyModifier().backgroundColor(Color.Orange));
        })
    }
    .width('100%')
  }
}

你可能感兴趣的鸿蒙文章

harmony 鸿蒙\@AnimatableExtend装饰器:定义可动画属性

harmony 鸿蒙管理应用拥有的状态概述

harmony 鸿蒙AppStorage:应用全局的UI状态存储

harmony 鸿蒙基本语法概述

harmony 鸿蒙\@Builder装饰器:自定义构建函数

harmony 鸿蒙\@BuilderParam装饰器:引用\@Builder函数

harmony 鸿蒙创建自定义组件

harmony 鸿蒙自定义组件混用场景指导

harmony 鸿蒙自定义组件成员属性访问限定符使用限制

harmony 鸿蒙自定义组件冻结功能

0  赞