当前位置:首页>鸿蒙APP>鸿蒙开发踩坑笔记:@ObservedV2 和 @Trace 都加了,@LocalBuilder 里的 UI 就是不刷新

鸿蒙开发踩坑笔记:@ObservedV2 和 @Trace 都加了,@LocalBuilder 里的 UI 就是不刷新

  • 2026-10-30 17:30:41
鸿蒙开发踩坑笔记:@ObservedV2 和 @Trace 都加了,@LocalBuilder 里的 UI 就是不刷新

你好,我是华鸿技术栈的曹老师。

最近在写一个页面,碰到一个挺典型的状态管理问题。场景不算复杂——网络请求回来数据,更新页面上的几个字段,按理说 @State 或者 @ObservedV2 加 @Trace 应该能搞定。但偏偏卡在一个地方:数据确实变了,@LocalBuilder 里面的 UI 一动不动。

排查了一圈,发现踩的是一个挺容易忽略的细节。记下来,如果你也碰到类似的情况,这篇应该能帮你省点时间。

问题长什么样

先看一段能复现问题的代码。你用 DevEco Studio 新建一个页面,把下面的代码贴进去跑一下,就能看到效果:

typescriptimport { UIUtils } from '@kit.ArkUI'

@Entry@ComponentV2export struct Index {  @Local person: Person = new Person()

aboutToAppear(): void {    // 模拟网络请求返回数据    setTimeout(() => {      this.person = UIUtils.makeObserved(        {          mine: {            size: '大',            color: '蓝色'          }        })    }, 1000)  }

@LocalBuilder  myBuilder(item: Bag) {    Column() {      Text(item.size)      Text(item.color)    }  }

build() {    Column() {      this.myBuilder(this.person.mine)      Button('change')        .onClick(() => {          let num = Math.random() * 100          this.person.mine.color = 红${num}        })    }    .width('100%')    .height('100%')    .justifyContent(FlexAlign.Center)  }}

@ObservedV2class Bag {  @Trace size: string = '初始size小'  @Trace color: string = '初始color红色';}

class Person {  mine: Bag = new Bag()}运行效果:页面上显示的一直是初始值“初始size小”和“初始color红色”。aboutToAppear 里模拟的网络数据回来了,this.person 也确实被重新赋值了,但 UI 没跟着变。点 change 按钮,this.person.mine.color 被改了,UI 还是不动。

问题来了:@ObservedV2 加了,@Trace 也加了,UIUtils.makeObserved 也用了,为什么还是不刷新?

排查过程遇到这种“数据变了 UI 不动”的问题,通常从几个方向排查。咱们挨个过一遍。

第一,被监听的类有没有加 @ObservedV2?

Bag 类需要被观测,代码里已经用 @ObservedV2 装饰了,里面的 size 和 color 也加了 @Trace。这一步没问题。

第二,网络数据转成可观测对象了吗?

网络请求回来的数据通常是 JSON 字符串,JSON.parse() 之后得到的是普通 JS 对象,不是通过 new 创建的类实例。这种普通对象直接赋值给状态变量,ArkTS 的状态管理机制是感知不到的——因为它没有被包装成“可观测”的数据。

代码里用了 UIUtils.makeObserved() 把字面量对象转成了可深度观测的状态变量,这一步也没问题。

第三,@LocalBuilder 的参数是怎么传进去的?

这是关键。

@LocalBuilder 的传参方式有三种:引用传递、值传递、回调传递。不同方式对状态变化的响应逻辑不一样。

看问题代码里这一行:

typescriptthis.myBuilder(this.person.mine)

这里传的是 this.person.mine,一个 Bag 类型的对象。看起来像是引用传递,但实际上,当你整体替换 this.person 时(在 aboutToAppear 里做的就是这个操作),this.person.mine 这个引用指向的还是老的那个 mine 对象,不是新的。@LocalBuilder 拿到的参数是旧引用,所以 UI 不更新。

实际就是:数据虽然变了,但传进 @LocalBuilder 的那根“绳子”还拴在旧数据上。

怎么修

两个方案,选哪个看你的具体场景。

方案一:拆开传,绕过整体替换的问题

既然整体替换 this.person 会导致 this.person.mine 的引用断掉,那就别整体传 mine 对象,把 mine 里面的属性拆开传进去:

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

@Entry@ComponentV2export struct refreshIndex {  @Local person: Person = new Person();

aboutToAppear(): void {    setTimeout(() => {      this.person = UIUtils.makeObserved(        {          mine: {            size: '大2',            color: '蓝色2'          }        });    }, 1000);  }

@LocalBuilder  myBuilder(item: Bag) {    Column() {      Text(item.size);      Text(item.color);    };  }

build() {    Column() {      // 拆开传,把 person.mine 的属性作为新对象的属性传进去      this.myBuilder({        size: this.person.mine.size,        color: this.person.mine.color      });      Button('change')        .onClick(() => {          let num = Math.random() * 100;          this.person.mine.color = 红${num};        });    }    .width('100%')    .height('100%')    .justifyContent(FlexAlign.Center);  }}

class Person {  mine: Bag = new Bag();}

class Bag {  size: string = '初始size小';  color: string = '初始color红色';}注意,这个方案里 Bag 类不需要加 @ObservedV2 和 @Trace,因为 UIUtils.makeObserved 已经帮你把整个数据转成可观测的了。你在调用 this.myBuilder 时传的是 { size: …, color: … } 这样一个新对象,它里面的值取自 this.person.mine,而 this.person 本身是被 makeObserved 处理过的可观测对象——值一变,这个新对象也会跟着变。

什么时候用方案一:你的数据结构比较简单,@LocalBuilder 只用到了对象里的几个属性,不涉及深层嵌套。

方案二:传整个对象,靠 @ObservedV2 驱动刷新

如果 @LocalBuilder 里需要用到的属性很多,一个个拆开传太啰嗦,那就直接把整个 person 传进去。这时候需要靠 @ObservedV2 和 @Trace 来驱动刷新:

typescript@Entry@ComponentV2export struct refreshIndex {  @Local person: Person = new Person();

aboutToAppear(): void {    setTimeout(() => {      // 注意:这里是逐属性赋值,不是整体替换      this.person.mine.size = '大2';      this.person.mine.color = '蓝色2';    }, 1000);  }

@LocalBuilder  myBuilder(item: Person) {    Column() {      Text(item.mine.size);      Text(item.mine.color);    };  }

build() {    Column() {      this.myBuilder(this.person);      Button('change')        .onClick(() => {          let num = Math.random() * 100;          this.person.mine.color = 红${num};        });    }    .width('100%')    .height('100%')    .justifyContent(FlexAlign.Center);  }}

class Person {  mine: Bag = new Bag();}

@ObservedV2class Bag {  @Trace size: string = '初始size小';  @Trace color: string = '初始color红色';}这里有两个要点:

第一,不能整体替换 this.person。如果你写 this.person = 新对象,this.person 的引用就变了,@LocalBuilder 里面拿到的还是旧引用,UI 不动。所以网络数据回来后,要逐个属性赋值:this.person.mine.size = '大2'。

第二,Bag 类必须保留 @ObservedV2 和 @Trace。这个方案没有用 UIUtils.makeObserved,完全是靠 @ObservedV2 装饰器让 ArkTS 追踪属性变化。

属性多了写起来确实麻烦,可以封装一个方法来批量赋值:

typescriptupdateMine(size: string, color: string) {  this.person.mine.size = size;  this.person.mine.color = color;}什么时候用方案二:数据结构复杂,属性多,@LocalBuilder 需要用到整个对象或者深层嵌套的属性。

两个方案怎么选

对比维度
方案一(拆开传)
方案二(传整体)
核心思路
用 UIUtils.makeObserved 把数据转成可观测,拆属性传给 @LocalBuilder
用 @ObservedV2/@Trace 装饰器驱动刷新,逐属性赋值
是否需要 @ObservedV2
不需要
需要
网络数据怎么赋值
整体替换 this.person
逐属性赋值,不能整体替换
适合什么场景
结构简单,@LocalBuilder 只用几个属性
结构复杂,属性多,需要传整个对象
代码量
少
属性多时较繁琐,需要封装方法

一句话总结

@LocalBuilder 里的 UI 不刷新,十有八九是传参方式的问题。你看一下传给 @LocalBuilder 的那个对象,它的引用是不是在数据更新时断掉了。如果断了——拆开传属性,或者换个层级传对象,问题就解决了。

这不是什么深奥的原理,就是个引用指向的问题。踩过一次,以后一眼就能看出来。

本文由「华鸿技术栈」原创发布,专注鸿蒙原生应用与元服务开发。分享实战代码、源码解析、性能优化及生态前沿。不讲虚的,只上干货。转载请联系授权。

最新文章

随机文章