做一个高颜值番茄钟,吃透 @ObservedV2 与 @Trace 新装饰器
很多同学学习 ArkUI V2 状态管理时总觉得「ObservedV2 Trace」的组合概念懂了但一到实际项目就不知道该怎么用、什么时候用。其实最适合入门的实战场景就是计时器类应用——状态集中在一个对象里、属性频繁变化、对刷新粒度要求高刚好能完美体现 V2 状态管理的核心优势。本文就带你实现一个高颜值番茄钟应用完整支持 25 分钟专注计时、暂停重置、环形进度动画、专注次数统计。全程基于ObservedV2 Trace架构不用层层传递状态、不用手动刷新引用修改对象内部属性即可自动驱动 UI 更新代码简洁优雅复制即可直接运行。一、为什么番茄钟最适合练手 V2 状态管理1.1 旧版写法的经典痛点如果用 ArkUI V1 的State管理计时器对象会遇到所有人都踩过的坑直接修改timer.remainSeconds不会触发 UI 刷新——因为State只能监听变量本身的引用变化无法感知对象内部属性的变更。旧版的两种解决方案都不够优雅方案一每次修改属性后重新赋值整个对象强制触发引用变更写法丑陋且有性能损耗方案二给类加Observed装饰子组件用ObjectLink接收必须拆分组件才能生效层级越深代码越繁琐1.2 V2 架构的完美适配ObservedV2 Trace的组合刚好彻底解决了这个问题自动监听内部属性给类加ObservedV2给需要监听的属性加Trace修改内部属性自动触发 UI 更新无需拆分子组件父组件内直接修改对象属性即可生效不用强制拆分ObjectLink子组件细粒度更新只有用到被修改属性的 UI 节点会重绘未关联的节点完全不受影响性能更优番茄钟的状态是典型的「单对象、多属性、频繁变更」场景用来练手 V2 状态管理既能直观感受到新旧写法的差异又能落地成一个可日常使用的工具。二、核心装饰器快速回顾在写代码之前先快速回顾三个核心装饰器的职责对应三个不同层级装饰器作用对象核心作用对应旧版ObservedV2类class让该类的实例具备可观察能力是 V2 状态体系的基础ObservedTrace类的属性标记该属性变化时触发 UI 更新实现细粒度控制无对应旧版是全对象监听Local组件内变量声明组件本地状态基于 V2 架构实现State核心原则只有被 Trace 标记的属性变化时才会触发 UI 刷新。不需要驱动界面的属性不用加既能减少不必要的重绘也能让状态逻辑更清晰。三、实战高颜值番茄钟完整实现3.1 第一步定义计时器数据模型这是 V2 状态管理的核心层。我们把番茄钟的所有状态和逻辑封装到一个独立的类中用ObservedV2装饰类给所有需要驱动 UI 的属性加上Trace。新建entry/src/main/ets/model/TomatoTimer.etsimport{hilog}fromkit.PerformanceAnalysisKit;constTAGTomatoTimer;/** * 番茄钟计时器模型 * 基于ArkUI V2状态管理属性变更自动触发UI更新 */ObservedV2exportclassTomatoTimer{// 专注时长秒默认25分钟TracetotalFocusSeconds:number25*60;// 休息时长秒默认5分钟TracetotalBreakSeconds:number5*60;// 当前剩余秒数TraceremainSeconds:number25*60;// 是否正在运行TraceisRunning:booleanfalse;// 累计完成专注次数TracefocusCount:number0;// 当前模式true休息中false专注中TraceisBreakMode:booleanfalse;// 定时器IDprivatetimerId:number-1;/** * 开始计时 */start(){if(this.isRunning)return;this.isRunningtrue;this.timerIdsetInterval((){if(this.remainSeconds0){// 直接修改属性自动触发UI更新this.remainSeconds--;}else{// 计时结束切换模式this.completeOneRound();}},1000);}/** * 暂停计时 */pause(){if(!this.isRunning)return;this.isRunningfalse;clearInterval(this.timerId);this.timerId-1;}/** * 重置当前计时 */reset(){this.pause();this.remainSecondsthis.isBreakMode?this.totalBreakSeconds:this.totalFocusSeconds;}/** * 完成一轮计时自动切换模式 */privatecompleteOneRound(){this.pause();if(!this.isBreakMode){// 专注结束增加计数进入休息this.focusCount;this.isBreakModetrue;this.remainSecondsthis.totalBreakSeconds;hilog.info(0x0000,TAG,专注完成进入休息模式);}else{// 休息结束回到专注this.isBreakModefalse;this.remainSecondsthis.totalFocusSeconds;hilog.info(0x0000,TAG,休息结束进入专注模式);}}/** * 格式化秒数为 MM:SS */getformatTime():string{constminutesMath.floor(this.remainSeconds/60);constsecondsthis.remainSeconds%60;return${String(minutes).padStart(2,0)}:${String(seconds).padStart(2,0)};}/** * 当前进度百分比0-100 */getprogress():number{consttotalthis.isBreakMode?this.totalBreakSeconds:this.totalFocusSeconds;return((total-this.remainSeconds)/total)*100;}/** * 销毁计时器释放资源 */destroy(){this.pause();}}代码核心亮点所有状态变更都是直接修改属性值没有任何替换引用、强制刷新的操作formatTime和progress是计算属性依赖的Trace属性变化时会自动重新计算业务逻辑完全封装在类内部页面只负责渲染和调用职责分离清晰3.2 第二步完整页面 UI 实现我们实现一个渐变背景 环形进度条的高颜值界面包含时间显示、模式状态、操作按钮、专注统计四个模块。打开entry/src/main/ets/pages/Index.ets完整代码如下复制即可直接运行import{TomatoTimer}from../model/TomatoTimer;import{promptAction}fromkit.ArkUI;EntryComponentstruct TomatoClockPage{// 组件本地状态使用V2推荐的Local装饰器Localtimer:TomatoTimernewTomatoTimer();aboutToDisappear(){// 页面销毁时释放定时器this.timer.destroy();}build(){Stack({alignContent:Alignment.Center}){// 渐变背景LinearGradient([[0,this.timer.isBreakMode?#E0F7FA:#FFF3E0],[1,this.timer.isBreakMode?#B2EBF2:#FFE0B2]]).width(100%).height(100%);Column({space:40}){// 顶部标题与状态Column({space:8}){Text(this.timer.isBreakMode?☕ 休息中: 专注中).fontSize(20).fontWeight(FontWeight.Medium).fontColor(this.timer.isBreakMode?#00838F:#E65100);Text(已完成${this.timer.focusCount}个番茄).fontSize(14).fontColor(#666666);}// 环形进度 时间显示Stack({alignContent:Alignment.Center}){// 底层背景圆环Circle({width:260,height:260}).strokeWidth(16).strokeColor(#FFFFFF80).fillOpacity(0);// 上层进度圆环Circle({width:260,height:260}).strokeWidth(16).strokeColor(this.timer.isBreakMode?#00ACC1:#FF8F00).fillOpacity(0).strokeDashArray([260*3.14,260*3.14]).strokeDashOffset(260*3.14*(1-this.timer.progress/100)).rotation({x:0,y:0,z:1,angle:-90}).animation({duration:1000,curve:Curve.Linear});// 中间时间文字Column({space:12}){Text(this.timer.formatTime).fontSize(56).fontWeight(FontWeight.Bold).fontColor(#333333).fontFamily(monospace);Text(this.timer.isBreakMode?休息一下吧:保持专注哦).fontSize(14).fontColor(#888888);}}// 操作按钮组Row({space:24}){Button(重置).width(100).height(50).borderRadius(25).backgroundColor(#FFFFFF).fontColor(#666666).onClick((){this.timer.reset();promptAction.showToast({message:已重置计时});});Button(this.timer.isRunning?暂停:开始).width(140).height(56).borderRadius(28).backgroundColor(this.timer.isBreakMode?#00ACC1:#FF8F00).fontSize(18).fontColor(#FFFFFF).onClick((){if(this.timer.isRunning){this.timer.pause();}else{this.timer.start();}});}Blank();// 底部说明Text( 专注25分钟休息5分钟高效工作不疲惫).fontSize(12).fontColor(#888888).margin({bottom:40});}.width(100%).padding({top:60});}.width(100%).height(100%);}}3.3 运行效果说明默认专注模式橙黄色渐变背景25 分钟倒计时点击开始按钮启动计时自动切换模式专注结束后自动进入休息模式背景变为青蓝色5 分钟休息倒计时实时进度动画环形进度条随秒数平滑推进时间文字实时更新统计累计每完成一次专注顶部计数自动 1暂停与重置随时可以暂停计时或一键重置当前轮次整个过程中你没有写任何「强制刷新 UI」的代码所有属性修改都自动驱动界面更新——这就是 ArkUI V2 状态管理的魅力。四、直观对比V2 写法 vs V1 写法我们以「倒计时减一秒」这个最核心的操作为例直观感受两种架构的代码差异。V1 旧版写法必须替换引用才能刷新// V1必须整个替换对象才能触发UI更新Statetimer:TimernewTimer();// 每秒执行一次tick(){// ❌ 直接修改无效UI不刷新// this.timer.remainSeconds--;// ✅ 必须创建新对象替换引用写法冗余this.timer{...this.timer,remainSeconds:this.timer.remainSeconds-1};}V2 新版写法直接修改自动生效// V2直接修改属性UI自动更新Localtimer:TomatoTimernewTomatoTimer();tick(){// ✅ 一行搞定自动触发UI重绘this.timer.remainSeconds--;}可以看到V2 写法不仅代码量更少逻辑也更符合直觉不用再为了触发刷新去写违背业务逻辑的代码。五、新手高频踩坑避坑指南1. 属性修改后 UI 不刷新漏加 Trace这是最高频的坑。ObservedV2只是让类具备可观察能力具体哪些属性变化能触发 UI 刷新完全由 Trace 决定。漏加标记的属性无论怎么修改都不会驱动界面更新。2. 使用对象字面量代替类实例ObservedV2只能装饰 class 类普通对象字面量const timer { remainSeconds: 1500 }无法被观察。必须通过new 类名()创建实例状态监听才会生效。3. 混用 V1 与 V2 装饰器旧版Observed和新版ObservedV2是两套独立的观察体系不要混用在同一个类上同理State配合 V2 类虽然能运行但性能和兼容性不如Local新项目建议统一使用 V2 体系。4. 给所有属性都加 Trace不需要驱动 UI 的属性比如内部定时器 ID、配置常量不要加Trace否则每次变更都会触发不必要的 UI 重绘反而影响性能。只给真正影响界面展示的属性加标记这也是细粒度更新的核心意义。5. 页面销毁不清理定时器计时器属于页面级资源页面退出时必须调用destroy()清理定时器否则会造成内存泄漏、后台持续耗电。统一在aboutToDisappear生命周期中释放资源。六、总结一个小小的番茄钟几乎覆盖了 ArkUI V2 状态管理最核心的使用场景嵌套对象的属性级监听计算属性的自动响应更新组件本地状态的声明业务逻辑与 UI 渲染分离吃透这个 Demo 后你再去处理列表元素属性变更、复杂表单状态、多模块数据联动等场景思路会清晰很多。V2 状态管理的核心理念就是「细粒度、低代码、高性能」它不是简单的 API 改名而是底层响应式架构的全面升级。