第4次:状态管理基础
状态管理是构建交互式应用的核心。本次课程将学习 ArkUI 提供的各种状态装饰器,理解数据如何在组件间流动,并实现应用的主题切换功能。
学习目标
- 理解状态与 UI 的关系
- 掌握 @State 状态装饰器
- 学会 @Prop 单向数据传递
- 理解 @Link 双向数据绑定
- 掌握 @StorageLink 应用级状态
- 实现深色/浅色主题切换功能
4.1 状态与 UI 的关系
什么是状态?
状态是组件中可变的数据,当状态改变时,UI 会自动更新。
typescript
@Component
struct Counter {
@State count: number = 0; // 这就是状态
build() {
Column() {
Text(`计数: ${this.count}`) // UI 依赖状态
Button('+1')
.onClick(() => {
this.count++; // 修改状态 → UI 自动更新
})
}
}
}
状态驱动 UI 更新
用户操作 → 修改状态 → 框架检测变化 → 重新执行 build() → 更新 UI
状态管理的层级
| 层级 | 装饰器 | 作用范围 |
|---|---|---|
| 组件内 | @State | 当前组件 |
| 父子组件 | @Prop / @Link | 父子组件间 |
| 跨组件 | @Provide / @Consume | 祖先与后代 |
| 应用级 | @StorageLink / @StorageProp | 整个应用 |
4.2 @State 状态装饰器
基本用法
@State 用于声明组件内部的可变状态:
typescript
@Component
struct MyComponent {
// 声明状态变量
@State message: string = 'Hello';
@State count: number = 0;
@State isVisible: boolean = true;
@State items: string[] = ['A', 'B', 'C'];
build() {
Column() {
Text(this.message)
Text(`Count: ${this.count}`)
Button('修改')
.onClick(() => {
this.message = 'World'; // 修改状态
this.count++;
})
}
}
}
@State 的特点
- 必须初始化:声明时必须赋初始值
- 私有性:只能在组件内部访问和修改
- 响应式:值改变时自动触发 UI 更新
支持的数据类型
typescript
@Component
struct StateTypes {
// 基本类型
@State str: string = '';
@State num: number = 0;
@State bool: boolean = false;
// 对象类型
@State user: User = { name: '张三', age: 25 };
// 数组类型
@State list: number[] = [1, 2, 3];
// 枚举类型
@State status: Status = Status.Pending;
build() {
// ...
}
}
对象和数组的更新
typescript
@Component
struct ObjectState {
@State user: { name: string; age: number } = { name: '张三', age: 25 };
@State items: string[] = ['A', 'B'];
build() {
Column() {
Text(`${this.user.name}, ${this.user.age}岁`)
Button('修改对象')
.onClick(() => {
// ✅ 正确:整体替换
this.user = { name: '李四', age: 30 };
// ✅ 正确:修改属性(会触发更新)
this.user.age = 26;
})
Button('修改数组')
.onClick(() => {
// ✅ 正确:使用数组方法
this.items.push('C');
// ✅ 正确:整体替换
this.items = [...this.items, 'D'];
})
}
}
}
4.3 @Prop 单向数据传递
什么是 @Prop?
@Prop 用于接收父组件传递的数据,实现单向数据流(父 → 子)。
typescript
// 子组件
@Component
struct ChildComponent {
@Prop title: string; // 接收父组件数据
@Prop count: number;
build() {
Column() {
Text(this.title)
Text(`Count: ${this.count}`)
}
}
}
// 父组件
@Component
struct ParentComponent {
@State parentTitle: string = '父组件标题';
@State parentCount: number = 0;
build() {
Column() {
// 传递数据给子组件
ChildComponent({
title: this.parentTitle,
count: this.parentCount
})
Button('修改')
.onClick(() => {
this.parentTitle = '新标题';
this.parentCount++;
})
}
}
}
@Prop 的特点
- 单向绑定:父组件数据变化会同步到子组件
- 本地副本:子组件持有数据的副本
- 子组件可修改:但不会影响父组件
typescript
@Component
struct Child {
@Prop value: number;
build() {
Column() {
Text(`Value: ${this.value}`)
Button('子组件修改')
.onClick(() => {
this.value++; // 只修改本地副本,不影响父组件
})
}
}
}
实际应用示例
typescript
// 模块卡片组件
@Component
struct ModuleCard {
@Prop icon: string;
@Prop title: string;
@Prop description: string;
@Prop progress: number;
build() {
Column() {
Text(this.icon).fontSize(32)
Text(this.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text(this.description)
.fontSize(12)
.fontColor('#666')
Progress({ value: this.progress, total: 100 })
.width('100%')
}
.padding(16)
.backgroundColor('#fff')
.borderRadius(12)
}
}
// 使用
@Entry
@Component
struct ModuleList {
@State modules: Array<{icon: string; title: string; desc: string; progress: number}> = [
{ icon: '⚛️', title: 'React 简介', desc: '了解 React', progress: 100 },
{ icon: '🛠️', title: '环境搭建', desc: '配置开发环境', progress: 50 },
];
build() {
Column({ space: 12 }) {
ForEach(this.modules, (item) => {
ModuleCard({
icon: item.icon,
title: item.title,
description: item.desc,
progress: item.progress
})
})
}
}
}
4.4 @Link 双向数据绑定
什么是 @Link?
@Link 实现父子组件间的双向数据绑定,子组件的修改会同步到父组件。
typescript
// 子组件
@Component
struct Counter {
@Link count: number; // 双向绑定
build() {
Row({ space: 12 }) {
Button('-')
.onClick(() => this.count--)
Text(`${this.count}`)
.fontSize(20)
Button('+')
.onClick(() => this.count++) // 修改会同步到父组件
}
}
}
// 父组件
@Entry
@Component
struct Parent {
@State totalCount: number = 0;
build() {
Column({ space: 20 }) {
Text(`父组件显示: ${this.totalCount}`)
.fontSize(24)
// 使用 $ 符号传递引用
Counter({ count: $totalCount })
}
}
}
@Link 的特点
- 双向同步:父子组件数据实时同步
- 引用传递 :使用
$符号传递状态引用 - 必须初始化:父组件必须传递 @State 变量
@Prop vs @Link
| 特性 | @Prop | @Link |
|---|---|---|
| 数据流向 | 单向(父→子) | 双向 |
| 传递方式 | 值传递 | 引用传递($) |
| 子组件修改 | 不影响父组件 | 同步到父组件 |
| 使用场景 | 展示数据 | 表单、计数器等 |
实际应用:表单组件
typescript
// 输入框组件
@Component
struct FormInput {
@Link value: string;
@Prop label: string;
@Prop placeholder: string;
build() {
Column() {
Text(this.label)
.fontSize(14)
.fontColor('#333')
.margin({ bottom: 8 })
TextInput({ placeholder: this.placeholder, text: this.value })
.onChange((value: string) => {
this.value = value; // 双向绑定
})
.height(44)
.borderRadius(8)
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
}
// 使用
@Entry
@Component
struct LoginForm {
@State username: string = '';
@State password: string = '';
build() {
Column({ space: 16 }) {
FormInput({
value: $username,
label: '用户名',
placeholder: '请输入用户名'
})
FormInput({
value: $password,
label: '密码',
placeholder: '请输入密码'
})
Button('登录')
.onClick(() => {
console.log(`用户名: ${this.username}, 密码: ${this.password}`);
})
}
.padding(20)
}
}
4.5 @StorageLink 应用级状态
什么是应用级状态?
应用级状态是跨页面、跨组件共享的全局状态,使用 AppStorage 管理。
AppStorage 基础
typescript
// 初始化应用级状态
AppStorage.setOrCreate('isDarkMode', false);
AppStorage.setOrCreate('username', '');
AppStorage.setOrCreate('token', '');
// 读取状态
let isDark = AppStorage.get<boolean>('isDarkMode');
// 修改状态
AppStorage.set('isDarkMode', true);
@StorageLink 装饰器
@StorageLink 创建与 AppStorage 的双向绑定:
typescript
@Entry
@Component
struct SettingsPage {
// 与 AppStorage 中的 'isDarkMode' 双向绑定
@StorageLink('isDarkMode') isDarkMode: boolean = false;
build() {
Column() {
Row() {
Text('深色模式')
Toggle({ type: ToggleType.Switch, isOn: this.isDarkMode })
.onChange((isOn: boolean) => {
this.isDarkMode = isOn; // 自动同步到 AppStorage
})
}
// 根据主题显示不同样式
Text('当前主题: ' + (this.isDarkMode ? '深色' : '浅色'))
.fontColor(this.isDarkMode ? '#fff' : '#000')
}
.backgroundColor(this.isDarkMode ? '#1a1a2e' : '#f8f9fa')
}
}
@StorageProp 装饰器
@StorageProp 创建单向绑定(只读):
typescript
@Component
struct DisplayComponent {
// 只读绑定,组件内修改不会同步到 AppStorage
@StorageProp('username') username: string = '';
build() {
Text(`欢迎, ${this.username}`)
}
}
@StorageLink vs @StorageProp
| 特性 | @StorageLink | @StorageProp |
|---|---|---|
| 绑定方式 | 双向 | 单向 |
| 组件修改 | 同步到 AppStorage | 不同步 |
| 使用场景 | 需要修改全局状态 | 只需读取 |
4.6 实操:实现主题切换功能
现在,让我们为应用实现完整的深色/浅色主题切换功能。
步骤 1:创建 ThemeUtil.ets
在 entry/src/main/ets/common/ 目录下创建 ThemeUtil.ets:
typescript
/**
* 主题工具类
* 管理应用深浅主题切换
*/
/**
* 主题模式枚举
*/
export enum ThemeMode {
AUTO = 'auto',
LIGHT = 'light',
DARK = 'dark'
}
/**
* 浅色主题颜色
*/
export const LightTheme = {
background: '#f8f9fa',
cardBackground: '#ffffff',
textPrimary: '#1a1a2e',
textSecondary: '#495057',
primary: '#61DAFB',
divider: '#e9ecef'
};
/**
* 深色主题颜色
*/
export const DarkTheme = {
background: '#1a1a2e',
cardBackground: '#282c34',
textPrimary: '#ffffff',
textSecondary: '#d1d5db',
primary: '#61DAFB',
divider: '#3d3d5c'
};
/**
* 初始化主题
*/
export function initTheme(): void {
// 初始化 AppStorage 中的主题状态
AppStorage.setOrCreate('isDarkMode', false);
AppStorage.setOrCreate('themeMode', ThemeMode.LIGHT);
console.info('[ThemeUtil] Theme initialized');
}
/**
* 切换主题
*/
export function toggleTheme(): void {
const currentIsDark = AppStorage.get<boolean>('isDarkMode') ?? false;
const newIsDark = !currentIsDark;
AppStorage.set('isDarkMode', newIsDark);
AppStorage.set('themeMode', newIsDark ? ThemeMode.DARK : ThemeMode.LIGHT);
console.info(`[ThemeUtil] Theme toggled to: ${newIsDark ? 'dark' : 'light'}`);
}
/**
* 获取当前主题颜色
*/
export function getThemeColors(isDarkMode: boolean) {
return isDarkMode ? DarkTheme : LightTheme;
}
步骤 2:更新 Index.ets
更新首页,添加主题支持:
typescript
/**
* React 学习教程 App - 首页(支持主题切换)
* 第4次课程实操代码
*/
import { initTheme, toggleTheme, LightTheme, DarkTheme } from '../common/ThemeUtil';
@Entry
@Component
struct Index {
@State currentTab: number = 0;
@StorageLink('isDarkMode') isDarkMode: boolean = false;
// 获取当前主题颜色
get theme() {
return this.isDarkMode ? DarkTheme : LightTheme;
}
aboutToAppear(): void {
initTheme();
}
build() {
Column() {
// 顶部标题栏
this.HeaderBar()
// 主内容区
this.MainContent()
// 底部导航栏
this.BottomNavigation()
}
.width('100%')
.height('100%')
.backgroundColor(this.theme.background)
}
@Builder
HeaderBar() {
Row() {
Text('⚛️')
.fontSize(28)
Text('React 学习教程')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(this.theme.textPrimary)
.margin({ left: 8 })
Blank()
// 主题切换按钮
Text(this.isDarkMode ? '🌙' : '☀️')
.fontSize(24)
.onClick(() => {
toggleTheme();
})
Text('🔍')
.fontSize(24)
.margin({ left: 16 })
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor(this.theme.cardBackground)
}
@Builder
MainContent() {
Scroll() {
Column() {
this.HeroBanner()
this.QuickAccess()
this.RecommendedModules()
}
}
.layoutWeight(1)
.scrollBar(BarState.Off)
}
@Builder
HeroBanner() {
Column() {
Text('开始你的 React 之旅')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
Text('由浅入深,系统掌握 React')
.fontSize(14)
.fontColor('rgba(255,255,255,0.9)')
.margin({ top: 8 })
Row() {
this.StatItem('0', '已完成')
this.Divider()
this.StatItem('35', '总课程')
this.Divider()
this.StatItem('0', '连续天数')
}
.width('100%')
.margin({ top: 24 })
Row() {
Text('📝 每日一题')
.fontSize(14)
.fontColor('#ffffff')
Blank()
Text('挑战 →')
.fontSize(14)
.fontColor('rgba(255,255,255,0.9)')
}
.width('100%')
.padding(12)
.margin({ top: 16 })
.backgroundColor('rgba(255,255,255,0.15)')
.borderRadius(12)
}
.width('100%')
.padding(20)
.linearGradient({
angle: 135,
colors: [['#61DAFB', 0], ['#21a0c4', 1]]
})
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
}
@Builder
StatItem(value: string, label: string) {
Column() {
Text(value)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
Text(label)
.fontSize(12)
.fontColor('rgba(255,255,255,0.9)')
.margin({ top: 4 })
}
.layoutWeight(1)
}
@Builder
Divider() {
Column()
.width(1)
.height(40)
.backgroundColor('rgba(255,255,255,0.3)')
}
@Builder
QuickAccess() {
Column() {
Text('快捷入口')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(this.theme.textPrimary)
.width('100%')
Row({ space: 12 }) {
this.QuickAccessItem('🎯', '面试题库', '海量面试真题')
this.QuickAccessItem('💻', '在线编程', '实战代码练习')
this.QuickAccessItem('📦', '成品下载', '11个示例项目')
}
.width('100%')
.margin({ top: 12 })
}
.width('100%')
.padding(16)
}
@Builder
QuickAccessItem(icon: string, title: string, desc: string) {
Column() {
Text(icon)
.fontSize(32)
Text(title)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor(this.theme.textPrimary)
.margin({ top: 6 })
Text(desc)
.fontSize(11)
.fontColor(this.theme.textSecondary)
.margin({ top: 2 })
}
.layoutWeight(1)
.padding(16)
.backgroundColor(this.theme.cardBackground)
.borderRadius(16)
.shadow({
radius: 8,
color: this.isDarkMode ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.05)',
offsetY: 2
})
}
@Builder
RecommendedModules() {
Column() {
Row() {
Text('推荐模块')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(this.theme.textPrimary)
Blank()
Text('查看全部 →')
.fontSize(14)
.fontColor(this.theme.primary)
}
.width('100%')
Scroll() {
Row({ space: 12 }) {
this.ModuleCard('⚛️', 'React 简介', '入门', '#51cf66')
this.ModuleCard('🛠️', '环境搭建', '入门', '#51cf66')
this.ModuleCard('🧩', '组件基础', '基础', '#339af0')
this.ModuleCard('🎯', '事件与渲染', '基础', '#339af0')
this.ModuleCard('🪝', 'Hooks 基础', '进阶', '#ff922b')
}
.padding({ right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.margin({ top: 12 })
}
.width('100%')
.padding({ left: 16, top: 8, bottom: 20 })
}
@Builder
ModuleCard(icon: string, title: string, level: string, color: string) {
Column() {
Row() {
Text(icon)
.fontSize(24)
Blank()
Text(level)
.fontSize(10)
.fontColor('#ffffff')
.backgroundColor(color)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8)
}
.width('100%')
Text(title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(this.theme.textPrimary)
.margin({ top: 8 })
Text('3 课时 · 30分钟')
.fontSize(11)
.fontColor(this.theme.textSecondary)
.margin({ top: 4 })
}
.width(140)
.padding(12)
.backgroundColor(this.theme.cardBackground)
.borderRadius(16)
.shadow({
radius: 8,
color: this.isDarkMode ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.08)',
offsetY: 4
})
}
@Builder
BottomNavigation() {
Row() {
this.NavItem('🏠', '首页', 0)
this.NavItem('📚', '课程', 1)
this.NavItem('📖', '源码', 2)
this.NavItem('🌟', '项目', 3)
this.NavItem('👤', '我的', 4)
}
.width('100%')
.height(60)
.backgroundColor(this.theme.cardBackground)
.border({ width: { top: 1 }, color: this.theme.divider })
}
@Builder
NavItem(icon: string, label: string, index: number) {
Column() {
Text(icon)
.fontSize(24)
Text(label)
.fontSize(12)
.fontColor(this.currentTab === index ? this.theme.primary : this.theme.textSecondary)
.margin({ top: 4 })
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentTab = index;
})
}
}
步骤 3:运行测试
- 运行应用
- 点击顶部的 ☀️/🌙 图标
- 观察整个应用的颜色变化
预期效果
浅色模式:
- 背景:浅灰色 (#f8f9fa)
- 卡片:白色 (#ffffff)
- 文字:深色 (#1a1a2e)
深色模式:
- 背景:深蓝色 (#1a1a2e)
- 卡片:深灰色 (#282c34)
- 文字:白色 (#ffffff)
本次课程小结
通过本次课程,你已经:
✅ 理解了状态与 UI 的关系
✅ 掌握了 @State 组件内部状态管理
✅ 学会了 @Prop 单向数据传递
✅ 理解了 @Link 双向数据绑定
✅ 掌握了 @StorageLink 应用级状态
✅ 实现了完整的深色/浅色主题切换功能
课后练习
-
添加主题持久化:使用 Preferences 保存用户的主题选择
-
创建设置页面:添加一个设置页面,包含主题切换开关
-
扩展主题:添加更多主题颜色(如蓝色主题、绿色主题)
下次预告
第5次:项目架构设计
我们将学习如何设计一个可维护的项目架构:
- 分层架构设计原则
- 目录结构规划
- 常量管理
- 搭建完整项目骨架
良好的架构是项目成功的基础!