HarmonyOS makeObserved 怎么用:JSON、三方对象和子线程数据回到 ArkUI 后为什么不刷新
2026/7/21 3:50:51 网站建设 项目流程

HarmonyOS makeObserved 怎么用:JSON、三方对象和子线程数据回到 ArkUI 后为什么不刷新

写 ArkUI 页面时,有一种问题很容易误判:数据确实变了,日志也能打出新值,但界面就是不刷新。很多人第一反应会去查ForEach的 key、组件复用、接口返回时机,最后绕一圈才发现,根因不是列表,也不是按钮事件,而是这份数据没有进入 ArkUI 状态管理的观察链路。

UIUtils.makeObserved解决的就是这类问题:对象不是自己定义的@ObservedV2class,或者数据来自JSON.parse、三方包对象、@Sendable数据、collections.Array这类来源,没法直接给字段加@Trace,但页面又需要在它变化时刷新。这个时候,与其把对象拆成一堆零散@Local字段,不如先把“哪份数据需要被观察”这件事交代清楚。

这篇文章只抓一个问题:外部来的复杂对象,回到 ArkUI 页面以后,怎么让字段变化真正带动 UI 更新。

先看它适合解决什么问题

官方文档里会说makeObserved可以把普通不可观察数据变为可观察数据。写代码时,我会按下面几个场景判断:

数据来源常见现象是否适合makeObserved
JSON.parse返回的对象字段改了,页面不刷新适合
三方库返回的 class 实例不能给 class 字段补@Trace适合
@Sendable数据从子线程回到 UI结果回来了,但直接改字段不稳定适合
collections.Array/Map/Set容器 API 改了数据,希望 UI 跟着动适合
自己能控制的页面模型可以直接写@ObservedV2+@Trace优先用 V2 装饰器
V1 页面里已有@State/@PropmakeObserved硬混可能出运行时问题先划迁移边界

下面用两个案例讲清楚。第一个案例处理taskpool子线程回来的@Sendable数据,第二个案例处理三方 SDK class 不能改源码的问题。两个案例都围绕同一个判断:数据能不能被 ArkUI 观察到。

案例一:子线程处理后的任务列表,回到 UI 后统计不刷新

先看一个容易写坏的版本。接口返回 JSON,页面解析出来以后塞进@Local,然后直接改数组项字段。

interface RemoteTask { id: string; title: string; priority: number; done: boolean; tagsText: string; } @Entry @ComponentV2 struct BadTaskPage { @Local tasks: RemoteTask[] = []; aboutToAppear(): void { const raw: string = '[{"id":"a1","title":"sync user config","priority":3,"done":false,"tagsText":"account/sync"}]'; this.tasks = JSON.parse(raw) as RemoteTask[]; } build() { Column({ space: 12 }) { Text(`Finished ${this.tasks.filter((item: RemoteTask) => item.done).length}`) ForEach(this.tasks, (item: RemoteTask) => { Row() { Checkbox({ name: item.id, group: 'task' }) .select(item.done) .onChange((checked: boolean) => { item.done = checked; }) Text(item.title) } }, (item: RemoteTask) => item.id) } .padding(16) } }

这段代码的问题不在Checkbox,也不在ForEachtasks这个数组被重新赋值时,页面能感知到;但item.done = checked改的是数组项内部字段。这个item来自普通 JSON 对象,字段变化没有声明成可观察变化,所以顶部统计和行状态就可能不同步。

真正复杂一点的页面还会多一层:数据先丢到子线程里排序、清洗、补默认值,回到 UI 线程以后再展示。如果这里没有把状态边界处理好,后面会出现几类现象:

- 勾选某一行,行内显示变了,顶部统计不变。

- 搜索筛选后再勾选,筛选结果和总数对不上。

- 重新拉取数据后,loading 结束了,但列表内部状态像旧的。

- 开发者开始怀疑ForEach复用,实际上是对象字段变化没有被观察。

正确做法是分三层:

  1. 子线程只处理普通数据,不接触 UI 代理。
  2. 回到 UI 线程后,用UIUtils.makeObserved接入可观察链路。
  3. 统计、筛选和行样式都从同一份数据推导,不维护第二份状态。

先把可传输模型单独放在一个文件里。

// TaskRecordModel.ets export interface RawTaskRow { id: string; title: string; priority: number; done: boolean; tagsText: string; } @Sendable export class TaskRecord { id: string = ''; title: string = ''; priority: number = 0; done: boolean = false; tagsText: string = ''; constructor(id: string, title: string, priority: number, done: boolean, tagsText: string) { this.id = id; this.title = title; this.priority = priority; this.done = done; this.tagsText = tagsText; } }

这里有两个细节不能省。

第一,@Sendableclass 里的字段要使用可传递的数据类型。原来我想用tags: string[],本地编译直接报错,所以改成tagsText: string。第二,@Concurrent函数有引用边界,同文件里随手写一个 class 再在并发函数里 new,编译时也会报错。把模型拆成模块导入,边界会清楚很多。

并发函数只做数据处理:

import { taskpool } from '@kit.ArkTS'; import { UIUtils } from '@kit.ArkUI'; import { RawTaskRow, TaskRecord } from './TaskRecordModel'; @Concurrent function normalizeTasks(rawJson: string): TaskRecord[] { const rows = JSON.parse(rawJson) as RawTaskRow[]; return rows .map((row: RawTaskRow) => { return new TaskRecord(row.id, row.title, row.priority, row.done, row.tagsText); }) .sort((a: TaskRecord, b: TaskRecord) => b.priority - a.priority); }

页面回到 UI 线程后再做makeObserved

@Entry @ComponentV2 struct TaskBoardPage { @Local keyword: string = ''; @Local loading: boolean = false; @Local taskList: TaskRecord[] = UIUtils.makeObserved(new Array<TaskRecord>()); @Computed get finishedCount(): number { return this.taskList.filter((item: TaskRecord) => item.done).length; } @Computed get visibleTasks(): TaskRecord[] { const key = this.keyword.trim().toLowerCase(); if (key.length === 0) { return this.taskList; } return this.taskList.filter((item: TaskRecord) => { return item.title.toLowerCase().includes(key) || item.tagsText.toLowerCase().includes(key); }); } aboutToAppear(): void { this.loadRemoteTasks(); } private loadRemoteTasks(): void { this.loading = true; const rawJson = JSON.stringify([ { id: 'a1', title: 'sync user config', priority: 3, done: false, tagsText: 'account/sync' }, { id: 'b2', title: 'build offline index', priority: 5, done: false, tagsText: 'search/cache' }, { id: 'c3', title: 'compress hero images', priority: 4, done: true, tagsText: 'image/performance' } ]); taskpool.execute(normalizeTasks, rawJson) .then((value: Object) => { const rows = value as TaskRecord[]; this.taskList = UIUtils.makeObserved(rows); }) .catch(() => { this.taskList = UIUtils.makeObserved(new Array<TaskRecord>()); }) .finally(() => { this.loading = false; }); } private toggleTask(id: string, checked: boolean): void { const target = this.taskList.find((item: TaskRecord) => item.id === id); if (!target) { return; } target.done = checked; } build() { Column({ space: 14 }) { TextInput({ placeholder: 'Filter by title or tag', text: this.keyword }) .onChange((value: string) => { this.keyword = value; }) Row() { Text(`Finished ${this.finishedCount}/${this.taskList.length}`) .fontSize(16) .fontWeight(FontWeight.Medium) Blank() Button('Reload') .onClick(() => this.loadRemoteTasks()) } .width('100%') if (this.loading) { Text('Loading tasks...') .fontColor('#666666') } else if (this.visibleTasks.length === 0) { Text('No matched tasks') .fontColor('#999999') } else { ForEach(this.visibleTasks, (item: TaskRecord) => { TaskRow({ task: item, onCheckedChange: (id: string, checked: boolean) => { this.toggleTask(id, checked); } }) }, (item: TaskRecord) => item.id) } } .padding(16) } }

子组件只接收一行数据,并把操作抛回父组件。

@ComponentV2 struct TaskRow { @Require @Param task: TaskRecord; @Event onCheckedChange: (id: string, checked: boolean) => void; build() { Row({ space: 10 }) { Checkbox({ name: this.task.id, group: 'task-board' }) .select(this.task.done) .onChange((checked: boolean) => { this.onCheckedChange(this.task.id, checked); }) Column({ space: 4 }) { Text(this.task.title) .fontSize(15) .decoration({ type: this.task.done ? TextDecorationType.LineThrough : TextDecorationType.None }) Text(`P${this.task.priority} - ${this.task.tagsText}`) .fontSize(12) .fontColor('#777777') } .layoutWeight(1) } .width('100%') .padding(12) .borderRadius(8) .backgroundColor(this.task.done ? '#F3F6F4' : '#FFFFFF') } }

这套写法解决的是“异步处理后的对象字段变化,UI 能不能继续追踪”的问题。taskpool返回的是普通TaskRecord[],回到 UI 线程后通过makeObserved接入。后续勾选、统计、筛选都围绕同一份数据变化,不再靠手动刷新数组去撞运气。

案例二:三方 SDK class 不能加 @Trace,字段变化怎么刷新

第二种情况也很常见:数据不是 JSON,也不是自己项目里的模型,而是三方 SDK 或公共包给你的 class。你不能去改它的源码,也不能给它的字段补@Trace。但页面上要显示它的状态,比如在线、离线、风险等级、同步状态、授权状态。

这里先模拟一个三方对象:

class VendorProfile { id: string = ''; displayName: string = ''; online: boolean = false; riskLevel: number = 0; constructor(id: string, displayName: string, online: boolean, riskLevel: number) { this.id = id; this.displayName = displayName; this.online = online; this.riskLevel = riskLevel; } }

如果这个 class 在三方包里,最差的做法是把它转成多个页面字段:

@Local displayName: string = ''; @Local online: boolean = false; @Local riskLevel: number = 0;

这样一开始看着简单,但后面很容易出问题:SDK 对象更新了,你要手动同步三个字段;字段多了以后,页面状态和对象状态迟早会分叉。更稳的写法是保留对象整体,只在进入 UI 状态时包一层。

@ComponentV2 struct VendorProfilePage { @Local profile: VendorProfile = UIUtils.makeObserved( new VendorProfile('u-1001', 'SDK imported user', false, 2) ); private toggleVendorOnline(): void { this.profile.online = !this.profile.online; this.profile.riskLevel = this.profile.online ? 1 : 3; } build() { Column({ space: 12 }) { VendorProfilePanel({ profile: this.profile, onToggleOnline: () => { this.toggleVendorOnline(); } }) } .padding(16) } } @ComponentV2 struct VendorProfilePanel { @Require @Param profile: VendorProfile; @Event onToggleOnline: () => void; build() { Column({ space: 10 }) { Text(this.profile.displayName) .fontSize(16) .fontWeight(FontWeight.Medium) Text(this.profile.online ? 'online' : 'offline') .fontColor(this.profile.online ? '#167A42' : '#9A3412') Text(`risk level: ${this.profile.riskLevel}`) .fontColor('#666666') Button('Toggle online state') .onClick(() => { this.onToggleOnline(); }) } .width('100%') .padding(12) .borderRadius(8) .backgroundColor('#F8FAFC') } }

这个案例和第一个案例的区别在于:第一个问题出在“数据从子线程回来后没有接入 UI 观察链路”,第二个问题出在“对象来源不可控,不能改 class 装饰器”。它们的解决方向相同,都是让外部对象进入页面状态前具备可观察能力;但判断依据不同。

几种方案怎么选

方案适合场景不适合场景
@ObservedV2+@Trace自己能控制的模型,长期维护三方 class、匿名对象、@Sendableclass
UIUtils.makeObservedJSON、三方对象、子线程结果、无法加@Trace的对象普通数字字符串、已经被观察过的对象、V1 状态硬混
拆成多个@Local字段字段很少、对象不需要整体传递字段多、对象会传子组件、状态容易分叉
每次整体重新赋值数组临时兜底、快速验证问题长期方案,容易掩盖真正的状态边界

我更倾向于这样定规则:

- 自己定义的页面模型,优先用@ObservedV2+@Trace

- 外部来的复杂对象,优先在进入 UI 状态时用makeObserved

- 只是一两个简单值,用@Local就够。

- V1 页面不要硬混,先看迁移边界。

能不能封装复用

可以封装,但不要封装成一个什么都接的万能函数。比较稳的是按数据来源封装。

export class ObservableAdapter { static fromJsonArray<T extends Object>(items: T[]): T[] { return UIUtils.makeObserved(items); } static fromExternalObject<T extends Object>(item: T): T { return UIUtils.makeObserved(item); } }

这个封装只表达一件事:外部对象进入 UI 前,统一经过观察能力适配。它不负责业务逻辑,不负责排序,不负责补默认值。排序、清洗、补默认值应该留在 service 或 taskpool 处理函数里。

如果项目里有 Repository,也可以把规则写得更明确:

- Repository 返回普通数据。

- ViewModel 或页面入口负责makeObserved

- 子组件只通过@Param接收对象,通过@Event抛出修改意图。

- 不在子组件里重新包装同一个对象,避免重复代理和排查困难。

我怎么验证这两个案例

这两个案例已经放到一个 HarmonyOS API 23 工程里跑过编译,验证过程里确实抓到了几个问题:

  1. @Sendableclass 里写string[]字段,编译不通过。
  2. V2 子组件的@Param没有默认值时,需要加@Require
  3. @Concurrent函数直接引用同文件 class,编译不通过,要拆到独立模块导入。

最终验证结果:

-:entry:default@CompileArkTS通过。

-:entry:assembleHap通过。

- 两个案例放在同一个临时验证页中编译成功。

功能验证可以按这几步做:

  1. 进入任务列表案例,确认列表按优先级排序。
  2. 勾选一行任务,顶部Finished x/y立刻变化。
  3. 输入search,只显示带search/cache的任务。
  4. 在筛选状态下勾选任务,统计仍然按全量列表计算。
  5. 进入三方对象案例,点击切换按钮,online/offlinerisk level一起变化。
  6. UIUtils.makeObserved(...)临时去掉,再观察字段变化是否还能稳定带动 UI。

最后一步不是为了破坏代码,而是为了确认问题边界:如果没有接入观察链路,数据本身变了,也不代表 UI 一定会刷新。

以后怎么避免这类问题

我会把进入页面的数据先分成三类:

- 页面自己持有的轻状态,用@Local

- 自己能控制的复杂模型,用@ObservedV2@Trace

- 外部进来的复杂对象,回到 UI 线程后用makeObserved接进来。

排查页面不刷新时,也按三个问题查:

  1. 这份数据是谁创建的?
  2. 字段变化发生在哪个线程?
  3. 变化后的对象有没有进入 ArkUI 的观察链路?

只要这三个问题回答清楚,很多“明明改了但页面不动”的问题就不会越修越乱。

还能继续联动哪些知识点

makeObserved不是孤立知识点,它后面可以继续接几类内容:

-canBeObserved:排查对象到底有没有被观察到。

-getTarget:拿到代理前的原始对象时,为什么改了不刷新。

-collections.Array / Map / Set:并发容器和 UI 状态怎么配合。

- V1/V2 混用迁移:旧页面怎么逐步迁到 V2,不把状态边界搅乱。

-Repeat/ForEach:可观察数组和列表渲染怎么避免行状态串行。

这些点都和“数据变了,UI 为什么没动”有关。写项目时不要只记一个 API 名字,要先判断数据来源、线程边界和组件持有关系。

参考资料

- 华为开发者文档:makeObserved接口:将非观察数据变为可观察数据

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-new-makeobserved

- 华为开发者文档:canBeObserved接口:判断对象是否可被观察

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-new-canbeobserved

- 华为开发者文档:TaskPool使用规范-多线程并发

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/task-pool-usage-guidelines

- 华为开发者文档:状态管理V1和V2混用指导

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-v1-v2-mixusage

- 华为开发者文档:Event装饰器:规范组件输出

https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/arkts-new-event

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询