HarmonyOS一杯冰美式的时间 -- UIUtils基础功能

作者:猫猫头啊日期:2026/1/13

一、前言

最近在写状态管理相关的代码,发现 HarmonyOS 的 UIUtils 这个工具类还挺实用的。它主要解决一些状态管理框架在使用过程中遇到的边界问题,比如代理对象、V1/V2 混用、数据绑定这些场景。

今天顺手整理一下它的几个核心功能,方便以后查。

该系列依旧会带着大家,了解,开阔一些不怎么热门的API,也可能是偷偷被更新的API,也可以是好玩的,藏在官方文档的边边角角~当然也会有一些API,之前是我们辛辛苦苦的手撸代码,现在有一个API能帮我们快速实现的,希望大家能找宝藏。

如果您有任何疑问、对文章写的不满意、发现错误、想吐槽或者有更好的想法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏

二、UIUtil

状态管理框架在背后做了很多工作,比如给对象加代理、自动追踪变化、触发 UI 更新等等。但有时候我们需要绕过这些机制,或者在不同版本的状态管理之间做转换,这时候 UIUtils 就派上用场了。

它提供的功能不算多,但每个都挺有针对性的。下面我们一个个看。

三、getTarget:拿到原始对象

这个功能解决什么问题呢?当你把一个普通对象赋值给 @State 或者 @Local 这样的状态变量时,框架会把它包装成代理对象。有时候你需要拿到原始对象做比较,或者传给某些不接受代理对象的 API,这时候就需要 getTarget

1class NonObservedClass {
2  name: string = 'Tom';
3}
4
5let nonObservedClass: NonObservedClass = new NonObservedClass();
6
7@Entry
8@Component
9struct Index {
10  @State someClass: NonObservedClass = nonObservedClass;
11
12  build() {
13    Column() {
14      // 直接比较是 false,因为 this.someClass 是代理对象
15      Text(`this.someClass === nonObservedClass: ${this.someClass === nonObservedClass}`) // false
16      
17      //  getTarget 拿到原始对象,就能比较了
18      Text(`UIUtils.getTarget(this.someClass) === nonObservedClass: ${UIUtils.getTarget(this.someClass) ===
19        nonObservedClass}`) // true
20    }
21  }
22}
23

实际开发中,这种场景其实不算特别常见,但遇到的时候确实能解决问题。比如你要把状态对象序列化,或者传给第三方库,可能就需要先拿到原始对象。

1、V2 状态管理的限制

需要注意的是,在 V2 状态管理中,getTarget 有个限制。状态管理 V2 装饰器会为装饰的变量生成 getter 和 setter 方法,同时为原有变量名添加 __ob_ 前缀。出于性能考虑,getTarget 接口不会对 V2 装饰器生成的前缀进行处理。

也就是说,如果你向 getTarget 传入 @ObservedV2 装饰的类对象实例,返回的对象依旧为对象本身,且被 @Trace 装饰的属性名仍有 __ob_ 前缀。

这个前缀不影响对象属性的 getter 和 setter 方法使用,但如果你需要序列化,可能会遇到问题。比如:

1@ObservedV2
2class FormDataClassV2 {
3  @Trace name: string = '默认名称';
4  @Trace price: number = 0;
5}
6
7@Entry
8@ComponentV2
9struct FormDataClassPage {
10  @Local data: FormDataClassV2 = new FormDataClassV2();
11
12  build() {
13    Column() {
14      Button('序列化')
15        .onClick(() => {
16          // 序列化后会有 __ob_ 前缀
17          console.info('序列化原始值:', JSON.stringify(this.data));
18          // 输出类似:{"__ob_name":"默认名称","__ob_price":0}
19        });
20    }
21    .height('100%')
22    .width('100%');
23  }
24}
25

2、解决方案

如果涉及序列化与反序列化,有两种方案可以去除前缀:

方案一:创建新类转换

创建一个新的类,类中属性名和原来的对象相同,用原来对象的值来初始化新类的对象:

1@ObservedV2
2class FormDataClassV2 {
3  @Trace name: string = '默认名称';
4  @Trace price: number = 0;
5}
6
7class FormDataClass {
8  name: string = '';
9  price: number = 0;
10
11  constructor(v: FormDataClassV2) {
12    this.name = v.name;
13    this.price = v.price;
14  }
15}
16
17@Entry
18@ComponentV2
19struct FormDataClassPage {
20  @Local data: FormDataClassV2 = new FormDataClassV2();
21
22  build() {
23    Column() {
24      Button('序列化')
25        .onClick(() => {
26          console.info('序列化原始值:', JSON.stringify(this.data));
27          // 转换后序列化,没有前缀
28          console.info('序列化转换后:', JSON.stringify(new FormDataClass(this.data)));
29        });
30    }
31    .height('100%')
32    .width('100%');
33  }
34}
35

这种方案比较稳妥,类型安全,但需要额外维护一个类。

方案二:字符串替换

如果序列化后 __ob_ 前缀会导致反序列化异常,可以考虑采用修改序列化后的字符串的方式,去除 __ob_ 前缀:

1@ObservedV2
2class FormDataClassV2 {
3  @Trace name: string = '默认名称';
4  @Trace price: number = 0;
5}
6
7@Entry
8@ComponentV2
9struct FormDataClassPage {
10  @Local data: FormDataClassV2 = new FormDataClassV2();
11
12  build() {
13    Column() {
14      Button('序列化')
15        .onClick(() => {
16          console.info('序列化原始值:', JSON.stringify(this.data));
17          // 用正则替换去除前缀
18          let newData = JSON.stringify(this.data).replace(/__ob_/g, '');
19          console.info('序列化过滤后:', newData);
20        });
21    }
22    .height('100%')
23    .width('100%');
24  }
25}
26

这种方案简单直接,但要注意如果属性名本身包含 __ob_ 字符串,也会被替换掉。所以如果数据比较复杂,建议用方案一。

总的来说,__ob_ 前缀是 V2 状态管理的实现细节,大部分情况下不影响使用。只有在序列化场景下才需要注意这个问题。

四、makeObserved:让普通数据可观察

这个功能在 V2 的状态管理中比较常用。有时候你拿到的是普通对象(比如从 JSON.parse 来的,或者普通 class 实例),但你想让它变成可观察的,这样修改属性时 UI 能自动更新。

1class NonObservedClass {
2  name: string = 'Tom';
3}
4
5@Entry
6@ComponentV2
7struct Index {
8  //  makeObserved 包装后,修改 name 会触发 UI 更新
9  observedClass: NonObservedClass = UIUtils.makeObserved(new NonObservedClass());
10  
11  // 普通对象,修改不会触发更新
12  nonObservedClass: NonObservedClass = new NonObservedClass();
13
14  build() {
15    Column() {
16      Text(`observedClass: ${this.observedClass.name}`)
17        .onClick(() => {
18          this.observedClass.name = 'Jane'; // 会刷新
19        })
20      Text(`nonObservedClass: ${this.nonObservedClass.name}`)
21        .onClick(() => {
22          this.nonObservedClass.name = 'Jane'; // 不会刷新
23        })
24    }
25  }
26}
27

注意,makeObserved 支持的类型还挺多的:普通 class、JSON.parse 返回的对象、Array、Map、Set、Date,还有 collections 里的类型。但如果是 @Sendable 修饰的 class,就不支持了。

五、V1 和 V2 混用的问题

HarmonyOS 的状态管理有两个版本,V1 和 V2。如果你在维护老项目,或者需要逐步迁移,可能会遇到混用的情况。这时候有两个方法能帮上忙。这个如果不是天天刷这个文档,我相信大家一定没有注意到的,嘿嘿

1、enableV2Compatibility:让 V1 状态在 V2 组件中可观察

如果你在 @Component(V1)里定义了 @State,然后想传给 @ComponentV2(V2)使用,直接用是不行的。V2 组件观察不到 V1 状态的变化。

这时候可以用 enableV2Compatibility 包装一下:

1@Observed
2class ObservedClass {
3  name: string = 'Tom';
4}
5
6@Entry
7@Component
8struct CompV1 {
9  @State observedClass: ObservedClass = new ObservedClass();
10
11  build() {
12    Column() {
13      Text(`@State observedClass: ${this.observedClass.name}`)
14        .onClick(() => {
15          this.observedClass.name = 'State'; // 刷新
16        })
17      // 包装后传给 V2 组件,V2 就能观察到第一层的变化了
18      CompV2({ observedClass: UIUtils.enableV2Compatibility(this.observedClass) })
19    }
20  }
21}
22
23@ComponentV2
24struct CompV2 {
25  @Param observedClass: ObservedClass = new ObservedClass();
26
27  build() {
28    Text(`@Param observedClass: ${this.observedClass.name}`)
29      .onClick(() => {
30        this.observedClass.name = 'Param'; // 刷新
31      })
32  }
33}
34

注意,V2 只能观察到第一层的变化。如果 ObservedClass 内部还有嵌套对象,修改嵌套对象的属性,V2 是观察不到的。

2、makeV1Observed:包装成 V1 可观察对象

这个功能和 makeObserved 类似,但包装出来的是 V1 的可观察对象。主要用在需要初始化 @ObjectLink 的场景。

1class Outer {
2  outerValue: string = 'outer';
3  inner: Inner;
4
5  constructor(inner: Inner) {
6    this.inner = inner;
7  }
8}
9
10class Inner {
11  interValue: string = 'inner';
12}
13
14@Entry
15@Component
16struct Index {
17  // makeV1Observed 的返回值可以初始化 @ObjectLink
18  @State outer: Outer = new Outer(UIUtils.makeV1Observed(new Inner()));
19
20  build() {
21    Column() {
22      Child({ inner: this.outer.inner })
23    }
24    .height('100%')
25    .width('100%')
26  }
27}
28
29@Component
30struct Child {
31  @ObjectLink inner: Inner;
32
33  build() {
34    Text(`${this.inner.interValue}`)
35      .onClick(() => {
36        this.inner.interValue += '!';
37      })
38  }
39}
40

makeV1Observed 不支持 collections 类型和 @Sendable 修饰的 class,也不支持 V2 的数据和 makeObserved 的返回值。如果传了不支持的参数,会直接返回原对象,不会报错。

六、makeBinding:创建数据绑定

这个功能在写 @Builder 函数时比较有用。有时候你想让 @Builder 接收一个可读或可写的数据绑定,而不是直接传值,这时候可以用 makeBinding 创建。

1、只读绑定

如果 @Builder 的参数类型是 Binding<T>,你可以用 makeBinding 创建一个只读绑定:

1@Builder
2function CustomButton(num1: Binding<number>) {
3  Row() {
4    Button(`Custom Button: ${num1.value}`)
5      .onClick(() => {
6        // num1.value += 1; 会报错,Binding 类型不支持修改
7      })
8  }
9}
10
11@Entry
12@ComponentV2
13struct CompV2 {
14  @Local number1: number = 5;
15
16  build() {
17    Column() {
18      Text('parent component')
19      // 每次访问 .value 时,会重新执行 getter 函数,拿到最新值
20      CustomButton(
21        UIUtils.makeBinding<number>(
22          () => this.number1 // GetterCallback
23        )
24      )
25    }
26  }
27}
28

2、可写绑定

如果需要双向绑定,可以传两个参数:getter 和 setter:

1@Builder
2function CustomButton(num2: MutableBinding<number>) {
3  Row() {
4    Button(`Custom Button: ${num2.value}`)
5      .onClick(() => {
6        // MutableBinding 支持修改
7        num2.value += 1;
8      })
9  }
10}
11
12@Entry
13@ComponentV2
14struct CompV2 {
15  @Local number2: number = 10;
16
17  build() {
18    Column() {
19      Text('parent component')
20      CustomButton(
21        UIUtils.makeBinding<number>(
22          () => this.number2, // GetterCallback
23          (val: number) => {
24            this.number2 = val; // SetterCallback,修改时会自动调用
25          }
26        )
27      )
28    }
29  }
30}
31

注意,如果创建 MutableBinding 时没传 setter,修改 .value 会报运行时错误。所以需要双向绑定时,setter 是必须的。

七、addMonitor 和 clearMonitor:动态监听

在 V2 的状态管理中,你可以用 @Monitor 装饰器监听状态变化。但有时候你需要动态添加或删除监听,这时候可以用 addMonitorclearMonitor

1@ObservedV2
2class ObservedClass {
3  @Trace name: string = 'Tom';
4
5  onChange(mon: IMonitor) {
6    mon.dirty.forEach((path: string) => {
7      console.info(`ObservedClass property ${path} change from ${mon.value(path)?.before} to ${mon.value(path)?.now}`);
8    });
9  }
10
11  constructor() {
12    // 在构造方法里添加监听,isSynchronous: true 表示同步回调
13    UIUtils.addMonitor(this, 'name', this.onChange, { isSynchronous: true });
14  }
15}
16
17@Entry
18@ComponentV2
19struct Index {
20  @Local observedClass: ObservedClass = new ObservedClass();
21
22  build() {
23    Column() {
24      Text(`name: ${this.observedClass.name}`)
25        .fontSize(20)
26        .onClick(() => {
27          this.observedClass.name = 'Jack';
28          this.observedClass.name = 'Jane';
29        })
30    }
31  }
32}
33

addMonitor 只支持 @ComponentV2@ObservedV2 实例,如果传了不支持的类型会抛运行时错误。

删除监听用 clearMonitor

1Button('clear monitor')
2  .onClick(() => {
3    // 删除指定路径的指定监听函数
4    UIUtils.clearMonitor(this.observedClass, 'age', this.observedClass.onChange);
5    
6    // 如果不传 monitorCallback,会删除该路径的所有监听函数
7    // UIUtils.clearMonitor(this.observedClass, 'age');
8  })
9

八、同步刷新:applySync、flushUpdates、flushUIUpdates

这三个方法都是用来同步刷新状态变化的,但作用范围不同。

1、applySync:刷新闭包内的修改

applySync 接收一个闭包函数,只刷新闭包内的状态修改:

1Button('change size')
2  .onClick(() => {
3    // 闭包内的修改会同步执行,包括更新 @Computed、@Monitor 回调和重新渲染 UI
4    UIUtils.applySync(() => {
5      this.w = 100;
6      this.h = 100;
7      this.message = 'Hello World';
8    });
9    
10    // 动画从刷新后的状态开始
11    this.getUIContext().animateTo({
12      duration: 1000
13    }, () => {
14      this.w = 200;
15      this.h = 200;
16      this.message = 'Hello ArkUI';
17    });
18  })
19

2、flushUpdates:刷新所有之前的修改

flushUpdates 会同步刷新调用之前所有的状态修改:

1Button('change size')
2  .onClick(() => {
3    this.w = 100;
4    this.h = 100;
5    this.message = 'Hello World';
6    
7    // 刷新之前所有的修改
8    UIUtils.flushUpdates();
9    
10    // 动画从刷新后的状态开始
11    this.getUIContext().animateTo({
12      duration: 1000
13    }, () => {
14      this.w = 200;
15      this.h = 200;
16      this.message = 'Hello ArkUI';
17    });
18  })
19

3、flushUIUpdates:只刷新 UI,不执行计算和回调

flushUIUpdates 只同步标脏 UI 节点,不会执行 @Computed 计算和 @Monitor 回调:

1Button('change size')
2  .onClick(() => {
3    this.w = 100;
4    this.h = 100;
5    this.message = 'Hello World';
6    
7    // 只刷新 UI,不执行计算和回调
8    UIUtils.flushUIUpdates();
9    
10    // 动画从刷新后的状态开始
11    this.getUIContext().animateTo({
12      duration: 1000
13    }, () => {
14      this.w = 200;
15      this.h = 200;
16      this.message = 'Hello ArkUI';
17    });
18  })
19

这三个方法在动画场景下比较有用。比如你想在动画开始前确保 UI 已经更新到某个状态,就可以用它们。

注意,这三个方法都不能在 @Computed 里调用,flushUpdatesflushUIUpdates 也不能在 @Monitor 回调里调用。

九、总结

UIUtils 提供的功能看着不多,但每个都挺实用的(非常实用了,属于是)。主要解决这些场景:

  1. 代理对象问题:用 getTarget 拿到原始对象
  2. 普通数据变可观察:用 makeObservedmakeV1Observed
  3. V1/V2 混用:用 enableV2Compatibility 让 V1 状态在 V2 中可观察
  4. 数据绑定:用 makeBinding 创建 BindingMutableBinding
  5. 动态监听:用 addMonitorclearMonitor 管理监听
  6. 同步刷新:用 applySyncflushUpdatesflushUIUpdates 控制刷新时机

遇到状态管理的边界问题时,确实能帮上忙。建议先了解这些功能的存在,需要的时候再查文档。用时不慌~~~

十、最后

如果您有任何疑问、对文章写的不满意、发现错误、想吐槽或者有更好的想法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏

谢谢读者姥爷


HarmonyOS一杯冰美式的时间 -- UIUtils基础功能》 是转载文章,点击查看原文


相关推荐


Elasticsearch 8.13.4 动态同义词实战全解析
detayun2026/1/4

在搜索引擎的江湖里,“词不达意"往往是阻碍用户找到心仪内容的最后一道鸿沟。当用户搜索"番茄"时,如果你的库里只有"西红柿"和"圣女果”,传统的精确匹配只能让用户空手而归。同义词库,便是那把填补语义裂痕的钥匙。然而,在 Elasticsearch 8.13.4 这个版本中,我们不再满足于重启服务来更新词库的"笨办法",我们要的是如丝般顺滑的动态热更新。 今天,我们就来一场技术突围,深度剖析在 ES 8.13.4 时代,如何玩转动态同义词,让你的搜索引擎拥有"自我进化"的灵魂。 一、 告别"文件搬运


卷积神经网络CNN
代码洲学长2025/12/26

CNN简介 卷积神经网络就是一个包括卷积层和池化层的神经网络,主要应用于计算机视觉方面,应用场景包括图像分类、目标检测、面部解锁、自动驾驶等。 整体架构流程 CNN的主要结构为 输入层,隐藏层 和输出层,主体架构主要体现在隐藏层中的网络,依次为卷积层 池化层 然后全连接层直接输出。CNN分别进行了两场卷积和池化 ,最终通过三个全连接层进行输出。 卷积层结构图 input(32, 32, 3) conv(3, 3, 6) relu(30, 30, 6) pool(2, 2, 6)


设计模式——责任链模式实战,优雅处理Kafka消息
KD2025/12/18

一、业务背景 Kafka接收消息,需要A,B,C...多种策略做处理,再通过http请求发送给下游。多种策略混在一起很难维护,通过责任链模式把每种策略的代码收敛到自己的Handler中 二、具体设计 classDiagram Handler <|-- StrategyAHandler Handler <|-- StrategyBHandler Handler <|-- UploadHandler Handler: +void handleUserData(UserContext, Handler


【Perfetto从入门到精通】2. 使用 Perfetto 追踪/分析 APP 的 Native/Java 内存
Lei_official2025/12/10

这个世界就是这样,你从失败中学到的东西可能比成功中学到的东西更多——《Android 传奇》 说起 Android APP 内存分析,我们第一时间想到的,往往是 Android Studio Profiler、MAT 这样的老牌工具,而 Perfetto 的出现,又为其提供了一种更加贴近底层的视角。而且相比于现有的工具,Perfetto 更加擅长于分析 Native 内存占用,可以说是补齐了工程师在这方面的短板。 在内存方向,我计划用2~3篇文章来介绍 Perfetto 的功能、特点、使用方


昨天分享了一套用 Nano Banana PRO做商业 PPT 定制的玩法,还推荐直接去咸鱼接单搞钱。
饼干哥哥2025/11/30

但有人说没有渠道、不知道怎么弄。。。 欸我还能说什么呢?只能是把做小生意的完整逻辑给大家讲一遍,包括:🧵- 怎么选择赛道? 公域流量:闲鱼实操、小红书怎么玩、公众号机会 私域谈单 SOP —、先讲一下认知:什么是 中介思维(Agent Thinking) 很多职场人或想要做副业的小白,最大的误区是觉得自己“必须先成为专家”才能赚钱。想做 PPT 代写觉得要设计大师,想做数据分析觉得要代码精通。这种思维导致你陷入技能学习的无底洞,或者单纯靠堆砌自己的时间去赚钱,不仅累,而且上限很低。一旦停下


LeetCode 377 组合总和 Ⅳ
展菲2026/1/21

文章目录 摘要描述题解答案题解代码分析1. 动态规划的基本思路2. 初始状态3. 状态转移方程4. 为什么这样能计算出排列个数?5. 与组合问题的区别6. 优化:避免不必要的计算 示例测试及结果示例 1:nums = [1,2,3], target = 4示例 2:nums = [9], target = 3示例 3:nums = [1,2], target = 3示例 4:nums = [1], target = 1 时间复杂度空间复杂度进阶问题:如果数组中含有负数问题分析解决方

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客