鸿蒙实战:@State 本地状态与 UI 刷新

前言(必读)

在这里插入图片描述

图:鸿蒙实战:@State 本地状态与 UI 刷新(第 73 篇) 运行效果截图(HarmonyOS NEXT)

@State 是 ArkUI 声明式 UI 中最核心的装饰器。它让状态变量与 UI 绑定:状态变了,UI 自动刷新。本文通过对项目全页面 @State 使用的系统性梳理,帮助读者深入理解状态驱动的渲染机制。

@State本地状态与UI刷新机制示意图

图:@State 状态驱动 UI 刷新的机制——属性变更触发组件重新渲染

渲染错误: Mermaid 渲染失败: Parse error on line 2: graph LR A[@State 状态变量] -->|值 ------------^ Expecting 'SEMI', 'NEWLINE', 'SPACE', 'EOF', 'subgraph', 'end', 'acc_title', 'acc_descr', 'acc_descr_multiline_value', 'AMP', 'COLON', 'STYLE', 'LINKSTYLE', 'CLASSDEF', 'CLASS', 'CLICK', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', 'direction_tb', 'direction_bt', 'direction_rl', 'direction_lr', 'direction_td', got 'LINK_ID'

一、@State 核心概念

1.1 工作机制

@State count: number = 0

用户操作 → 点击按钮
              ↓
         this.count = 1    ← 状态变更
              ↓
    ArkUI 框架自动检测变更
              ↓
    触发依赖该状态的 UI 重渲染
              ↓
    Text(`点击了 ${this.count} 次`)  ← UI 自动更新

核心特点:

  • 状态变更 → UI 自动重新渲染(无需手动操作 DOM)
  • 仅重渲染依赖该状态的组件(性能优化)
  • 状态是响应式的(Reactive)

1.2 语法

@Entry
@Component
struct MyComponent {
  @State message: string = 'Hello'   // @State 装饰状态变量

  build() {
    Column() {
      Text(this.message)              // 读取状态 → 绑定 UI
        .onClick(() => {
          this.message = 'Clicked'    // 修改状态 → 触发重渲染
        })
    }
  }
}

二、项目中 @State 的分类

从搜索结果看,全应用的 @State 变量可归纳为以下四类:

2.1 数据状态(业务数据)

// 列表/详情数据
@State archives: ArchiveItem[] = []        // ArchivePage
@State dimensions: DuetDimension[] = []    // DuetReportPage
@State result: AnalysisResult | null = null // ReportDetailPage

// 统计数值
@State totalHandwritings: string = '··'     // MePage
@State consecutiveDays: string = '··'       // MePage

// 用户信息
@State userName: string = '小李'           // TapSharePage
@State personalityType: string = '温和理性型' // TapSharePage

2.2 UI 控制状态

// 加载状态
@State isLoading: boolean = false           // ArchivePage

// 创建/编辑对话框
@State showCreateDialog: boolean = false    // ArchivePage
@State newArchiveName: string = ''          // ArchivePage

// 搜索模式
@State searchMode: boolean = false          // ArchivePage
@State searchQuery: string = ''             // ArchivePage

// 条件开关
@State backupOn: boolean = true             // MePage
@State terminalOn: boolean = false          // MePage
@State remindOn: boolean = true             // MePage

2.3 动画状态

// 入场动画
@State contentOpacity: number = 0           // ArchivePage
@State contentOffset: number = 20           // ArchivePage

// 按压缩放
@State createBtnScale: number = 1.0         // ArchivePage
@State cancelBtnScale: number = 1.0         // ArchivePage

// 卡片入场
@State cardScales: number[] = []            // ArchivePage
@State cardEntryOpacities: number[] = []    // ArchivePage
@State cardEntryOffsets: number[] = []      // ArchivePage

// Tab 按压缩放(数组)
@State tabScales: number[] = [1.0, 1.0, 1.0, 1.0]  // ArchivePage

2.4 全局流程状态

// 拍照流程
@State isCapturing: boolean = false         // CapturePage
@State hasPreview: boolean = false          // CapturePage
@State previewPath: string = ''             // CapturePage

// 分享流程
@State isSharing: boolean = false           // TapSharePage
@State shareSuccess: boolean = false        // TapSharePage

// 雷达图动画
@State @Watch('onDrawProgressChange') drawProgress: number = 0  // DuetReportPage

三、典型场景:按压缩放的 @State 模式

3.1 单一按钮缩放

// 最简单的 @State 动画模式
@State createBtnScale: number = 1.0

Button('创建')
  .scale({ x: this.createBtnScale, y: this.createBtnScale })
  .animation({ duration: 150, curve: Curve.EaseOut })
  .onTouch((e: TouchEvent) => {
    if (e.type === TouchType.Down) {
      this.createBtnScale = 0.96     // 状态变更 → 触发 UI 刷新 → 按钮缩小
    } else if (e.type === TouchType.Up || e.type === TouchType.Cancel) {
      this.createBtnScale = 1.0      // 状态变更 → 按钮弹回
    }
  })

3.2 数组索引缩放

// TabBar 多按钮缩放
@State tabScales: number[] = [1.0, 1.0, 1.0, 1.0]

ForEach(this.tabs, (tab: TabItem, index: number) => {
  Text(tab.label)
    .scale({ x: this.tabScales[index], y: this.tabScales[index] })
    .animation({ duration: 150, curve: Curve.EaseOut })
    .onTouch((e: TouchEvent) => {
      if (e.type === TouchType.Down) {
        const arr = [...this.tabScales]           // 创建新数组
        arr[index] = 0.92
        this.tabScales = arr                      // 触发重渲染
      } else if (e.type === TouchType.Up || e.type === TouchType.Cancel) {
        const arr = [...this.tabScales]
        arr[index] = 1.0
        this.tabScales = arr
      }
    })
})

关键细节:ArkUI 的 @State 只能检测到值类型的变更或引用变化。对于数组,必须创建新数组引用[...this.tabScales])才能触发重渲染。


四、数据流:@State 的典型更新路径

4.1 异步数据加载

@State archives: ArchiveItem[] = []     // 初始状态:空数组
@State isLoading: boolean = false

aboutToAppear() {
  this.loadData()
}

async loadData() {
  this.isLoading = true                // UI 显示 Loading
  const data = await ArchiveDao.listByUser(userId)
  this.archives = data                  // UI 渲染列表
  this.isLoading = false                // UI 隐藏 Loading
}

build() {
  if (this.isLoading) {
    LoadingProgress()                   // 加载中
  } else {
    List() {
      ForEach(this.archives, ...)       // 数据列表
    }
  }
}

4.2 @State + @Watch 监听变化

// DuetReportPage.ets 中的雷达图动画
@State @Watch('onDrawProgressChange') drawProgress: number = 0

onDrawProgressChange(): void {
  // 当 drawProgress 变化时,通过 Canvas 重新绘制雷达图
  this.drawRadarOnCanvas()
}

startRadarAnimation(): void {
  animateTo({ duration: AppAnimations.RADAR_DRAW_DURATION }, () => {
    this.drawProgress = 1   // 0 → 1,触发 @Watch 回调
  })
}

4.3 @State 数组的不可变性

// ❌ 错误:直接修改数组元素不会触发 UI 刷新
this.archives[0].name = '新名称'       // 不触发重渲染

// ✅ 正确:创建新数组或使用 setter
const newList = [...this.archives]
newList[0] = { ...newList[0], name: '新名称' }
this.archives = newList                // 触发重渲染

五、@State 的不可变性规则

数据类型 正确修改方式 错误修改方式
number this.count = 1
string this.name = 'new'
boolean this.isReady = true
对象 this.obj = { ...this.obj, key: 'val' } this.obj.key = 'val'
数组 this.arr = [...this.arr, newItem] this.arr.push(newItem)
数组元素 this.arr = this.arr.map(...) this.arr[0] = newVal

六、@State vs @Prop vs @Link

// @State:组件内部状态(私有)
@Component
struct Parent {
  @State count: number = 0

  build() {
    Child({ count: this.count })     // 向子组件传值
  }
}

// @Prop:单向传递(子组件不能修改父组件状态)
@Component
struct Child {
  @Prop count: number               // 从父组件接收的只读副本
}

// @Link:双向绑定(子组件可修改父组件状态)
@Component
struct Child {
  @Link count: number               // 与父组件共享同一变量
}
装饰器 数据源 修改能力 适用场景
@State 自身 仅自身 组件私有状态
@Prop 父组件传入 仅自身(不影响父组件) 子组件展示数据
@Link 父组件传入 可修改父组件 表单双向绑定

七、@State 性能注意事项

// 1. 适当使用局部 @State,避免过大状态
// ❌ 存储大量数据
@State bigList: Item[] = thousandsOfItems   // 每次变更都重渲染

// ✅ 只存储 UI 所需的最小状态
@State visibleItems: Item[] = []            // 按需加载

// 2. @State 粒度
// ❌ 一个 @State 管理太多
@State pageConfig: { mode: string, theme: string, filter: string }

// ✅ 拆分成多个 @State
@State currentMode: string = 'view'
@State currentTheme: string = 'warm'
@State activeFilter: string = 'all'

// 3. 使用 ForEach 的 key 优化列表渲染
ForEach(this.items, (item) => { ... }, (item) => item.id.toString())

八、注意事项与常见问题

8.1 开发注意事项

在正式开发前,建议按以下步骤完成环境准备与前置检查:

  1. 版本确认:检查 DevEco Studio 与 SDK 版本,确保满足目标 API Level 要求
  2. 权限声明:在 module.json5requestPermissions 字段中提前声明所有需要的系统权限
  3. 设备能力检查:调用前验证设备是否支持目标能力(相机、NFC、传感器等)
  4. 异步封装:所有耗时操作(数据库、文件 I/O、网络请求)统一使用 async/await 处理
  5. 资源释放:在组件 aboutToDisappear() 生命周期钩子中及时释放系统资源,防止内存泄漏

8.2 常见错误与解决方案

开发过程中的高频问题汇总:

错误现象 可能原因 解决方案
权限被系统拒绝 未在 module.json5 中静态声明 添加 requestPermissions 权限配置项
页面跳转后白屏 路由路径未在 main_pages.json 注册 检查并补充路由配置文件
UI 状态不刷新 状态变量未添加响应式装饰器 为变量加 @State / @Observed 装饰器
数据库查询返回空 查询条件与存储数据格式不匹配 使用 hilog.debug 打印 SQL 条件排查
相机预览黑屏 运行时相机权限未获取 先调用 requestPermissionsFromUser 申请

总结

  • @State 机制:状态变更 → 自动 UI 重渲染(响应式)
  • 四类状态:数据状态 / UI 控制 / 动画 / 流程
  • 不可变性:数组和对象修改必须创建新引用
  • 数组索引缩放[...arr] 展开语法更新
  • @Watch 监听:监听状态变化执行副作用(Canvas 绘制等)
  • @State vs @Prop vs @Link:私有 / 单向 / 双向的层级关系
  • 性能优化:最小化状态粒度、合理使用 ForEach key

下一篇将讲解路由参数传递与接收


📌 收藏提示:ArkUI 的关键心法——状态不可变性。修数组必须创建新引用,否则 UI 不刷新。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:


七、@State 最佳实践

7.1 @State 只应声明 UI 相关状态

// ✅ 适合 @State:直接影响 UI 渲染
@State isLoading: boolean = false;
@State showModal: boolean = false;
@State currentTab: number = 0;

// ❌ 不适合 @State:业务数据请存 AppStorage 或数据库
@State userList: UserInfo[] = [];  // 如果数据来自数据库,用普通变量+手动触发

7.2 避免 @State 嵌套对象陷阱

@State 只观察第一层属性变化,深层对象修改不触发刷新:

@State userData: { name: string; age: number } = { name: '鹿鹿', age: 25 };

// ❌ 不触发刷新
this.userData.name = '新名字';

// ✅ 重新赋值才触发刷新
this.userData = { ...this.userData, name: '新名字' };

// 或使用 @Observed + @ObjectLink 解决深层监听

7.3 刷新粒度优化

将频繁变化的状态隔离到子组件,减少不必要的父组件重渲染:

// 推荐:计数器状态只属于 CounterCard 组件
@Component
struct CounterCard {
  @State count: number = 0;  // 变化只重渲 CounterCard
  build() {
    Text(`${this.count}`).onClick(() => this.count++)
  }
}

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐