鸿蒙原生开发手记:徒步迹 - @State/@Prop/@Link 状态管理
掌握 ArkUI 装饰器体系,构建响应式 UI
一、前言
状态管理是声明式 UI 框架的核心。ArkUI 提供了一套装饰器体系,让开发者声明式地定义组件状态和依赖关系,框架自动追踪状态变化并更新 UI。
本文将详解 @State、@Prop、@Link 三个最基础也最重要的装饰器。
二、状态管理概览
ArkUI 装饰器按作用范围分为三层:
| 装饰器 | 作用范围 | 用途 |
|---|---|---|
@State | 组件内部 | 组件私有状态 |
@Prop | 父→子 | 父组件向子组件传数据 |
@Link | 父子双向 | 父子组件双向同步 |
@Provide | 跨级传递 | 祖先→后代(提供者) |
@Consume | 跨级传递 | 后代→祖先(消费者) |
@Watch | 状态监听 | 监听状态变化回调 |
@ObjectLink | 对象属性 | 监听对象属性级变化 |
三、@State - 组件私有状态
3.1 基本用法
@Componentstruct StepCounter{@StatestepCount:number=0;// 状态变量@StateisRecording:boolean=false;@StaterouteName:string='';build(){Column(){Text(`步数:${this.stepCount}`)// 自动响应变化.fontSize(20);Button('加1').onClick(()=>{this.stepCount++;// 状态变化,UI 自动更新});Button(this.isRecording?'记录中':'开始记录').backgroundColor(this.isRecording?'#FF6F00':'#4CAF50').onClick(()=>{this.isRecording=!this.isRecording;});}}}3.2 @State 的深层变化监听
@Componentstruct RouteDisplay{@Stateroute:HikingRoute={id:1,name:'香山徒步',distance:8.5,tags:['森林','山景'],};build(){Column(){Text(this.route.name).fontSize(18);// 修改对象属性 - @State 会自动监听深层变化Button('修改名称').onClick(()=>{this.route={...this.route,name:'新路线名',// 重新赋值触发更新};});}}}注意:@State 监听的是赋值操作,修改对象属性时需要重新赋值才会触发更新。对于数组,需要使用新数组替换。
四、@Prop - 父传子
@Prop 用于父组件向子组件传递数据,是单向数据流:
// 子组件 - 接收父组件传入的数据@Componentstruct RouteCard{@ProprouteName:string='';// 从父组件接收@Propdistance:number=0;@Propdifficulty:string='简单';build(){Column(){Text(this.routeName).fontSize(16).fontWeight(FontWeight.Bold);Text(`距离:${this.distance}km`).fontSize(14).fontColor('#666');Text(`难度:${this.difficulty}`).fontSize(12).fontColor(this.getDifficultyColor());}.padding(12).backgroundColor('#FFF').borderRadius(8);}getDifficultyColor():ResourceColor{if(this.difficulty==='简单')return'#4CAF50';if(this.difficulty==='中等')return'#FF9800';return'#F44336';}}// 父组件 - 传入数据@Entry@Componentstruct RouteListPage{build(){Column(){RouteCard({routeName:'香山徒步',distance:8.5,difficulty:'中等',});}.padding(16);}}@Prop 的限制
- 子组件不能修改 @Prop 的值(只读)
- 父组件数据变化时,@Prop 自动同步更新
- @Prop 是深拷贝,不会影响父组件原始数据
五、@Link - 父子双向同步
@Link 实现父子组件间的数据双向同步:
// 子组件@Componentstruct RecordingControl{@Link@Watch('onStatusChanged')status:string;// 双向绑定@Linkduration:number;onStatusChanged():void{console.log(`状态变更为:${this.status}`);}build(){Column(){// 子组件修改 @Link 变量 → 父组件同步更新Button(this.status==='recording'?'暂停':'继续').onClick(()=>{this.status=this.status==='recording'?'paused':'recording';});Text(`时长:${this.duration}s`);}}}// 父组件@Entry@Componentstruct TrackingPage{@StaterecordingStatus:string='idle';@StaterecordingDuration:number=0;build(){Column(){Text(`状态:${this.recordingStatus}`).fontSize(16);// 使用 $ 前缀传递 @Link 引用RecordingControl({status:$recordingStatus,duration:$recordingDuration,});}}}$ 语法
使用$变量名将 @State 变量的引用传递给 @Link:
// 父组件传递ChildComponent({propVar:this.stateVar,// @Prop: 传值linkVar:$stateVar,// @Link: 传引用})六、实战:徒步迹状态管理
结合"徒步迹"的轨迹记录页面,实际演示状态管理的使用:
// TrackingPage.ets@Entry@Componentstruct TrackingPage{@StateisRecording:boolean=false;@StateisPaused:boolean=false;@Stateduration:number=0;@Statedistance:number=0;privatetimerId:number=-1;build(){Column(){// 统计数据区域Row(){StatItem({label:'时长',value:this.formatTime(this.duration)});StatItem({label:'距离',value:`${this.distance.toFixed(2)}`});StatItem({label:'配速',value:this.calcPace()});}.width('100%').padding(16);// 控制按钮区域Row(){if(!this.isRecording){Button('开始徒步').width(180).height(56).backgroundColor('#4CAF50').borderRadius(28).onClick(()=>this.startRecording());}else{Button(this.isPaused?'继续':'暂停').width(72).height(72).backgroundColor(this.isPaused?'#4CAF50':'#FF9800').borderRadius(36).onClick(()=>{if(this.isPaused)this.resumeRecording();elsethis.pauseRecording();});Button('停止').width(72).height(72).backgroundColor('#F44336').borderRadius(36).margin({left:40}).onClick(()=>this.stopRecording());}}.padding({bottom:40});}.width('100%').height('100%');}startRecording():void{this.isRecording=true;this.timerId=setInterval(()=>{this.duration++;this.distance+=0.005;},1000);}pauseRecording():void{this.isPaused=true;clearInterval(this.timerId);}resumeRecording():void{this.isPaused=false;this.startTimer();}stopRecording():void{this.isRecording=false;this.isPaused=false;clearInterval(this.timerId);// 重置数据this.duration=0;this.distance=0;}startTimer():void{this.timerId=setInterval(()=>{this.duration++;this.distance+=0.005;},1000);}formatTime(seconds:number):string{constm=Math.floor(seconds/60);consts=seconds%60;return`${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`;}calcPace():string{if(this.distance<=0)return'0\'00"';constpace=this.duration/this.distance;return`${Math.floor(pace/60)}'${Math.floor(pace%60).toString().padStart(2,'0')}"`;}}@Componentstruct StatItem{@Proplabel:string='';@Propvalue:string='';build(){Column(){Text(this.value).fontSize(24).fontWeight(FontWeight.Bold);Text(this.label).fontSize(13).fontColor('#666');}.alignItems(HorizontalAlign.Center);}}七、@State 最佳实践
- 最小化状态:只保留需要 UI 响应的变量
- 避免冗余:能从其他状态推导的值不要单独声明
- 类型明确:始终为 @State 变量提供初始值
- 不可变性:尽量使用只读接口
// 好 - 状态最小化@Statedistance:number=0;// 只需存储基础数据// 配速由 duration 和 distance 计算得出,不需要额外状态// 不好 - 状态冗余@Statedistance:number=0;@Statepace:string='0\'00"';// 可以从 distance/duration 推导八、总结
本文详细介绍了 ArkUI 状态管理的三个核心装饰器:@State(组件私有)、@Prop(父传子)、@Link(双向同步)。理解它们的区别和适用场景,是构建正确响应式 UI 的关键。
下一篇文章将学习跨组件通信的 @Provide/@Consume 装饰器。
下一篇预告:鸿蒙原生开发手记:徒步迹 - @Provide/@Consume 跨组件通信
