HarmonyOS APP《画伴梦工厂》开发第3篇:@State 与状态管理——让页面“活“起来

第1.3篇:@State 与状态管理------让页面"活"起来

系列 :HarmonyOS 从入门到实践 · 画伴梦工厂实战

难度 :⭐⭐ 进阶

前置知识 :1.2 ArkUI 声明式 UI 基础

涉及源文件products/default/src/main/ets/pages/Index.etsproducts/default/src/main/ets/pages/RecognitionWaitingPage.ets


在上一篇中,我们学习了如何用 ArkUI 组件搭建静态页面。但真实的 App 需要响应交互、更新数据、刷新 UI。这就要靠 ArkUI 的状态管理机制来实现了。

ArkUI 最核心的设计理念是:UI 是状态(State)的函数。当状态变化时,框架自动重新渲染受影响的 UI 部分,开发者无需手动操作 DOM。


一、@State 装饰器:响应式数据绑定

@State 是 ArkUI 中最基础的响应式装饰器。被 @State 修饰的变量一旦赋值发生变化,所有依赖该变量的 UI 组件都会自动刷新。

语法规则

typescript 复制代码
@State 变量名: 类型 = 初始值;

来看"画伴梦工厂"首页 Index 组件中声明的 @State 变量:

typescript 复制代码
@Entry
@Component
struct Index {
  // 响应式状态变量
  @State private currentTab: number = 0;               // 当前选中的Tab
  @State private secondaryPage: number = SECONDARY_NONE; // 二级页面状态
  @State private generationProgress: number = 68;        // 生成进度
  @State private isPlaying: boolean = true;              // 视频播放状态
  @State private chatInput: string = '';                 // 聊天输入框内容
  @State private chatMessages: ChatMessage[] = [         // 聊天消息列表
    { id: 1, role: 'assistant', text: '说出你想创作的角色和场景...', imageUri: '' }
  ];
  @State private noticeText: string = '';                // 通知文本
  @State private animationFrame: number = 0;             // 动画帧
  @State private selectedCreationMode: number = 0;       // 选中的创作模式
  // ... 更多 @State 变量
}

可以看到,@State 可以修饰多种数据类型

类型 示例 说明
number currentTab: number = 0 数值,用于索引、进度等
boolean isPlaying: boolean = true 布尔值,控制显隐、状态切换
string chatInput: string = '' 字符串,文本内容
自定义接口数组 chatMessages: ChatMessage[] 数组,增删元素触发 UI 刷新
枚举数值 secondaryPage: number 通过常量值枚举页面状态

二、状态驱动 UI 变化

2.1 Progress 进度条驱动

在 RecognitionWaitingPage 中,@State progress 的变化会实时驱动 Progress 组件的显示:

typescript 复制代码
@State private progress: number = 12;

build() {
  Column() {
    // 进度百分比文本
    Text(this.progress.toString() + '%')
      .fontSize(32)
      .fontWeight(FontWeight.Bold)
      .fontColor(this.brandPurple)

    // 进度条------随 progress 变化自动刷新
    Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
      .width('84%')
      .height(8)
      .color(this.mint)
      .backgroundColor('#ECECF6')
      .borderRadius(6)
  }
}

Progress({ value: this.progress }) 直接绑定到 @State progress。当 progress 从 12 逐渐增加到 100,进度条和百分比文本会同步更新,无需任何手动刷新代码。

2.2 文本和按钮状态联动

同一个页面的状态变量还会联动影响多个 UI 元素:

typescript 复制代码
@State private statusText: string = '正在准备生成任务';
@State private failed: boolean = false;
@State private completed: boolean = false;

build() {
  Column() {
    // 状态文本------颜色随 failed 变化
    Text(this.statusText)
      .fontColor(this.failed ? '#D94C3D' : '#68708A')

    // 按钮------文字和颜色随 completed/failed 变化
    Button(this.failed ? '重试生成' : (this.completed ? '查看视频结果' : '正在做动画...'))
      .backgroundColor((this.completed || this.failed) ? this.brandPurple : '#A9A0D8')
  }
}

@State 的联动效果:

  • statusText 变化 → 显示文本更新
  • failedtrue → 文本变红色(#D94C3D),按钮显示"重试生成"
  • completedtrue → 按钮显示"查看视频结果",背景色恢复为品牌紫色

一个状态变量可以同时影响多个组件的多个属性,这正是声明式 UI 的强大之处。


三、异步状态更新(定时器)

在实际应用中,状态往往不是一次性变化的,而是通过异步操作逐步更新。项目中的生成等待页面使用了 setInterval 来模拟进度推进:

typescript 复制代码
private startWaitingTimer() {
  this.timerId = setInterval(() => {
    if (this.failed || this.completed) {
      clearInterval(this.timerId);
      return;
    }
    // 更新动画帧计数(用于气泡动画)
    this.animationFrame = (this.animationFrame + 1) % 4;

    // 更新进度
    if (this.progress < 92) {
      this.progress = Math.min(92, this.progress + 3);
    } else {
      this.progress = Math.min(97, this.progress + 1);
    }
    // 根据进度更新步骤索引
    this.activeStep = Math.min(3, Math.floor(this.progress / 28));
  }, 1200);
}

关键点:

  • 每 1200ms 触发一次回调
  • 修改 @State 变量(progressanimationFrameactiveStep
  • 每次修改触发 UI 重新渲染
  • failedcompletedtrue 时停止定时器

Index 页面中也有类似的用法:

typescript 复制代码
aboutToAppear() {
  if (this.animationTimer < 0) {
    this.animationTimer = setInterval(() => {
      if (this.isPlaying && this.secondaryPage === SECONDARY_WORKS) {
        this.animationFrame = (this.animationFrame + 1) % 8;
      }
    }, 650);
  }
}

四、状态在 build() 中的条件渲染

@State 变量的一个核心用途是在 build() 方法中控制显示哪个页面或组件

三元表达式控制页面切换

在 Index 组件的 CurrentPage @Builder 中,通过 secondaryPagecurrentTab 控制页面显示:

typescript 复制代码
@Builder
private CurrentPage() {
  if (this.secondaryPage === SECONDARY_WORKS) {
    this.WorksPage()
  } else if (this.secondaryPage === SECONDARY_ANALYSIS) {
    this.AnalysisPage()
  } else if (this.currentTab === 0) {
    this.HomePage()
  } else if (this.currentTab === 1) {
    this.CreationPage()
  } else {
    this.WorldPage()
  }
}

build() {
  Column() {
    this.CurrentPage()
  }
}

这种模式相当于一个简化版的路由系统

  • secondaryPage === SECONDARY_NONE → 显示首页/创作页/世界页(通过 currentTab 切换)
  • secondaryPage === SECONDARY_WORKS → 切换到作品详情二级页
  • secondaryPage === SECONDARY_ANALYSIS → 切换到分析二级页

openWorksSecondary() 被调用时:

typescript 复制代码
private openWorksSecondary(): void {
  this.secondaryPage = SECONDARY_WORKS;  // 改变状态
  this.noticeText = '';
  this.refreshWorks();
}

@State secondaryPageSECONDARY_NONE 变为 SECONDARY_WORKSbuild() 中的条件判断结果改变,整个页面立即切换到作品页。

条件渲染的方式

ArkUI 中状态驱动的条件渲染有多种写法:

方式一:三元表达式(适合二选一)

typescript 复制代码
// Index.ets - 在 build 中可用
this.secondaryPage === SECONDARY_WORKS ?
  this.worksPage() : this.homePage()

方式二:if/else 语句(适合多分支)

typescript 复制代码
// 在 @Builder 中使用 if/else
@Builder
private CurrentPage() {
  if (this.secondaryPage === SECONDARY_WORKS) {
    this.WorksPage()
  } else if (...) {
    // ...
  }
}

注意:在 build() 方法中,早期版本推荐使用三元表达式;在 @Builder 函数中可以使用 if 语句。


五、@Builder 中消费 @State

@Builder 是 ArkUI 中的自定义构建函数,它同样可以读取和消费 @State 变量。

状态驱动的 @Builder 参数

在 RecognitionWaitingPage 中,StepRow @Builder 根据 activeStep 状态动态改变样式:

typescript 复制代码
@Builder
private StepRow(label: string, index: number) {
  Row() {
    Text((index + 1).toString())
      .fontSize(12)
      .fontWeight(FontWeight.Bold)
      .fontColor(this.activeStep >= index ? '#FFFFFF' : '#8A8FA4')
      .backgroundColor(this.activeStep >= index ? this.mint : '#ECECF6')
      .borderRadius(15)
    Text(label)
      .fontSize(13)
      .fontColor(this.activeStep >= index ? this.ink : '#8A8FA4')
    Text(this.activeStep > index ? '完成' : (this.activeStep === index ? '进行中' : '等待'))
      .fontSize(11)
      .fontColor(this.activeStep >= index ? this.mint : '#9AA0B5')
  }
}

this.activeStep 是组件级的 @State 变量,@Builder 中可以直接访问。随着 activeStep 从 0 → 1 → 2 → 3 递增,每一步的图标颜色、文字颜色、状态标签都会联动变化。

状态驱动的动画气泡

另一个生动例子是 LoadingBubbles

typescript 复制代码
@Builder
private LoadingBubbles() {
  Row() {
    Circle()
      .width(this.animationFrame === 0 ? 24 : 16)
      .height(this.animationFrame === 0 ? 24 : 16)
      .fill(this.sunshine)
      .opacity(this.animationFrame === 0 ? 1 : 0.55)
    Circle()
      .width(this.animationFrame === 1 ? 24 : 16)
      .height(this.animationFrame === 1 ? 24 : 16)
      .fill(this.mint)
      .opacity(this.animationFrame === 1 ? 1 : 0.55)
      .margin({ left: 12 })
    Circle()
      .width(this.animationFrame === 2 ? 24 : 16)
      .height(this.animationFrame === 2 ? 24 : 16)
      .fill(this.brandPurple)
      .opacity(this.animationFrame === 2 ? 1 : 0.55)
      .margin({ left: 12 })
  }
}

animationFrame 每 1200ms 在 0~3 之间循环切换,三个圆点依次变大变亮,形成波浪动画效果。纯状态驱动的无代码动画,无需 Animation API 也能实现生动的加载效果。


六、@State 与生命周期

@State 变量的初始化和清理与组件的生命周期紧密相关。

aboutToAppear:初始化状态

当组件即将挂载到页面时触发,适合在此处初始化 @State 变量或启动定时器:

typescript 复制代码
aboutToAppear() {
  // 注册断点系统
  this.breakpointSystem.register(this.getUIContext());

  // 注册碰一碰分享
  this.registerKnockShare();

  // 刷新作品列表
  this.refreshWorks();

  // 处理路由参数
  const params = this.getUIContext().getRouter().getParams() as IndexParams;
  if (params && params.tab === 'works') {
    this.openWorkDetail(/*...*/);
  }

  // 启动定时器更新 animationFrame
  if (this.animationTimer < 0) {
    this.animationTimer = setInterval(() => {
      if (this.isPlaying && this.secondaryPage === SECONDARY_WORKS) {
        this.animationFrame = (this.animationFrame + 1) % 8;
      }
    }, 650);
  }
}

RecognitionWaitingPage 中的初始化:

typescript 复制代码
aboutToAppear() {
  // 从路由参数读取并初始化 @State 变量
  const params = this.getUIContext().getRouter().getParams() as WaitingParams;
  if (params && params.source) {
    this.source = params.source;
  }
  if (params && params.prompt) {
    this.prompt = params.prompt;
  }
  // 启动定时器和异步任务
  this.startWaitingTimer();
  this.startGeneration();
}

aboutToDisappear:清理状态

当组件即将卸载时触发,必须在此处清理定时器等资源,防止内存泄漏:

typescript 复制代码
aboutToDisappear(): void {
  // 注销碰一碰分享
  this.unregisterKnockShare();

  // 清理定时器
  if (this.animationTimer >= 0) {
    clearInterval(this.animationTimer);
    this.animationTimer = -1;
  }

  // 注销断点系统
  this.breakpointSystem.unregister();

  // 销毁语音识别服务
  this.voiceRecognitionService.destroy();
}

RecognitionWaitingPage 中的清理:

typescript 复制代码
aboutToDisappear() {
  if (this.timerId >= 0) {
    clearInterval(this.timerId);
    this.timerId = -1;
  }
}

重要原则 :凡是在 aboutToAppear 中开启的定时器、注册的监听器、创建的服务,都要在 aboutToDisappear 中对称地清理。


七、性能注意事项

虽然 @State 使用非常方便,但项目中也有一些值得注意的性能最佳实践:

1. @State 变量数量控制

@State 变量不宜过多。Index 组件中声明了约 20+ 个 @State 变量,对于复杂页面这是合理的。但过多的 @State 变量会增加框架的依赖追踪开销。

项目中可以看到合理的做法:将 UI 状态(currentTabsecondaryPage)与业务数据(chatMessagessavedWorks)分开管理。

2. 非 UI 状态使用普通成员变量

对于不需要驱动 UI 刷新的变量,使用普通 private 变量而非 @State:

typescript 复制代码
// 非响应式------不需要驱动 UI
private readonly pagePadding: number = 18;
private readonly brandPurple: string = '#7657F3';
private readonly ink: string = '#1E2442';
private videoController: VideoController = new VideoController();
private animationTimer: number = -1;

这些成员变量要么是常量,要么是不需要 UI 绑定的控制器/定时器ID。用普通变量存储可以避免不必要的性能开销。

3. 定时器中更新 @State 的注意事项

在定时器回调中更新 @State 变量时,ArkUI 会自动将更新合并到下一个渲染帧,不会出现中间态的闪烁。但要注意避免在短时间内频繁触发大量 @State 更新,建议控制定时器间隔(项目中为 650ms~1200ms)。


八、状态管理全景图

本文只介绍了最基础的 @State。HarmonyOS 的状态管理体系是一个完整的层级结构:

装饰器 作用域 用途
@State 组件内部 组件私有的响应式状态
@Link 父子组件 父子组件间双向同步
@Prop 父子组件 父 → 子单向数据传递
@StorageLink 应用全局 跨组件/跨页面的全局状态(如项目中的 currentBreakpoint
@Consume/@Provide 组件树 跨层级传递,避免逐层传递

项目中的 @StorageLink 示例:

typescript 复制代码
@StorageLink('mainBreakpoint') currentBreakpoint: string = 'md';

@StorageLink 通过 AppStorage 实现全局状态共享,项目的断点系统通过它通知所有组件屏幕尺寸变化。


总结

本文通过"画伴梦工厂"的实际代码,深入讲解了 @State 的核心用法:

特性 说明
响应式绑定 @State 变量变化自动驱动 UI 刷新
多类型支持 number、boolean、string、数组、自定义接口
条件渲染 通过三元表达式或 if/else 控制页面切换
@Builder 消费 @Builder 中直接访问 @State,实现局部动态样式
异步更新 setInterval 中修改 @State 实现进度和动画效果
生命周期 aboutToAppear 初始化,aboutToDisappear 清理资源
性能意识 区分 @State 与普通成员变量,控制变量数量

下一篇: 第 1.4 篇将介绍 @Link/@Prop/@StorageLink------跨越组件边界的状态通信,让不同组件间的数据协同工作。


参考源码

本文所有代码均来自项目文件:

  • products/default/src/main/ets/pages/Index.ets --- 首页主组件,包含丰富的 @State 声明、条件渲染、定时器和生命周期管理
  • products/default/src/main/ets/pages/RecognitionWaitingPage.ets --- 生成等待页,展示 @State 驱动进度条、文本、按钮和动画效果的完整实践