
HarmonyOS makeObserved 怎么用JSON、三方对象和子线程数据回到 ArkUI 后为什么不刷新写 ArkUI 页面时有一种问题很容易误判数据确实变了日志也能打出新值但界面就是不刷新。很多人第一反应会去查ForEach的 key、组件复用、接口返回时机最后绕一圈才发现根因不是列表也不是按钮事件而是这份数据没有进入 ArkUI 状态管理的观察链路。UIUtils.makeObserved解决的就是这类问题对象不是自己定义的ObservedV2class或者数据来自JSON.parse、三方包对象、Sendable数据、collections.Array这类来源没法直接给字段加Trace但页面又需要在它变化时刷新。这个时候与其把对象拆成一堆零散Local字段不如先把“哪份数据需要被观察”这件事交代清楚。这篇文章只抓一个问题外部来的复杂对象回到 ArkUI 页面以后怎么让字段变化真正带动 UI 更新。先看它适合解决什么问题官方文档里会说makeObserved可以把普通不可观察数据变为可观察数据。写代码时我会按下面几个场景判断数据来源常见现象是否适合makeObservedJSON.parse返回的对象字段改了页面不刷新适合三方库返回的 class 实例不能给 class 字段补Trace适合Sendable数据从子线程回到 UI结果回来了但直接改字段不稳定适合collections.Array/Map/Set容器 API 改了数据希望 UI 跟着动适合自己能控制的页面模型可以直接写ObservedV2Trace优先用 V2 装饰器V1 页面里已有State/Prop和makeObserved硬混可能出运行时问题先划迁移边界下面用两个案例讲清楚。第一个案例处理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也不在ForEach。tasks这个数组被重新赋值时页面能感知到但item.done checked改的是数组项内部字段。这个item来自普通 JSON 对象字段变化没有声明成可观察变化所以顶部统计和行状态就可能不同步。真正复杂一点的页面还会多一层数据先丢到子线程里排序、清洗、补默认值回到 UI 线程以后再展示。如果这里没有把状态边界处理好后面会出现几类现象- 勾选某一行行内显示变了顶部统计不变。- 搜索筛选后再勾选筛选结果和总数对不上。- 重新拉取数据后loading 结束了但列表内部状态像旧的。- 开发者开始怀疑ForEach复用实际上是对象字段变化没有被观察。正确做法是分三层子线程只处理普通数据不接触 UI 代理。回到 UI 线程后用UIUtils.makeObserved接入可观察链路。统计、筛选和行样式都从同一份数据推导不维护第二份状态。先把可传输模型单独放在一个文件里。// 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 线程后再做makeObservedEntry ComponentV2 struct TaskBoardPage { Local keyword: string ; Local loading: boolean false; Local taskList: TaskRecord[] UIUtils.makeObserved(new ArrayTaskRecord()); 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 ArrayTaskRecord()); }) .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 装饰器”。它们的解决方向相同都是让外部对象进入页面状态前具备可观察能力但判断依据不同。几种方案怎么选方案适合场景不适合场景ObservedV2Trace自己能控制的模型长期维护三方 class、匿名对象、SendableclassUIUtils.makeObservedJSON、三方对象、子线程结果、无法加Trace的对象普通数字字符串、已经被观察过的对象、V1 状态硬混拆成多个Local字段字段很少、对象不需要整体传递字段多、对象会传子组件、状态容易分叉每次整体重新赋值数组临时兜底、快速验证问题长期方案容易掩盖真正的状态边界我更倾向于这样定规则- 自己定义的页面模型优先用ObservedV2Trace。- 外部来的复杂对象优先在进入 UI 状态时用makeObserved。- 只是一两个简单值用Local就够。- V1 页面不要硬混先看迁移边界。能不能封装复用可以封装但不要封装成一个什么都接的万能函数。比较稳的是按数据来源封装。export class ObservableAdapter { static fromJsonArrayT extends Object(items: T[]): T[] { return UIUtils.makeObserved(items); } static fromExternalObjectT extends Object(item: T): T { return UIUtils.makeObserved(item); } }这个封装只表达一件事外部对象进入 UI 前统一经过观察能力适配。它不负责业务逻辑不负责排序不负责补默认值。排序、清洗、补默认值应该留在 service 或 taskpool 处理函数里。如果项目里有 Repository也可以把规则写得更明确- Repository 返回普通数据。- ViewModel 或页面入口负责makeObserved。- 子组件只通过Param接收对象通过Event抛出修改意图。- 不在子组件里重新包装同一个对象避免重复代理和排查困难。我怎么验证这两个案例这两个案例已经放到一个 HarmonyOS API 23 工程里跑过编译验证过程里确实抓到了几个问题Sendableclass 里写string[]字段编译不通过。V2 子组件的Param没有默认值时需要加Require。Concurrent函数直接引用同文件 class编译不通过要拆到独立模块导入。最终验证结果-:entry:defaultCompileArkTS通过。-:entry:assembleHap通过。- 两个案例放在同一个临时验证页中编译成功。功能验证可以按这几步做进入任务列表案例确认列表按优先级排序。勾选一行任务顶部Finished x/y立刻变化。输入search只显示带search/cache的任务。在筛选状态下勾选任务统计仍然按全量列表计算。进入三方对象案例点击切换按钮online/offline和risk level一起变化。把UIUtils.makeObserved(...)临时去掉再观察字段变化是否还能稳定带动 UI。最后一步不是为了破坏代码而是为了确认问题边界如果没有接入观察链路数据本身变了也不代表 UI 一定会刷新。以后怎么避免这类问题我会把进入页面的数据先分成三类- 页面自己持有的轻状态用Local。- 自己能控制的复杂模型用ObservedV2和Trace。- 外部进来的复杂对象回到 UI 线程后用makeObserved接进来。排查页面不刷新时也按三个问题查这份数据是谁创建的字段变化发生在哪个线程变化后的对象有没有进入 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