鸿蒙实战:@State 本地状态与 UI 刷新
·
鸿蒙实战:@State 本地状态与 UI 刷新
前言(必读)

图:鸿蒙实战:@State 本地状态与 UI 刷新(第 73 篇) 运行效果截图(HarmonyOS NEXT)
@State 是 ArkUI 声明式 UI 中最核心的装饰器。它让状态变量与 UI 绑定:状态变了,UI 自动刷新。本文通过对项目全页面 @State 使用的系统性梳理,帮助读者深入理解状态驱动的渲染机制。

图:@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 开发注意事项
在正式开发前,建议按以下步骤完成环境准备与前置检查:
- 版本确认:检查 DevEco Studio 与 SDK 版本,确保满足目标 API Level 要求
- 权限声明:在
module.json5的requestPermissions字段中提前声明所有需要的系统权限 - 设备能力检查:调用前验证设备是否支持目标能力(相机、NFC、传感器等)
- 异步封装:所有耗时操作(数据库、文件 I/O、网络请求)统一使用
async/await处理 - 资源释放:在组件
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++)
}
}
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
更多推荐




所有评论(0)