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
根据你这边的demo分析,可能和ObservedV2无关,您这边@Builder构造函数使用的方案不是引用传递,可能无法触发UI刷新。可以参考以下方案试下。
【背景知识】
- Builder函数:ArkUI提供了一种轻量的UI元素复用机制@Builder,该自定义组件内部UI结构固定,仅与使用方进行数据传递,开发者可以将重复使用的UI元素抽象成一个方法,在build方法里调用。详见[@Builder装饰器:自定义构建函数](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-builder)。
- 详情请见官方文档:Builder限制条件。
【解决方案】
1、Builder只传一个参数时,按引用传递UI会刷新,按值传递UI不刷新。
-
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); -
组件代码示例如下:
@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不刷新。
-
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); -
组件代码示例如下:
@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刷新。所以当使用状态变量的时候,推荐使用按引用传递。

这是我之前踩的坑 [@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)装饰- 需要监听的属性(如
iconUrl、title)必须使用[@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 的嵌套监听机制。在 HarmonyOS Next 中,@ObservedV2 装饰的类,其属性如果也是 @ObservedV2 类,默认不会自动触发深层属性的变更监听。
在你的代码中:
CustTitleBarParam被@ObservedV2装饰。- 它的属性
rightOneIcon是CustTitleBarIconModel类型,该类也被@ObservedV2装饰。 - 当你切换页面,传入
titleBarArray中不同的CustTitleBarParam对象时,CustTitleBar组件接收到的titleBarParam引用确实发生了变化,这会被框架捕获并触发UI更新。 - 但是,
CustTitleBarParam内部属性的变化(例如,从initIndexTitleBar创建的对象的rightOneIcon切换到initUserCenterTitleBar创建的对象的rightOneIcon),对于@ObservedV2的嵌套对象,如果只是外部对象引用切换,而内部对象属性(如value)的变更监听可能没有建立或触发。
解决方案:
你需要确保嵌套对象的属性变更也能被监听到。有几种方法可以解决:
- 为嵌套对象属性也添加
@Trace装饰器:这已经做了,但关键在于触发更新的方式。 - 在赋值时创建新对象:确保在
CustTitleBarParam中,当rightOneIcon等属性需要变化时,不是修改现有CustTitleBarIconModel实例的属性,而是创建一个新的CustTitleBarIconModel实例并赋值。因为@ObservedV2对引用变化更敏感。 - 使用
@ObservedV2的嵌套监听支持:HarmonyOS Next 的@ObservedV2支持嵌套监听,但需要确保嵌套对象的类也被正确装饰,并且属性的赋值操作是触发在响应式上下文中(例如,在@State或@Local修饰的变量改变时)。
根据你的代码,你是在切换整个 CustTitleBarParam 对象,这应该能触发UI更新。问题可能出在 CustTitleBar 组件内部对 titleBarParam.rightOneIcon 的访问上。当 titleBarParam 引用变化时,titleBarParam.rightOneIcon 也应该被识别为新的引用,从而更新 Image 或 Text 组件。
检查点:
- 确认
currentIndex变化时,titleBarArray[this.currentIndex]确实返回了不同的CustTitleBarParam实例。 - 确认
CustTitleBar组件中的TitleBarBtnBuilder 里使用的this.titleBarParam.rightOneIcon.value能正确获取到新实例的资源。
如果上述检查无误,问题可能在于 Image 组件对 Resource 类型资源的缓存或识别。可以尝试强制 Image 组件重新渲染,例如通过给 Image 添加一个 key 属性,绑定到 rightOneIcon.value 或 rightOneIcon 的某个唯一标识。
修改示例(在 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 组件会被销毁并重新创建,从而显示新图片。
另外,确保 CustTitleBarIconModel 的 value 属性是 Resource 类型,且 $r('app.media.svg_title_bar_search') 和 $r('app.media.svg_title_bar_setting') 是正确的资源引用。
如果问题依旧,可以简化测试:直接在 CustTitleBar 组件内打印 this.titleBarParam.rightOneIcon.value,确认切换页面时值是否正确变化。


