第4篇:状态管理——让界面动起来

本课目标:掌握@State装饰器和事件处理,让界面能响应用户操作
**作者:**中文编程倡导者—— 李金雨
预计课时:3课时(135分钟)
难度等级:⭐⭐⭐(进阶)


一、开篇引入

1.1 从"静态"到"动态"

前面的课程,我们做的界面都是"静态"的——就像一张照片,只能看,不能交互。

但真正的应用应该是"动态"的:

  • 点击按钮,数字会增加
  • 输入文字,界面会同步显示
  • 切换开关,背景会变颜色

1.2 生活例子:自动售货机

想象一台自动售货机:

状态(State) = 机器当前的"情况"

  • 库存:还剩3瓶可乐
  • 投币:已投入5元
  • 选择:用户选了可乐

事件(Event) = 用户做的"动作"

  • 投入硬币 → 金额增加
  • 按下按钮 → 选择商品
  • 确认购买 → 出货、找零

界面更新 = 显示的变化

  • 投币后,屏幕显示"已投5元"
  • 选择后,对应商品灯亮起
  • 购买后,商品掉下来

这就是状态管理的核心:状态变化 → 界面自动更新

1.3 本课目标

今天我们要学习:

  1. 什么是状态(State)
  2. 怎么用 @State 创建状态
  3. 怎么处理事件(点击、输入等)
  4. 实战:计数器、点赞按钮、简易计算器

1.4 预期成果

完成本课后,你能做出这样的应用:

计数器:              点赞按钮:           简易计算器:
┌─────────────┐      ┌─────────────┐     ┌─────────────┐
│             │      │             │     │   123+456   │
│   当前计数   │      │    ❤️ 999   │     ├─────────────┤
│             │      │             │     │ 7  8  9  ÷  │
│    15       │      │  [点赞按钮]  │     │ 4  5  6  ×  │
│             │      │             │     │ 1  2  3  -  │
│ [-]  [+]    │      └─────────────┘     │ 0  .  =  +  │
│             │                          │             │
└─────────────┘                          └─────────────┘

二、概念讲解

2.1 什么是状态(State)?

定义

状态就是会变化的数据

当状态变化时,使用这个状态的界面会自动更新。

生活例子:温度计
    温度计
    ┌───┐
    │   │
    │   │
    │███│ ← 红色液柱(状态)
    │███│
    │███│
    └───┘
     25°C  ← 显示的数字(界面)
  • 状态:温度计里的液柱高度
  • 界面:显示的温度数字
  • 变化:温度升高 → 液柱上升 → 数字变大
程序中的状态
@State 温度: number = 25    // 这是一个状态

// 当温度变化时
this.温度 = 30               // 界面会自动显示30

2.2 @State装饰器——标记状态

什么是装饰器?

装饰器就像给数据贴一个"特殊标签",告诉ArkTS:“这个数据很重要,它变化时要通知界面更新!”

@State 计数: number = 0
部分 含义
@State 装饰器,标记这是状态
计数 状态的名字
number 状态的类型
0 初始值
使用状态
@Entry
@Component
struct 计数器 {
  @State 计数: number = 0      // 定义状态

  build() {
    Column() {
      // 使用状态显示在界面上
      Text(`当前计数: ${this.计数}`)
        .fontSize(40)
      
      Button("增加")
        .onClick(() => {
          // 修改状态
          this.计数 = this.计数 + 1
        })
    }
  }
}

关键点

  1. @State 定义状态
  2. this.状态名 读取状态
  3. 给状态赋新值,界面自动更新

2.3 事件处理——响应用户操作

什么是事件?

事件就是用户做的动作,比如:

  • 点击按钮
  • 输入文字
  • 滑动屏幕
  • 长按图片
点击事件(onClick)
Button("点我")
  .onClick(() => {
    // 点击后要做的事情
    console.log("按钮被点击了!")
    this.计数 = this.计数 + 1
  })
输入事件(onChange)
TextInput({ placeholder: "请输入" })
  .onChange((输入的内容: string) => {
    // 输入内容变化时要做的事情
    this.用户名 = 输入的内容
  })
滑动事件(onSwipe)
Image("xxx.jpg")
  .onSwipe((事件: SwipeEvent) => {
    if (事件.direction == SwipeDirection.Left) {
      console.log("向左滑动了")
    }
  })

2.4 状态变化的流程

让我们看看点击按钮后发生了什么:

用户点击按钮
    ↓
触发 onClick 事件
    ↓
执行 this.计数 = this.计数 + 1
    ↓
状态发生变化
    ↓
ArkTS检测到状态变化
    ↓
自动重新渲染界面
    ↓
界面上显示新的数字

这就是响应式编程的魅力:数据驱动界面

2.5 多种状态类型

数字状态
@State 计数: number = 0
@State 分数: number = 100
@State 价格: number = 99.9
文字状态
@State 用户名: string = ""
@State 提示信息: string = "请输入用户名"
@State 当前页面: string = "首页"
真假状态
@State 是否登录: boolean = false
@State 是否加载中: boolean = true
@State 是否夜间模式: boolean = false
数组状态
@State 待办列表: string[] = ["吃饭", "睡觉", "打豆豆"]
@State 购物车: object[] = []

三、动手实践

3.1 基础练习:计数器

做一个可以加减数字的计数器:

// 完整可运行代码,复制到 Index.ets 即可运行
@Entry
@Component
struct Index {
  @State 计数: number = 0

  build() {
    Column({ space: 30 }) {
      // 标题
      Text("计数器")
        .fontSize(35)
        .fontWeight(FontWeight.Bold)
        .fontColor("#333333")
      
      // 显示当前计数
      Text(`${this.计数}`)
        .fontSize(80)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.计数 >= 0 ? "#4CAF50" : "#F44336")
      
      // 操作按钮
      Row({ space: 30 }) {
        // 减号按钮
        Button("-", { type: ButtonType.Circle })
          .width(70)
          .height(70)
          .fontSize(40)
          .backgroundColor("#FF5722")
          .onClick(() => {
            this.计数 = this.计数 - 1
          })
        
        // 重置按钮
        Button("重置", { type: ButtonType.Capsule })
          .width(100)
          .height(50)
          .backgroundColor("#9E9E9E")
          .onClick(() => {
            this.计数 = 0
          })
        
        // 加号按钮
        Button("+", { type: ButtonType.Circle })
          .width(70)
          .height(70)
          .fontSize(40)
          .backgroundColor("#4CAF50")
          .onClick(() => {
            this.计数 = this.计数 + 1
          })
      }
      
      // 快捷操作
      Row({ space: 15 }) {
        Button("+10")
          .onClick(() => {
            this.计数 = this.计数 + 10
          })
        
        Button("-10")
          .onClick(() => {
            this.计数 = this.计数 - 10
          })
        
        Button("×2")
          .onClick(() => {
            this.计数 = this.计数 * 2
          })
      }
      .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .justifyContent(FlexAlign.Center)
  }
}

3.2 进阶练习:点赞按钮

做一个带动画效果的点赞按钮:

// 完整可运行代码,复制到 Index.ets 即可运行
@Entry
@Component
struct Index {
  @State 点赞数: number = 999
  @State 是否已点赞: boolean = false
  @State 心形大小: number = 1

  build() {
    Column({ space: 20 }) {
      // 显示点赞数
      Row({ space: 5 }) {
        Text(this.是否已点赞 ? "❤️" : "🤍")
          .fontSize(40 + this.心形大小 * 10)
          .onClick(() => {
            this.切换点赞状态()
          })
        
        Text(`${this.点赞数}`)
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.是否已点赞 ? "#F44336" : "#666666")
      }
      
      // 提示文字
      Text(this.是否已点赞 ? "已点赞" : "点击点赞")
        .fontSize(16)
        .fontColor("#999999")
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
    .justifyContent(FlexAlign.Center)
  }
  
  切换点赞状态() {
    if (this.是否已点赞) {
      // 取消点赞
      this.点赞数 = this.点赞数 - 1
      this.是否已点赞 = false
    } else {
      // 点赞
      this.点赞数 = this.点赞数 + 1
      this.是否已点赞 = true
      
      // 简单动画效果
      this.心形大小 = 1.5
      setTimeout(() => {
        this.心形大小 = 1
      }, 200)
    }
  }
}

3.3 进阶练习:输入框同步显示

实现输入框内容和界面实时同步:

// 完整可运行代码,复制到 Index.ets 即可运行
@Entry
@Component
struct Index {
  @State 用户名: string = ""
  @State 密码: string = ""
  @State 年龄: string = ""

  build() {
    Column({ space: 20 }) {
      Text("实时预览")
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })
      
      // 输入区域
      Column({ space: 15 }) {
        // 用户名输入
        Column({ space: 5 }) {
          TextInput({ placeholder: "请输入用户名", text: this.用户名 })
            .width("90%")
            .height(50)
            .backgroundColor("#F5F5F5")
            .borderRadius(8)
            .onChange((value: string) => {
              this.用户名 = value
            })
          
          Text(`当前输入:${this.用户名}`)
            .fontSize(14)
            .fontColor("#666666")
        }
        
        // 密码输入
        Column({ space: 5 }) {
          TextInput({ placeholder: "请输入密码", text: this.密码 })
            .width("90%")
            .height(50)
            .backgroundColor("#F5F5F5")
            .borderRadius(8)
            .type(InputType.Password)
            .onChange((value: string) => {
              this.密码 = value
            })
          
          Text(`密码长度:${this.密码.length}`)
            .fontSize(14)
            .fontColor("#666666")
        }
        
        // 年龄输入
        Column({ space: 5 }) {
          TextInput({ placeholder: "请输入年龄", text: this.年龄 })
            .width("90%")
            .height(50)
            .backgroundColor("#F5F5F5")
            .borderRadius(8)
            .type(InputType.Number)
            .onChange((value: string) => {
              this.年龄 = value
            })
          
          // 根据年龄显示不同提示
          if (this.年龄 != "") {
            let 年龄数字: number = parseInt(this.年龄)
            if (年龄数字 < 0) {
              Text("年龄不能为负数!")
                .fontSize(14)
                .fontColor("#F44336")
            } else if (年龄数字 < 18) {
              Text("未成年用户")
                .fontSize(14)
                .fontColor("#FF9800")
            } else if (年龄数字 > 100) {
              Text("年龄过大,请检查")
                .fontSize(14)
                .fontColor("#F44336")
            } else {
              Text("年龄合法")
                .fontSize(14)
                .fontColor("#4CAF50")
            }
          }
        }
      }
      
      // 信息汇总卡片
      Column({ space: 10 }) {
        Text("信息汇总")
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
        
        Text(`用户名:${this.用户名 || "未填写"}`)
        Text(`密码:${"*".repeat(this.密码.length) || "未填写"}`)
        Text(`年龄:${this.年龄 || "未填写"}`)
      }
      .width("90%")
      .padding(20)
      .backgroundColor("#E3F2FD")
      .borderRadius(12)
      .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#FFFFFF')
  }
}

3.4 挑战练习:简易计算器

做一个功能完整的计算器:

// 完整可运行代码,复制到 Index.ets 即可运行
@Entry
@Component
struct Index {
  @State 显示内容: string = "0"
  @State 上一个数字: number = 0
  @State 当前操作符: string = ""
  @State 是否刚输入操作符: boolean = false

  build() {
    Column() {
      // 显示区域
      Column() {
        Text(this.显示内容)
          .fontSize(60)
          .fontWeight(FontWeight.Medium)
          .width('100%')
          .textAlign(TextAlign.End)
          .padding(20)
      }
      .width('100%')
      .height(150)
      .backgroundColor('#333333')
      .justifyContent(FlexAlign.End)
      
      // 按钮区域
      Column({ space: 2 }) {
        // 第一行:C、÷、×、删除
        Row({ space: 2 }) {
          this.功能按钮("C", "#FF5722", () => this.清空())
          this.功能按钮("÷", "#FF9800", () => this.设置操作符("÷"))
          this.功能按钮("×", "#FF9800", () => this.设置操作符("×"))
          this.功能按钮("⌫", "#FF9800", () => this.删除())
        }
        .height(80)
        
        // 第二行:7、8、9、-
        Row({ space: 2 }) {
          this.数字按钮("7")
          this.数字按钮("8")
          this.数字按钮("9")
          this.功能按钮("-", "#FF9800", () => this.设置操作符("-"))
        }
        .height(80)
        
        // 第三行:4、5、6、+
        Row({ space: 2 }) {
          this.数字按钮("4")
          this.数字按钮("5")
          this.数字按钮("6")
          this.功能按钮("+", "#FF9800", () => this.设置操作符("+"))
        }
        .height(80)
        
        // 第四行:1、2、3、=
        Row({ space: 2 }) {
          this.数字按钮("1")
          this.数字按钮("2")
          this.数字按钮("3")
          this.功能按钮("=", "#4CAF50", () => this.计算结果())
            .height(162)  // 跨两行的高度
        }
        .height(80)
        
        // 第五行:0、.
        Row({ space: 2 }) {
          this.数字按钮("0")
            .width('50%')  // 0按钮占两个位置
          this.数字按钮(".")
          this.功能按钮("", "#333333", () => {})
            .visibility(Visibility.Hidden)  // 占位
        }
        .height(80)
      }
      .width('100%')
      .layoutWeight(1)
      .backgroundColor('#CCCCCC')
      .padding(2)
    }
    .width('100%')
    .height('100%')
  }
  
  // 数字按钮
  @Builder
  数字按钮(数字: string) {
    Button(数字)
      .width('25%')
      .height('100%')
      .fontSize(30)
      .fontWeight(FontWeight.Medium)
      .backgroundColor('#FFFFFF')
      .fontColor('#333333')
      .onClick(() => {
        this.输入数字(数字)
      })
  }
  
  // 功能按钮
  @Builder
  功能按钮(文字: string, 颜色: string, 点击动作: () => void) {
    Button(文字)
      .width('25%')
      .height('100%')
      .fontSize(30)
      .fontWeight(FontWeight.Medium)
      .backgroundColor(颜色)
      .fontColor('#FFFFFF')
      .onClick(点击动作)
  }
  
  // 输入数字
  输入数字(数字: string) {
    if (this.显示内容 == "0" || this.是否刚输入操作符) {
      this.显示内容 = 数字
      this.是否刚输入操作符 = false
    } else {
      this.显示内容 = this.显示内容 + 数字
    }
  }
  
  // 设置操作符
  设置操作符(操作符: string) {
    this.上一个数字 = parseFloat(this.显示内容)
    this.当前操作符 = 操作符
    this.是否刚输入操作符 = true
  }
  
  // 计算结果
  计算结果() {
    let 当前数字: number = parseFloat(this.显示内容)
    let 结果: number = 0
    
    switch (this.当前操作符) {
      case "+":
        结果 = this.上一个数字 + 当前数字
        break
      case "-":
        结果 = this.上一个数字 - 当前数字
        break
      case "×":
        结果 = this.上一个数字 * 当前数字
        break
      case "÷":
        if (当前数字 == 0) {
          this.显示内容 = "错误"
          return
        }
        结果 = this.上一个数字 / 当前数字
        break
      default:
        return
    }
    
    this.显示内容 = 结果.toString()
    this.当前操作符 = ""
  }
  
  // 清空
  清空() {
    this.显示内容 = "0"
    this.上一个数字 = 0
    this.当前操作符 = ""
    this.是否刚输入操作符 = false
  }
  
  // 删除
  删除() {
    if (this.显示内容.length > 1) {
      this.显示内容 = this.显示内容.substring(0, this.显示内容.length - 1)
    } else {
      this.显示内容 = "0"
    }
  }
}

四、知识总结

4.1 核心概念回顾

  1. 状态(State):会变化的数据,用 @State 标记
  2. 事件(Event):用户的操作,如点击、输入
  3. 响应式更新:状态变化 → 界面自动更新

4.2 状态管理流程

定义状态
    ↓
在界面中使用状态
    ↓
用户触发事件
    ↓
修改状态值
    ↓
界面自动刷新

4.3 关键代码速查

// 定义状态
@State 状态名: 类型 = 初始值

// 读取状态
this.状态名

// 修改状态
this.状态名 = 新值

// 点击事件
.onClick(() => {
  // 点击后做的事
})

// 输入事件
.onChange((新值: 类型) => {
  this.状态名 = 新值
})

4.4 常见错误提醒

错误现象 原因 解决方法
界面不更新 忘记加@State 给变量加上@State装饰器
状态无法修改 用了const 改用let或@State
点击没反应 onClick写错位置 确保onClick跟在组件后面
状态值不对 类型不匹配 检查类型定义和赋值是否一致
数组不更新 直接修改数组元素 使用push/pop等方法或重新赋值

五、课后作业

5.1 巩固练习(必做)

练习1:待办事项列表

做一个简单的待办事项应用:

  • 输入框添加新事项
  • 点击事项标记完成(文字加删除线)
  • 显示已完成/未完成数量

练习2:开关控制器

做一个控制面板:

  • 多个开关(WiFi、蓝牙、飞行模式)
  • 开关状态改变时背景色变化
  • 显示当前开启的功能数量

练习3:投票系统

做一个简单的投票应用:

  • 多个选项
  • 点击选项投票
  • 实时显示每个选项的票数

5.2 创意编程(选做)

创意1:计时器

  • 开始/暂停/重置按钮
  • 显示经过的时间
  • 记录多圈时间

创意2:猜数字游戏

  • 系统随机生成一个数字
  • 用户输入猜测
  • 提示"太大"或"太小"
  • 记录猜测次数

创意3:简易画板

  • 选择画笔颜色
  • 选择画笔粗细
  • 清空画布
  • 保存作品

5.3 下篇预习

下一篇,我们将学习列表渲染,展示大量数据。预习问题:

  1. 怎么显示一个班级50个学生的信息?
  2. 怎么让列表可以滚动?
  3. 怎么点击列表项进行操作?

附录:更多状态管理技巧

技巧1:状态联动

@State 单价: number = 10
@State 数量: number = 1

// 计算属性(自动更新)
get 总价(): number {
  return this.单价 * this.数量
}

技巧2:对象状态

interface 用户信息 {
  姓名: string
  年龄: number
}

@State 用户: 用户信息 = { 姓名: "张三", 年龄: 20 }

// 修改对象属性
this.用户.姓名 = "李四"    // 这样不会触发更新!

// 正确做法:重新赋值整个对象
this.用户 = { 姓名: "李四", 年龄: this.用户.年龄 }

技巧3:数组状态操作

@State 列表: string[] = ["a", "b", "c"]

// 添加元素
this.列表.push("d")           // 不会触发更新!
this.列表 = [...this.列表, "d"]  // 正确做法

// 删除元素
this.列表.splice(1, 1)        // 不会触发更新!
this.列表 = this.列表.filter((_, i) => i != 1)  // 正确做法

// 修改元素
this.列表[0] = "A"            // 不会触发更新!
this.列表 = this.列表.map((item, i) => i == 0 ? "A" : item)  // 正确做法

恭喜你完成了第4篇的学习! 🎉

现在你已经掌握了状态管理的核心概念,可以做出能交互的应用了。记住:数据驱动界面,只要管理好状态,界面就会自动更新!

下节课,我们将学习如何处理大量数据的展示!

Logo

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

更多推荐