文章目录
- 先看两个被观察的对象
- `addScore()` 比构造函数更值得看
- `@ObjectLink` 适合拆子组件
- 页面结构怎么看
- 完整代码
- 写在最后
@Observed和@ObjectLink很适合讲“对象级响应式”,但只讲概念会有点飘。这个案例把它放到用户资料和每日任务里,就好理解多了:用户积分变了,资料卡要刷新;任务完成了,任务行也要刷新。
这类页面如果只靠普通对象,很容易出现“数据确实改了,但 UI 没动”的情况。HarmonyOS7 里做对象监听,关键就是把对象本身纳入响应式体系。
先看两个被观察的对象
代码里有两个@Observed类:UserProfile和Task。前者保存用户名、年龄、等级、头像和积分;后者保存任务 id、标题、完成状态和奖励积分。
它们不是随便写的普通类。加上@Observed后,子组件可以通过@ObjectLink观察对象属性变化。这样父组件传给子组件的不是一份死数据,而是可以继续响应更新的对象。
addScore()比构造函数更值得看
用户完成任务后,积分会增加,积分达到等级阈值时还会升级。这个逻辑放在UserProfile.addScore()里,比散落在按钮点击事件中清楚很多。
addScore(pts:number){this.score+=ptsif(this.score>=this.level*100){this.level++this.score=this.score-(this.level-1)*100}}这段代码说明了一个很实用的写法:对象自己的业务规则,尽量放回对象内部。页面只负责触发动作,不要把升级规则、积分计算、UI 刷新混在一个点击事件里。
@ObjectLink适合拆子组件
像用户卡片、任务行这种组件,如果每个字段都用@Prop传,参数会越来越多,而且子组件无法自然感知对象内部变化。@ObjectLink更适合这种“整对象传入,局部字段更新”的场景。
不过它也不是万能的。只有对象被@Observed标记,@ObjectLink才有意义;如果对象结构经常临时加字段,ArkTS 的静态类型也不会喜欢这种写法。
页面结构怎么看
ObservedProfilePage里维护了userProfile和taskList。用户资料卡关注积分和等级,每日任务列表关注完成状态。读代码时可以先看两个类,再看子组件如何接收对象,最后看完成任务时怎么更新积分。
如果你做的是用户中心、任务系统、订单卡片这类页面,这种写法比一堆零散@State更容易维护。
完整代码
下面是完整代码。入口组件名是ObservedProfilePage,适合拿来练习 HarmonyOS7 中对象状态监听和子组件刷新。
// @Observed 与 @ObjectLink 对象监听@ObservedclassUserProfile{name:stringage:numberlevel:numberavatar:stringscore:numberconstructor(name:string,age:number,level:number,avatar:string,score:number){this.name=namethis.age=agethis.level=levelthis.avatar=avatarthis.score=score}addScore(pts:number){this.score+=ptsif(this.score>=this.level*100){this.level++this.score=this.score-(this.level-1)*100}}}@ObservedclassTask{id:numbertitle:stringdone:booleanpoints:numberconstructor(id:number,title:string,done:boolean,points:number){this.id=idthis.title=titlethis.done=donethis.points=points}}// 子组件:用户卡片(@ObjectLink 监听 UserProfile)@Componentstruct UserCard{@ObjectLinkuser:UserProfilebuild(){Column({space:10}){Row({space:12}){Text(this.user.avatar).fontSize(36).width(60).height(60).textAlign(TextAlign.Center).backgroundColor('#EFF6FF').borderRadius(30)Column({space:4}){Text(this.user.name).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#111827')Row({space:8}){Text(`Lv.${this.user.level}`).fontSize(12).fontColor(Color.White).backgroundColor('#2563EB').padding({left:8,right:8,top:2,bottom:2}).borderRadius(10)Text(`年龄:${this.user.age}`).fontSize(12).fontColor('#6B7280')}}.alignItems(HorizontalAlign.Start).layoutWeight(1)Column({space:2}){Text(this.user.score.toString()).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#D97706')Text('积分').fontSize(11).fontColor('#9CA3AF')}.alignItems(HorizontalAlign.Center)}.width('100%').alignItems(VerticalAlign.Center)// 经验进度条Column({space:4}){Row(){Text('经验值').fontSize(12).fontColor('#6B7280').layoutWeight(1)Text(`${this.user.score}/${this.user.level*100}`).fontSize(12).fontColor('#2563EB')}Progress({value:this.user.score,total:this.user.level*100,type:ProgressType.Linear}).color('#2563EB').backgroundColor('#E5E7EB').style({strokeWidth:8}).width('100%')}}.width('100%').padding(16).backgroundColor(Color.White).borderRadius(12).shadow({radius:8,color:'rgba(0,0,0,0.06)',offsetY:2})}}// 子组件:任务行(@ObjectLink 监听 Task)@Componentstruct TaskRow{@ObjectLinktask:TaskonComplete:(pts:number)=>void=()=>{}build(){Row({space:12}){Text(this.task.done?'✅':'⬜').fontSize(22).onClick(()=>{if(!this.task.done){this.task.done=truethis.onComplete(this.task.points)}})Column({space:3}){Text(this.task.title).fontSize(14).fontColor(this.task.done?'#9CA3AF':'#111827').decoration({type:this.task.done?TextDecorationType.LineThrough:TextDecorationType.None})Text(`+${this.task.points}积分`).fontSize(12).fontColor(this.task.done?'#9CA3AF':'#D97706')}.alignItems(HorizontalAlign.Start).layoutWeight(1)if(!this.task.done){Text('去完成').fontSize(12).fontColor('#2563EB').backgroundColor('#EFF6FF').padding({left:10,right:10,top:4,bottom:4}).borderRadius(12).onClick(()=>{this.task.done=truethis.onComplete(this.task.points)})}else{Text('已完成').fontSize(12).fontColor('#059669').backgroundColor('#D1FAE5').padding({left:10,right:10,top:4,bottom:4}).borderRadius(12)}}.width('100%').padding({left:16,right:16,top:12,bottom:12}).backgroundColor(this.task.done?'#FAFAFA':Color.White).borderRadius(10).alignItems(VerticalAlign.Center)}}@Entry@Componentstruct ObservedProfilePage{@StateuserProfile:UserProfile=newUserProfile('张三',25,3,'🧑💻',65)@StatetaskList:Task[]=[newTask(1,'完成每日签到',false,10),newTask(2,'阅读一篇技术文章',false,20),newTask(3,'完成一个练习项目',false,50),newTask(4,'参与社区问答',false,30),newTask(5,'分享学习心得',false,40),]build(){Column({space:0}){// 标题Text('@Observed & @ObjectLink').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#111827').width('100%').padding({left:20,right:20,top:20,bottom:4})Text('监听对象内部属性变化,实现嵌套数据驱动视图').fontSize(13).fontColor('#6B7280').width('100%').padding({left:20,right:20,bottom:12})// 说明Row(){Text('💡').fontSize(14).margin({right:6})Text('@Observed 类 + @ObjectLink 子组件:对象属性变化时触发子组件重渲染').fontSize(12).fontColor('#1E40AF').layoutWeight(1)}.width('100%').padding({left:16,right:16,top:8,bottom:8}).backgroundColor('#DBEAFE').margin({bottom:12}).alignItems(VerticalAlign.Top)Scroll(){Column({space:12}){// 用户卡片UserCard({user:this.userProfile}).margin({left:20,right:20})// 操作按钮Row({space:10}){Button('+10 积分').fontSize(12).fontColor('#D97706').backgroundColor('#FEF3C7').borderRadius(16).padding({left:14,right:14,top:8,bottom:8}).onClick(()=>{this.userProfile.addScore(10)})Button('+50 积分').fontSize(12).fontColor('#059669').backgroundColor('#D1FAE5').borderRadius(16).padding({left:14,right:14,top:8,bottom:8}).onClick(()=>{this.userProfile.addScore(50)})Button('年龄+1').fontSize(12).fontColor('#7C3AED').backgroundColor('#F5F3FF').borderRadius(16).padding({left:14,right:14,top:8,bottom:8}).onClick(()=>{this.userProfile.age++})}.padding({left:20,right:20})// 任务列表标题Text('每日任务(完成任务获得积分)').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#374151').padding({left:20})Column({space:8}){ForEach(this.taskList,(task:Task)=>{TaskRow({task:task,onComplete:(pts:number)=>{this.userProfile.addScore(pts)}}).margin({left:20,right:20})},(task:Task)=>task.id.toString())}// 已完成统计Row({space:12}){Text('✅').fontSize(20)Text(`已完成${this.taskList.filter((t:Task)=>t.done).length}/${this.taskList.length}任务`).fontSize(14).fontColor('#374151').layoutWeight(1)Text(`+${this.taskList.filter((t:Task)=>t.done).reduce((s:number,t:Task)=>s+t.points,0)}积分`).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#D97706')}.width('100%').padding({left:20,right:20,top:12,bottom:12}).backgroundColor('#FFFBEB').margin({bottom:24}).alignItems(VerticalAlign.Center)}}.layoutWeight(1)}.width('100%').height('100%').backgroundColor('#F9FAFB')}}写在最后
这个例子不复杂,但很适合反复看。很多 ArkUI 页面写不顺,其实不是组件不会用,而是状态、结构和反馈没有放在一起设计。