# V1->V2迁移指导 ## 概述 ArkUI状态管理的主要职责是:负责将可观察数据的变化自动同步到UI界面,实现数据驱动的UI刷新,使开发者能更加够专注于UI界面的实现和设计。 在状态管理框架的演进过程中,先后推出了状态管理V1和V2两个版本。V1强调组件层级的状态管理,而V2则增强了对数据对象的深度观察与管理能力,不再局限于组件层级。通过V2,开发者能够更灵活地控制数据和状态,实现更高效的UI刷新。具体V1和V2的区别可以参见[状态管理概述](./arkts-state-management-overview.md)。 ## V1V2使用指引 1. V2是V1的增强版本,为开发者提供更多功能和灵活性。 2. 对于新开发的应用,建议直接使用V2版本范式来进行开发。 3. 对于已经使用V1的应用,如果V1的功能和性能已能满足需求,则不必立即切换到V2。如果开发者在开发过程中受限于V1不能深度观察等特性,则建议开发者尽早规划向V2的迁移,以便未来实现平滑过渡和改进。 4. 对于需要在现阶段混用V1和V2的场景,请参阅[混用文档](./arkts-custom-component-mixed-scenarios.md)。编译器、工具链、IDE对某些不推荐的误用和混用场景会进行校验,虽然开发者可能可以通过特殊手段绕过这些校验,但还是强烈建议开发者遵循[混用文档](./arkts-custom-component-mixed-scenarios.md)的指导,避免因双重代理等问题给应用带来不确定性。 ## 迁移指南的目的 1. 对希望将现有V1应用迁移到V2的开发者,提供系统化的模板和指导,帮助完成V1到V2的迁移。 2. 对希望逐步将V1应用过渡到V2的开发者,提供参考,结合本迁移文档与[混用文档](./arkts-custom-component-mixed-scenarios.md),可以帮助开发者实现逐步改造。 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](#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: ```ts @Entry @Component struct Child { @State val: number = 10; build(){ Text(this.val.toString()) } } ``` V2迁移策略:直接替换。 ```ts @Entry @ComponentV2 struct Child { @Local val: number = 10; build(){ Text(this.val.toString()) } } ``` **复杂类型** V1的@State能够观察复杂对象的第一层属性变化,但V2的@Local无法观察对象内部变化。为了解决这个问题,需要在类上添加@ObservedV2,并在需要观察的属性上添加@Trace。这样,框架就能追踪对象内部的属性变化。 V1: ```ts 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。 ```ts @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实现: ```ts @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。 ```ts @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实现: ```ts @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 ```ts @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实现: ```ts @Component struct Child { @Prop value: number; build() { Text(this.value.toString()) } } @Entry @Component struct Parent { build() { Column(){ Child({ value: 30 }) } } } ``` V2迁移策略:直接替换 ```ts @ComponentV2 struct Child { @Param value: number = 0; build() { Text(this.value.toString()) } } @Entry @ComponentV2 struct Parent { build() { Column(){ Child({ value: 30 }) } } } ``` **复杂类型的单向数据传递** 在V2中,传递复杂类型时,如果希望实现严格的单向数据绑定,防止子组件修改父组件的数据,需要在使用@Param传递复杂对象时进行深拷贝以避免传递对象的引用。 V1实现: ```ts 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迁移策略:使用深拷贝 ```ts @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实现: ```ts @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 ```ts @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: - 改变子组件`Child`的`localValue`,不会同步回父组件`Parent`。 - 父组件更新`value`,通知子组件`Child`更新,并覆写本地子组件`localValue`的值。 ```ts @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`的值。 ```ts @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实现: ```ts @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 ```ts @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实现: ```ts @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 ```ts @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实现: ```ts @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的情况下,依赖属性名进行匹配 ```ts @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实现: ```ts @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可以本地初始化 ```ts @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实现: ```ts @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 ```ts @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实现: ```ts @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 ```ts @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实现: ```ts @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迁移策略:直接替换 ```ts @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实现: ```ts @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迁移策略:同时监听多个变量,以及获取变化前的值 ```ts @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](../reference/apis-arkui/js-apis-window.md#loadcontent9)和[getShared](../reference/apis-arkui/arkui-ts/ts-state-management.md#getshared10)接口实现页面间的状态变量共享。 ``` // EntryAbility.ets import { UIAbility } from '@kit.AbilityKit'; import { window } from '@kit.ArkUI'; export default class EntryAbility extends UIAbility { para:Record = { 'count': 47 }; storage: LocalStorage = new LocalStorage(this.para); onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.loadContent('pages/Page1', this.storage); } } ``` 在下面的示例中,使用\@LocalStorageLink,可以使得开发者本地的修改同步回LocalStorage中。 ``` // Page1.ets // 通过getShared接口获取stage共享的LocalStorage实例 @Entry(LocalStorage.getShared()) @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++; }) } } .onReady((context: NavDestinationContext) => { this.pathStack = context.pathStack; }) } } ``` 使用Navigation时,需要添加配置系统路由表文件src/main/resources/base/profile/route_map.json,并替换pageSourceFile为Page2页面的路径,并且在module.json5中添加:"routerMap": "$profile:route_map"。 ```json { "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"。 ```json { "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的变量。 ```ts // 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('count') as number + 100); }) Button('push to Page2') .onClick(() => { this.pageStack.pushPathByName('Page2', null); }) } } } } ``` ```ts // 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('count') as number + 100); }) } } .onReady((context: NavDestinationContext) => { this.pathStack = context.pathStack; }) } } ``` 在V2中,可以借助\@Local和\@Monitor实现类似的效果。 - \@Local装饰的`count`变量为组件本地的值,其改变不会同步回`storage`。 - \@Monitor监听`storage.count`的变化,当`storage.count`改变时,在\@Monitor的回调里改变本地\@Local的值。 ```ts // 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); }) } } } } ``` ```ts // 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: ```ts 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装饰的属性代替。 ```ts // 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; } } ``` 在`pageOneStack`、`pageTwoStack`和`pageThreeStack`组件内分别创建`MyStorageA`、`MyStorageB`、`MyStorageC`的实例,并通过\@Param传递给其子组件`NavigationContentMsgStack`,从而实现类似LocalStorage实例在子组件树上共享的能力。 ```ts // 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: common.UIAbilityContext= getContext(this) 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: common.UIAbilityContext= getContext(this) 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: common.UIAbilityContext= getContext(this) 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: common.UIAbilityContext= getContext(this) 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: ```ts // EntryAbility Index.ets import { common, Want } from '@kit.AbilityKit'; @Entry @Component struct Index { @StorageProp('count') count: number = 0; private context: common.UIAbilityContext = getContext(this) 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('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); }) } } } ``` ```ts // EntryAbility1 Index1.ets import { common, Want } from '@kit.AbilityKit'; @Entry @Component struct Index1 { @StorageProp('count') count: number = 0; private context: common.UIAbilityContext = getContext(this) 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('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来实现类似的效果,示例如下。 ```ts 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: common.UIAbilityContext= getContext(this) 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); }) } } } ``` ```ts 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: common.UIAbilityContext= getContext(this) 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属性](../reference/apis-ability-kit/js-apis-inner-application-uiAbilityContext.md#属性)获取系统环境变量。 V1: 以`languageCode`为例。 ```ts // 将设备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.ts 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.save](./arkts-new-persistencev2.md#save手动持久化数据)和[PersistenceV2.connect](./arkts-new-persistencev2.md#connect创建或获取储存的数据)接口来手动触发持久化写入和读取。 V1: ``` PersistentStorage.persistProp('aProp', 47); @Entry @Component struct Index { @StorageLink('aProp') aProp: number = 48; build() { Row() { Column() { // 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果 Text(`${this.aProp}`) .onClick(() => { this.aProp += 1; }) } } } } ``` V2: 下面的案例展示了: - 对标V1的`PersistentStorage`能力:`aProp`的改变自动触发`PersistenceV2`的持久化。 - 对比V1的`PersistentStorage`能力增强:`bProp`是非状态变量,其变化不能被观察和监听,但是开发者仍然可以主动调用[PersistenceV2.save](./arkts-new-persistencev2.md#save手动持久化数据)接口,进行持久化。 - 点击`aProp`,UI刷新。 - 点击`bProp`,UI没有刷新。 - 点击`save storage`,触发`PersistentStorage`链接数据的落盘。 - 退出重启应用,Text组件显示的`aProp`和`bProp`为上次改变的值。 ``` import { PersistenceV2 } from '@kit.ArkUI'; // 数据中心 @ObservedV2 class Storage { @Trace aProp: number = 0; bProp: number = 10; } // 接受序列化失败的回调 PersistenceV2.notifyOnError((key: string, reason: string, msg: string) => { console.error(`error key: ${key}, reason: ${reason}, message: ${msg}`); }); @Entry @ComponentV2 struct Page1 { // 在PersistenceV2中创建一个key为Sample的键值对(如果存在,则返回PersistenceV2中的数据),并且和prop关联 @Local storage: Storage = PersistenceV2.connect(Storage, () => new Storage())!; build() { Column() { Text(`@Trace aProp: ${this.storage.aProp}`) .fontSize(30) .onClick(() => { this.storage.aProp++; }) Text(`bProp:: ${this.storage.bProp}`) .fontSize(30) .onClick(() => { // 页面不刷新,但是bProp的值改变了 this.storage.bProp++; }) Button('save storage') .onClick(() => { // 和V1不同,PersistenceV2不依赖状态变量的观察能力,开发者可以主动持久化 PersistenceV2.save(Storage); }) } } } ``` ## 存量迁移场景 对于已经使用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](../reference/apis-arkui/arkui-ts/ts-container-list.md#childrenmainsize12)来设置List的子组件在主轴方向的大小信息。 V1: 在状态管理V1中,可以通过[\@State](./arkts-state.md)装饰观察其api调用。 具体示例如下: ```ts @Entry @Component struct ListExample { private arr: Array = 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](./arkts-new-local.md)只能观察本身的变化,无法观察第一层的变化,又因为ChildrenMainSize定义在框架中,开发者无法使用[\@Trace](./arkts-new-observedV2-and-trace.md)来标注ChildrenMainSize的属性,此时可以使用[makeObserved](./arkts-new-makeObserved.md)替代。 具体示例如下: ```ts import { UIUtils } from '@kit.ArkUI'; @Entry @ComponentV2 struct ListExample { private arr: Array = 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](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md#waterflowsections12)来设置WaterFlow瀑布流分组信息。 需要注意的是,数组arr的长度需要与WaterFlowSections的中所有SectionOptions的itemsCount的总和保持一致,否则WaterFlow无法处理,导致UI不刷新。 以下两个示例请按照'push option' -> 'splice option' -> 'update option'的顺序进行点击。 V1: 在状态管理V1中,可以通过[\@State](./arkts-state.md)装饰观察其api调用。 具体示例如下: ```ts @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 = 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](./arkts-new-local.md)只能观察本身的变化,无法观察第一层的变化,又因为WaterFlowSections定义在框架中,开发者无法使用[\@Trace](./arkts-new-observedV2-and-trace.md)来标注WaterFlowSections的属性,此时可以使用[makeObserved](./arkts-new-makeObserved.md)替代。 具体示例如下: ```ts 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 = 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](../reference/apis-arkui/arkui-ts/ts-universal-attributes-attribute-modifier.md#attributemodifier)动态设置组件的属性方法。 V1: 在状态管理V1中,可以通过[\@State](./arkts-state.md)装饰观察其变化。 具体示例如下: ```ts class MyButtonModifier implements AttributeModifier { 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](./arkts-new-local.md)只能观察本身的变化,无法观察第一层的变化,如果要观察attributeModifier的属性变化,可以使用[makeObserved](./arkts-new-makeObserved.md)替代。 具体示例如下: ```ts import { UIUtils } from '@kit.ArkUI'; class MyButtonModifier implements AttributeModifier { 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](../reference/apis-arkui/arkui-ts/ts-universal-attributes-attribute-modifier.md#自定义modifier)为例。 V1: 在状态管理V1中,可以通过[\@State](./arkts-state.md)装饰观察其变化。 具体实例如下: ```ts 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](./arkts-new-local.md)只能观察本身的变化,无法观察第一层的变化,又因为[CommonModifier](../reference/apis-arkui/arkui-ts/ts-universal-attributes-attribute-modifier.md#自定义modifier)在框架内是通过其属性触发刷新,此时可以使用[makeObserved](./arkts-new-makeObserved.md)替代。 具体示例如下: ```ts 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%') } } ``` #### 组件Modfier 动态设置组件的属性类。以Text组件为例。 V1: 在状态管理V1中,可以通过[\@State](./arkts-state.md)装饰观察其变化。 具体示例如下: ```ts 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](./arkts-new-local.md)只能观察本身的变化,无法观察第一层的变化,此时可以使用[makeObserved](./arkts-new-makeObserved.md)替代。 具体示例如下: ```ts 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%') } } ```