HarmonyOS鸿蒙Next中@ObservedV2不能嵌套对象监听

HarmonyOS鸿蒙Next中@ObservedV2不能嵌套对象监听 场景如下:

我自定义了一个TitleBar,外部通过传参进行控制
问题场景如下:

第一步:选择第一页,会传入数组第一个对象,右侧图片显示正常
第二步:选择第二页,会传入数组第二个对象,右边第一个不能更新图片,还是展示第一页的图片
仅展示部分代码:

页面:

@Local titleBarArray: Array<CustTitleBarParam> = this.initTitleBarData();

initTitleBarData(): Array<CustTitleBarParam> {
    const titleBarArray = new Array<CustTitleBarParam>();
    titleBarArray.push(this.initIndexTitleBar())
    titleBarArray.push(this.initUserCenterTitleBar())
    return titleBarArray;
  }

initUserCenterTitleBar(): CustTitleBarParam {
  const titleBar = new CustTitleBarParam();
  titleBar.rightOneIcon = new CustTitleBarIconModel(
    $r('app.media.svg_title_bar_setting'),
    () => {
    },
  )
  titleBar.rightTwoIcon = new CustTitleBarIconModel(
    $r('app.media.svg_title_bar_scan'),
    () => {
    }
  )
  titleBar.title = new CustTitleBarTitleModel(
    $r('app.string.home_bar_user_center')
  )
  return titleBar;
}

initIndexTitleBar(): CustTitleBarParam {
  const titleBar = new CustTitleBarParam();
  titleBar.leftIcon = new CustTitleBarIconModel(
    $r('app.media.svg_title_bar_menu'),
    () => {

    }
  );
  titleBar.rightOneIcon = new CustTitleBarIconModel(
    $r('app.media.svg_title_bar_search'),
    () => {
      this.getUIContext().getRouter().replaceUrl({
        url: 'pages/SearchPage'
      }, (error) => {

      })
    }
  )
  titleBar.titleTab = new CustTitleBarTabModel()
  return titleBar;
}

CustTitleBar({ titleBarParam: this.titleBarArray[this.currentIndex] })
      .id('title_bar_box')

TitleBar:

@Require @Param titleBarParam: CustTitleBarParam;

@Builder
TitleBarBtn(isTextBtn: boolean, value: string | Resource) {
  if (!isTextBtn) {
    Image(value)
      .width($r('app.float.icon_size_medium'))
      .height($r('app.float.icon_size_medium'))
  } else {
    Text(value)
      .width('100%')
      .margin({
        left: $r('app.float.spacing_medium'),
      })
      .attributeModifier(new GlbTextStyle())
      .textAlign(TextAlign.Center)
  }
}

if (this.titleBarParam.rightOneIcon !== null) {
  Stack() {
    this.TitleBarBtn(this.titleBarParam.rightOneIcon.isText, this.titleBarParam.rightOneIcon.value)
  }
  .id('title_bar_right_btn_one')
  .width('auto')
  .height('auto')
  .alignRules({
    top: { anchor: "__container__", align: VerticalAlign.Top },
    bottom: { anchor: "__container__", align: VerticalAlign.Bottom },
    right: { anchor: "__container__", align: HorizontalAlign.End },
  })
  .onClick(() => {
  })
}


[@ObservedV2](/user/ObservedV2)
export class CustTitleBarParam {
  @Trace title: CustTitleBarTitleModel | null = null;
  @Trace leftIcon: CustTitleBarIconModel | null = null;
  @Trace rightOneIcon: CustTitleBarIconModel | null = null;
  @Trace rightTwoIcon: CustTitleBarIconModel | null = null;
  @Trace search: CustTitleBarSearchModel | null = null;
  @Trace titleTab: CustTitleBarTabModel | null = null;

  constructor() {
  }
}

[@ObservedV2](/user/ObservedV2)
export class CustTitleBarIconModel {
  @Trace isText: boolean;
  @Trace value: Resource | string;
  @Trace onClick: (() => void) | null;

  constructor(value: Resource | string, onClick: (() => void) | null = null, isText: boolean = false) {
    this.isText = isText;
    this.value = value;
    this.onClick = onClick;
  }
}

更多关于HarmonyOS鸿蒙Next中@ObservedV2不能嵌套对象监听的实战教程也可以访问 https://www.itying.com/category-93-b0.html

10 回复

根据你这边的demo分析,可能和ObservedV2无关,您这边@Builder构造函数使用的方案不是引用传递,可能无法触发UI刷新。可以参考以下方案试下。

【背景知识】

【解决方案】

1、Builder只传一个参数时,按引用传递UI会刷新,按值传递UI不刷新

  1. Builder代码示例如下:

    import { util } from '@kit.ArkTS';
    
    class Tmp {
      paramA1: string = '';
    }
    
    // 只设置一个参数
    [@Builder](/user/Builder)
    function test(param: Tmp) {
      Column() {
        HelloComponent({ message: param.paramA1 });
      };
    }
    
    // 只设置一个参数
    [@Builder](/user/Builder)
    function test2(message: string) {
      Column() {
        HelloComponent({ message: message });
      };
    }
    
    @Component
    struct HelloComponent {
      @Prop message: string;
    
      build() {
        Column() {
          Text(this.message);
        };
      }
    }
    
    let globalBuilder2: WrappedBuilder<[string]> = wrapBuilder(test2);
    let globalBuilder: WrappedBuilder<[Tmp]> = wrapBuilder(test);
    
  2. 组件代码示例如下:

    @Entry
    @Component
    export struct WrappedBuilderDemoPage {
      @State message: string = 'message';
    
      build() {
        Row() {
          Column({ space: 10 }) {
            Button(`点击改变builder传值`).onClick(() => {
              this.message = util.generateRandomUUID();
            });
            Column() {
              Text('传递给Builder的参数');
              Text(this.message);
            }.width('100%')
            .alignItems(HorizontalAlign.Start);
    
            Text('按引用传递').width('100%').fontWeight(FontWeight.Bold).fontSize(20).margin({ top: 20 });
            Column() {
              Column() {
                Text('Builder呈现的').margin({ bottom: 10 });
                globalBuilder.builder({ paramA1: this.message }); // 引用传递
              }.alignItems(HorizontalAlign.Start);
            }
            .width('100%')
            .justifyContent(FlexAlign.Start)
            .alignItems(HorizontalAlign.Start);
    
            Text('按值传递').width('100%').fontWeight(FontWeight.Bold).fontSize(20).margin({ top: 20 });
            Column() {
              Column() {
                Text('Builder呈现的').margin({ bottom: 10 });
                globalBuilder2.builder(this.message); // 值传递
              }.alignItems(HorizontalAlign.Start);
            }
            .width('100%')
            .justifyContent(FlexAlign.Start)
            .alignItems(HorizontalAlign.Start);
          }
          .width('100%')
          .padding(20);
        }
        .height('100%');
      }
    }
    

2、需要传两个或两个以上参数时,将多个参数封装为一个类,按照引用传入UI会刷新,拆开值传递UI不刷新

  1. Builder代码示例如下:

    import { util } from '@kit.ArkTS';
    
    interface HelloComponentParam {
      message: string;
      message2: string;
    }
    
    // 多个参数接收
    [@Builder](/user/Builder)
    function test(message: string, message2: string) {
      HelloComponentTwo({
        param: { message: message, message2: message2 }
      });
    }
    
    // 一个参数接收
    [@Builder](/user/Builder)
    function test2(param: HelloComponentParam) {
      HelloComponentTwo({
        param: { message: param.message, message2: param.message2 }
      });
    }
    
    @Component
    struct HelloComponentTwo {
      @Prop
      param: HelloComponentParam = {
        message: '',
        message2: ''
      };
    
      build() {
        Column() {
          Text(this.param.message);
          Text(this.param.message2);
        };
      }
    }
    
    let globalBuilder: WrappedBuilder<[string, string]> = wrapBuilder(test);
    let globalBuilder2: WrappedBuilder<[HelloComponentParam]> = wrapBuilder(test2);
    
  2. 组件代码示例如下:

    @Entry
    @Component
    export struct WrappedBuilderDemoPageTwo {
      @State param: HelloComponentParam = {
        message: 'message',
        message2: 'message',
      };
    
      build() {
        Row() {
          Column({ space: 10 }) {
            Button(`点击改变builder传值`).onClick(() => {
              this.param = {
                message: util.generateRandomUUID(),
                message2: util.generateRandomUUID()
              };
            });
            Column() {
              Text('传递给Builder的参数');
              Text(JSON.stringify(this.param));
            }.width('100%')
            .alignItems(HorizontalAlign.Start);
    
            Text('多个参数拆开传入').width('100%').fontWeight(FontWeight.Bold).fontSize(20).margin({ top: 20 });
            Column() {
              Column() {
                Text('Builder呈现的').margin({ bottom: 10 });
                globalBuilder.builder(this.param.message, this.param.message2);
              }.alignItems(HorizontalAlign.Start);
            }
            .width('100%')
            .justifyContent(FlexAlign.Start)
            .alignItems(HorizontalAlign.Start);
    
            Text('多个参数放在一个对象字面量内').width('100%').fontWeight(FontWeight.Bold).fontSize(20).margin({ top: 20 });
            Column() {
              Column() {
                Text('Builder呈现的').margin({ bottom: 10 });
                globalBuilder2.builder({ message: this.param.message, message2: this.param.message2 });
              }.alignItems(HorizontalAlign.Start);
            }
            .width('100%')
            .justifyContent(FlexAlign.Start)
            .alignItems(HorizontalAlign.Start);
          }
          .width('100%')
          .padding(20);
        }
        .height('100%');
      }
    }
    

更多关于HarmonyOS鸿蒙Next中@ObservedV2不能嵌套对象监听的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


问题已解决 详见7楼,

问题已解决

只能改成字面量传参,这样可以监听得到变化

this.TitleBarBtn({
  value: this.titleBarParam.rightOneIcon.value,
  isText: this.titleBarParam.rightOneIcon.isText,
  onClick: this.titleBarParam.rightOneIcon.onClick
})
@Builder
TitleBarBtn(model: CustTitleBarIconModel) {
  Image(model.value)
    .width($r('app.float.icon_size_medium'))
    .height($r('app.float.icon_size_medium'))
    .visibility(model.isText ? Visibility.None : Visibility.Visible)
    .onClick(() => {
      console.log("ok," + model.isText)
    })
  Text(model.value)
    .width('100%')
    .margin({
      left: $r('app.float.spacing_medium'),
    })
    .attributeModifier(new GlbTextStyle())
    .textAlign(TextAlign.Center)
    .visibility(!model.isText ? Visibility.None : Visibility.Visible)
}

这样就能正常监听到了

但是按照你们API文档的方式,用V2,确实监听不到变化 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-builder#%E4%BD%BF%E7%94%A8componentv2%E8%A3%85%E9%A5%B0%E5%99%A8%E8%A7%A6%E5%8F%91%E5%8A%A8%E6%80%81%E5%88%B7%E6%96%B0

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

  • @ObservedV2装饰器与@Trace装饰器需要配合使用,单独使用@ObservedV2装饰器或@Trace装饰器没有任何作用。
  • @Trace装饰器装饰的属性property变化时,仅会通知property关联的组件进行刷新。
  • 在嵌套类中,嵌套类中的属性property被@Trace装饰且嵌套类被@ObservedV2装饰时,才具有触发UI刷新的能力。
  • 在继承类中,父类或子类中的属性property被@Trace装饰且该property所在类被@ObservedV2装饰时,才具有触发UI刷新的能力。
  • 未被@Trace装饰的属性用在UI中无法感知到变化,也无法触发UI刷新。
  • @ObservedV2的类实例目前不支持使用JSON.stringify进行序列化。
  • 使用@ObservedV2@Trace装饰器的类,需通过new操作符实例化后,才具备被观测变化的能力。

参考

[@ObservedV2装饰器和@Trace装饰器:类属性变化观测-V2所属装饰器-状态管理(V2)-学习UI范式状态管理-UI开发 (ArkTS声明式开发范式)-ArkUI(方舟UI框架)-应用框架 - 华为HarmonyOS开发者](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-new-observedv2-and-trace#trace装饰对象数组)

还有一个原因 可能是在Builder 传数据 不会生效,这是我之前踩的一个坑 调用@Builder装饰的函数默认按值传递。当传递的参数为状态变量时,状态变量的改变不会引起@Builder函数内的UI刷新。所以当使用状态变量的时候,推荐使用按引用传递

cke_4322.png

这是我之前踩的坑 [@Trace装饰Map类型 数据发生改变未监听到-华为开发者问答 | 华为开发者联盟](https://developer.huawei.com/consumer/cn/forum/topic/0204195926454795661?fid=0109140870620153026)

参考:[@Builder装饰器:自定义构建函数-组件扩展-学习UI范式基本语法-UI开发 (ArkTS声明式开发范式)-ArkUI(方舟UI框架)-应用框架 - 华为HarmonyOS开发者](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-builder#按值传递参数)

开发者你好,这个问题确实是 V2状态管理 开发的常见场景。当数组中的对象属性变化时,UI 无法正确更新,主要是因为 [@ObservedV2](/user/ObservedV2) 需要配合 [@Trace](/user/Trace) 装饰器才能实现深度监听。

下面提供了 3 个方案:

解决方案

方案一:为数据模型类添加 @ObservedV2@Trace 装饰器(推荐)

方案说明:确保 CustTitleBarParam 类使用 [@ObservedV2](/user/ObservedV2) 装饰,并在需要监听的属性上使用 [@Trace](/user/Trace) 装饰器。

实现步骤

1. 定义可观察的数据模型类

// CustTitleBarParam.ets
import { ObservedV2, Trace } from '@kit.ArkUI'

[@ObservedV2](/user/ObservedV2)
export class CustTitleBarParam {
  [@Trace](/user/Trace) iconUrl: string | Resource = ''  // 需要监听的图片属性
  [@Trace](/user/Trace) title: string = ''                // 需要监听的标题属性
  // 其他需要监听的属性也要添加 [@Trace](/user/Trace)
  
  constructor(iconUrl?: string | Resource, title?: string) {
    if (iconUrl !== undefined) {
      this.iconUrl = iconUrl
    }
    if (title !== undefined) {
      this.title = title
    }
  }
}

2. 在页面组件中使用数组

// 页面组件
@ComponentV2
struct YourPage {
  @Local titleBarArray: Array<CustTitleBarParam> = this.initTitleBarData()
  @Local currentIndex: number = 0
  
  /**
   * 初始化标题栏数据
   */
  initTitleBarData(): Array<CustTitleBarParam> {
    const titleBarArray = new Array<CustTitleBarParam>()
    titleBarArray.push(new CustTitleBarParam($r('app.media.icon1'), '页面一'))
    titleBarArray.push(new CustTitleBarParam($r('app.media.icon2'), '页面二'))
    return titleBarArray
  }
  
  /**
   * 切换页面时,更新当前标题栏参数
   */
  onPageChange(index: number): void {
    this.currentIndex = index
    // 直接访问数组元素,[@ObservedV2](/user/ObservedV2) + [@Trace](/user/Trace) 会自动触发UI更新
  }
  
  build() {
    Column() {
      // 使用当前的标题栏参数
      TitleBar({ titleBarParam: this.titleBarArray[this.currentIndex] })
      
      // 其他页面内容
    }
  }
}

3. 在 TitleBar 组件中使用 @Param 接收参数

// TitleBar 组件
@ComponentV2
struct TitleBar {
  [@Param](/user/Param) titleBarParam: CustTitleBarParam = new CustTitleBarParam()
  
  @Builder
  TitleBarBtn(isTextBtn: boolean, value: string | Resource) {
    if (!isTextBtn) {
      Image(value)  // 使用传入的 value,会自动响应 titleBarParam.iconUrl 的变化
        .width($r('app.float.icon_size_medium'))
        .height($r('app.float.icon_size_medium'))
    } else {
      Text(value)
        .fontSize(16)
    }
  }
  
  build() {
    Row() {
      // 直接绑定 titleBarParam 的属性,变化会自动更新
      this.TitleBarBtn(false, this.titleBarParam.iconUrl)
      Text(this.titleBarParam.title)
        .fontSize(16)
    }
  }
}

关键点

  • CustTitleBarParam 类必须使用 [@ObservedV2](/user/ObservedV2) 装饰
  • 需要监听的属性(如 iconUrltitle)必须使用 [@Trace](/user/Trace) 装饰
  • 使用 [@Param](/user/Param) 传递对象时,会自动建立响应式连接

方案二:使用数组索引绑定,触发对象替换

方案说明:如果不想修改数据模型类,可以通过替换整个数组元素来触发更新。

在切换页面时替换数组元素:

@ComponentV2
struct YourPage {
  @Local titleBarArray: Array<CustTitleBarParam> = this.initTitleBarData()
  @Local currentIndex: number = 0
  
  /**
   * 切换页面时,创建新对象替换数组元素
   */
  onPageChange(index: number): void {
    this.currentIndex = index
    // 创建新对象替换,触发UI更新
    const newParam = new CustTitleBarParam(
      this.titleBarArray[index].iconUrl,
      this.titleBarArray[index].title
    )
    this.titleBarArray[index] = newParam  // 替换数组元素
  }
  
  build() {
    Column() {
      TitleBar({ titleBarParam: this.titleBarArray[this.currentIndex] })
    }
  }
}

关键点

  • 通过创建新对象并替换数组元素来触发更新
  • 适用于数据模型类无法修改的场景

方案三:使用计算属性或临时变量

方案说明:使用 @Local 状态变量存储当前选中的对象,切换时直接替换整个对象。

@ComponentV2
struct YourPage {
  @Local titleBarArray: Array<CustTitleBarParam> = this.initTitleBarData()
  @Local currentTitleBarParam: CustTitleBarParam = new CustTitleBarParam()
  
  /**
   * 切换页面时,更新当前标题栏参数
   */
  onPageChange(index: number): void {
    // 直接替换整个对象引用,触发UI更新
    this.currentTitleBarParam = this.titleBarArray[index]
  }
  
  build() {
    Column() {
      TitleBar({ titleBarParam: this.currentTitleBarParam })
    }
  }
}

关键点

  • 使用独立的 @Local 变量存储当前对象
  • 切换时直接替换对象引用,简单直接

注意事项

  • @ObservedV2 必须配合 @Trace 使用:只使用 [@ObservedV2](/user/ObservedV2) 装饰类是不够的,属性必须使用 [@Trace](/user/Trace) 装饰才能实现深度监听
  • 数组元素替换:如果直接修改数组元素的属性值,UI 可能不会更新,建议创建新对象替换
  • @Param 的引用传递[@Param](/user/Param) 对于复杂类型是引用传递,确保传入的对象是被 [@ObservedV2](/user/ObservedV2) 装饰的类实例
  • 避免直接修改属性:虽然 [@ObservedV2](/user/ObservedV2) + [@Trace](/user/Trace) 支持属性修改,但数组元素的替换更可靠

如果以上方案对您有帮助,欢迎采纳答案,也欢迎继续提问交流!🙏

好像还需要一个@Type 装饰器给 CustTitleBarParam 中的每一个属性,其他代码没看,你可以去搜一下试试

应该不是这个@Type 是给json序列化用的吧,

他好像能监听到这个对象变了,但是监听不到属性变了

我用@Monitor监听了一下,属性是正确变换的 问题就是两个页面都有右侧图片,切换后图片不更新,它可能以为这个对象没有变化,所以这种问题怎么解决

[@Monitor](/user/Monitor)('titleBarParam')
onValueChange(monitor: IMonitor) {
  let lastValue: CustTitleBarParam = monitor.value()?.before as CustTitleBarParam;
  let curValue: CustTitleBarParam = monitor.value()?.now as CustTitleBarParam;
  console.log('111111111'+lastValue.rightOneIcon?.isText.valueOf())
  console.log('1111111112'+curValue.rightOneIcon?.isText.valueOf())
}

@ObservedV2装饰器

@ObservedV2装饰器在HarmonyOS Next中用于实现对象属性的深度监听。它不支持直接嵌套监听,即无法自动监听嵌套对象内部属性的变化。若需监听嵌套对象,需将嵌套对象本身也用@ObservedV2装饰,或将其属性提升到外层对象进行管理。这是当前ArkTS框架的设计约束。

你遇到的问题核心在于 @ObservedV2 的嵌套监听机制。在 HarmonyOS Next 中,@ObservedV2 装饰的类,其属性如果也是 @ObservedV2 类,默认不会自动触发深层属性的变更监听。

在你的代码中:

  • CustTitleBarParam@ObservedV2 装饰。
  • 它的属性 rightOneIconCustTitleBarIconModel 类型,该类也被 @ObservedV2 装饰。
  • 当你切换页面,传入 titleBarArray 中不同的 CustTitleBarParam 对象时,CustTitleBar 组件接收到的 titleBarParam 引用确实发生了变化,这会被框架捕获并触发UI更新。
  • 但是,CustTitleBarParam 内部属性的变化(例如,从 initIndexTitleBar 创建的对象的 rightOneIcon 切换到 initUserCenterTitleBar 创建的对象的 rightOneIcon),对于 @ObservedV2 的嵌套对象,如果只是外部对象引用切换,而内部对象属性(如 value)的变更监听可能没有建立或触发。

解决方案:

你需要确保嵌套对象的属性变更也能被监听到。有几种方法可以解决:

  1. 为嵌套对象属性也添加 @Trace 装饰器:这已经做了,但关键在于触发更新的方式。
  2. 在赋值时创建新对象:确保在 CustTitleBarParam 中,当 rightOneIcon 等属性需要变化时,不是修改现有 CustTitleBarIconModel 实例的属性,而是创建一个新的 CustTitleBarIconModel 实例并赋值。因为 @ObservedV2 对引用变化更敏感。
  3. 使用 @ObservedV2 的嵌套监听支持:HarmonyOS Next 的 @ObservedV2 支持嵌套监听,但需要确保嵌套对象的类也被正确装饰,并且属性的赋值操作是触发在响应式上下文中(例如,在 @State@Local 修饰的变量改变时)。

根据你的代码,你是在切换整个 CustTitleBarParam 对象,这应该能触发UI更新。问题可能出在 CustTitleBar 组件内部对 titleBarParam.rightOneIcon 的访问上。当 titleBarParam 引用变化时,titleBarParam.rightOneIcon 也应该被识别为新的引用,从而更新 ImageText 组件。

检查点:

  • 确认 currentIndex 变化时,titleBarArray[this.currentIndex] 确实返回了不同的 CustTitleBarParam 实例。
  • 确认 CustTitleBar 组件中的 TitleBarBtn Builder 里使用的 this.titleBarParam.rightOneIcon.value 能正确获取到新实例的资源。

如果上述检查无误,问题可能在于 Image 组件对 Resource 类型资源的缓存或识别。可以尝试强制 Image 组件重新渲染,例如通过给 Image 添加一个 key 属性,绑定到 rightOneIcon.valuerightOneIcon 的某个唯一标识。

修改示例(在 CustTitleBar 组件中):

// 修改 Image 部分,添加 key
Image(value)
  .key(this.titleBarParam.rightOneIcon.value.id) // 假设 Resource 有 id 属性,或者用其他唯一值
  .width($r('app.float.icon_size_medium'))
  .height($r('app.float.icon_size_medium'))

如果 Resource 没有唯一标识,可以用 rightOneIcon 的引用或组合值:

.key(`${this.titleBarParam.rightOneIcon.value}-${this.titleBarParam.rightOneIcon.isText}`)

这能确保当 rightOneIcon 变化时,Image 组件会被销毁并重新创建,从而显示新图片。

另外,确保 CustTitleBarIconModelvalue 属性是 Resource 类型,且 $r('app.media.svg_title_bar_search')$r('app.media.svg_title_bar_setting') 是正确的资源引用。

如果问题依旧,可以简化测试:直接在 CustTitleBar 组件内打印 this.titleBarParam.rightOneIcon.value,确认切换页面时值是否正确变化。

回到顶部