📖 鸿蒙NEXT开发实战系列 | 第16篇 | 状态管理篇 🎯 适合人群:有ArkTS基础的开发者 ⏰ 阅读时间:约15分钟 | 💻 开发环境:DevEco Studio 5.0+

上一篇:DevEco Studio必备工具清单 | 下一篇:敬请期待


目录


一、引言:为什么状态管理如此重要?

在鸿蒙NEXT应用开发中,状态管理是构建动态UI的核心基石。想象一下:电商App的商品数量无法实时更新、用户的登录状态无法同步到各个页面、表单输入无法即时反馈——这样的应用体验将是灾难性的。

ArkUI采用声明式UI范式,其核心思想是"状态驱动UI":当状态(State)发生变化时,框架会自动更新对应的UI组件。这种模式相比传统的命令式UI,代码更简洁、逻辑更清晰、维护更方便。

本文将系统讲解ArkUI四大核心状态管理装饰器:@State@Prop@Link@Provide/@Consume,并通过5个完整的实战案例帮助你彻底掌握它们的使用场景和最佳实践。


二、@State装饰器:组件内的状态管家

2.1 基本概念

@State是最基础的状态装饰器,用于管理组件内部的状态数据。被@State装饰的变量具有以下特性:

  • 响应式:变量值改变时,自动触发UI刷新

  • 私有化:仅在当前组件内可见和可修改

  • 初始化:支持在组件创建时进行初始化

2.2 基础用法

@Component
struct CounterComponent {
  // 使用@State装饰器声明状态变量
  @State count: number = 0
  
  build() {
    Column() {
      Text(`当前计数:${this.count}`)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })
      
      Row() {
        Button('减少')
          .onClick(() => {
            // 直接修改@State变量,UI会自动更新
            this.count--
          })
          .margin({ right: 10 })
        
        Button('增加')
          .onClick(() => {
            this.count++
          })
          .margin({ left: 10 })
      }
    }
    .width('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

2.3 实战案例一:计数器应用

// 实战案例:功能完整的计数器
@Entry
@Component
struct CounterApp {
  @State count: number = 0
  @State step: number = 1
  @State history: number[] = []
  
  build() {
    Column() {
      // 标题
      Text('计数器应用')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 30 })
      
      // 计数显示
      Text(`${this.count}`)
        .fontSize(72)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1890FF')
        .margin({ bottom: 20 })
      
      // 步长设置
      Row() {
        Text('步长:')
          .fontSize(16)
        
        Button('-')
          .onClick(() => {
            if (this.step > 1) {
              this.step--
            }
          })
          .width(40)
          .height(40)
        
        Text(`${this.step}`)
          .fontSize(18)
          .margin({ left: 10, right: 10 })
        
        Button('+')
          .onClick(() => {
            this.step++
          })
          .width(40)
          .height(40)
      }
      .margin({ bottom: 30 })
      
      // 操作按钮
      Row() {
        Button('减少')
          .onClick(() => {
            this.count -= this.step
            this.history = [...this.history, this.count]  // 记录历史
          })
          .margin({ right: 15 })
        
        Button('重置')
          .onClick(() => {
            this.count = 0
            this.history = []
          })
          .margin({ right: 15 })
        
        Button('增加')
          .onClick(() => {
            this.count += this.step
            this.history = [...this.history, this.count]
          })
      }
      .margin({ bottom: 30 })
      
      // 历史记录
      if (this.history.length > 0) {
        Text('操作历史:')
          .fontSize(16)
          .margin({ bottom: 10 })
        
        Text(this.history.join(' → '))
          .fontSize(14)
          .fontColor('#666666')
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

2.4 @State支持的数据类型

@State支持多种数据类型,但不同类型的行为略有差异:

数据类型

触发更新方式

注意事项

number/string/boolean

直接赋值

最简单,推荐使用

对象(Object)

整体赋值新对象

直接修改属性不会触发更新

数组(Array)

整体赋值或splice

push/pop不会触发更新

对象类型正确用法

interface UserInfo {
  name: string
  age: number
}

@Component
struct ObjectDemo {
  @State user: UserInfo = { name: '张三', age: 25 }
  
  build() {
    Column() {
      Text(`用户名:${this.user.name}`)
      
      Button('更新用户名')
        .onClick(() => {
          // 正确方式:整体赋值
          this.user = { ...this.user, name: '李四' }
        })
    }
  }
}

三、@Prop装饰器:父子组件的单向数据流

3.1 基本概念

@Prop用于实现父组件到子组件的单向数据传递。特点如下:

  • 单向绑定:数据只能从父组件流向子组件

  • 本地副本:子组件接收到的是父组件数据的副本,修改不会影响父组件

  • 自动同步:父组件数据变化时,子组件自动更新

3.2 实战案例二:商品列表

// 商品数据接口
interface Product {
  id: number
  name: string
  price: number
  image: string
}

// 商品卡片组件(子组件)
@Component
struct ProductCard {
  @Prop product: Product      // 使用@Prop接收父组件数据
  @Prop isSelected: boolean   // 选中状态
  
  build() {
    Column() {
      // 商品图片(使用Text模拟)
      Text(this.product.image)
        .fontSize(48)
        .margin({ bottom: 10 })
      
      // 商品名称
      Text(this.product.name)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 5 })
      
      // 商品价格
      Text(`¥${this.product.price}`)
        .fontSize(18)
        .fontColor('#FF4D4F')
        .margin({ bottom: 10 })
      
      // 选中状态标识
      if (this.isSelected) {
        Text('已选中')
          .fontSize(12)
          .fontColor('#52C41A')
          .backgroundColor('#F6FFED')
          .padding({ left: 8, right: 8, top: 2, bottom: 2 })
          .borderRadius(4)
      }
    }
    .width(150)
    .padding(15)
    .backgroundColor(this.isSelected ? '#E6F7FF' : '#FFFFFF')
    .borderRadius(12)
    .shadow({
      radius: 8,
      color: '#0000001A',
      offsetX: 0,
      offsetY: 2
    })
    .margin({ right: 10 })
  }
}

// 商品列表父组件
@Entry
@Component
struct ProductListDemo {
  @State products: Product[] = [
    { id: 1, name: '智能手机', price: 4999, image: '📱' },
    { id: 2, name: '笔记本电脑', price: 8999, image: '💻' },
    { id: 3, name: '无线耳机', price: 999, image: '🎧' },
    { id: 4, name: '智能手表', price: 1999, image: '⌚' }
  ]
  @State selectedId: number = -1
  
  build() {
    Column() {
      Text('商品列表')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })
      
      // 横向滚动商品列表
      Scroll() {
        Row() {
          ForEach(this.products, (product: Product) => {
            ProductCard({
              product: product,                    // 传递商品数据
              isSelected: product.id === this.selectedId  // 传递选中状态
            })
            .onClick(() => {
              this.selectedId = product.id  // 更新选中状态
            })
          })
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .width('100%')
      
      // 选中商品信息
      if (this.selectedId !== -1) {
        Text(`已选中:${this.products.find(p => p.id === this.selectedId)?.name}`)
          .fontSize(18)
          .margin({ top: 20 })
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

3.3 @Prop的关键特性

// @Prop是只读的,子组件不能修改
@Component
struct ReadOnlyDemo {
  @Prop message: string
  
  build() {
    Column() {
      Text(this.message)
      
      Button('尝试修改')
        .onClick(() => {
          // this.message = '新消息'  // 编译错误!@Prop是只读的
          console.info('Prop不能在子组件中修改')
        })
    }
  }
}

四、@Link装饰器:父子组件的双向数据绑定

4.1 基本概念

@Link用于实现父子组件之间的双向数据绑定。特点如下:

  • 双向绑定:父子组件中任何一方修改数据,另一方都会同步更新

  • 同一引用:父子组件共享同一个数据源

  • 必须初始化:父组件必须使用$$操作符传递数据

4.2 实战案例三:设置页面

// 设置项组件(子组件)
@Component
struct SettingItem {
  @Link isEnabled: boolean  // 使用@Link实现双向绑定
  label: string = ''
  
  build() {
    Row() {
      Text(this.label)
        .fontSize(16)
        .layoutWeight(1)
      
      // 开关组件
      Toggle({ type: ToggleType.Switch, isOn: this.isEnabled })
        .onChange((isOn: boolean) => {
          this.isEnabled = isOn  // 子组件修改,父组件同步更新
        })
    }
    .width('100%')
    .padding(15)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    .margin({ bottom: 10 })
  }
}

// 设置页面(父组件)
@Entry
@Component
struct SettingsPage {
  @State notifications: boolean = true
  @State darkMode: boolean = false
  @State autoUpdate: boolean = true
  @State locationService: boolean = false
  
  build() {
    Column() {
      Text('应用设置')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 30 })
      
      // 设置项列表
      SettingItem({ isEnabled: $notifications, label: '推送通知' })
      SettingItem({ isEnabled: $darkMode, label: '深色模式' })
      SettingItem({ isEnabled: $autoUpdate, label: '自动更新' })
      SettingItem({ isEnabled: $locationService, label: '定位服务' })
      
      // 显示当前设置状态
      Column() {
        Text('当前设置状态:')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 10 })
        
        Text(`通知: ${this.notifications ? '开启' : '关闭'}`)
        Text(`深色模式: ${this.darkMode ? '开启' : '关闭'}`)
        Text(`自动更新: ${this.autoUpdate ? '开启' : '关闭'}`)
        Text(`定位服务: ${this.locationService ? '开启' : '关闭'}`)
      }
      .padding(15)
      .backgroundColor('#F5F5F5')
      .borderRadius(8)
      .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F0F2F5')
  }
}

4.3 @Link与@Prop的关键区别

// 对比示例
@Component
struct PropChild {
  @Prop value: number
  
  build() {
    Column() {
      Text(`Prop子组件:${this.value}`)
      Button('修改(无效)')
        .onClick(() => {
          // this.value = 100  // 编译错误
        })
    }
    .padding(10)
    .backgroundColor('#FFEBEE')
  }
}

@Component
struct LinkChild {
  @Link value: number
  
  build() {
    Column() {
      Text(`Link子组件:${this.value}`)
      Button('修改(有效)')
        .onClick(() => {
          this.value = 100  // 可以修改,父组件同步更新
        })
    }
    .padding(10)
    .backgroundColor('#E8F5E9')
  }
}

@Entry
@Component
struct ComparisonDemo {
  @State propValue: number = 0
  @State linkValue: number = 0
  
  build() {
    Column() {
      PropChild({ value: this.propValue })
      LinkChild({ value: $linkValue })  // 注意:使用$操作符
    }
    .width('100%')
    .padding(20)
  }
}

五、@Provide/@Consume装饰器:跨层级数据共享

5.1 基本概念

@Provide@Consume用于实现跨组件层级的数据共享,无需通过中间组件逐层传递。特点如下:

  • 跨层级传递:可以在任意深度的子组件中访问数据

  • 双向绑定@Provide@Consume之间是双向的

  • 一对多:一个@Provide可以对应多个@Consume

5.2 实战案例四:主题切换系统

// 主题配置接口
interface ThemeConfig {
  primaryColor: string
  backgroundColor: string
  textColor: string
  isDark: boolean
}

// 预定义主题
const lightTheme: ThemeConfig = {
  primaryColor: '#1890FF',
  backgroundColor: '#FFFFFF',
  textColor: '#333333',
  isDark: false
}

const darkTheme: ThemeConfig = {
  primaryColor: '#4DABF7',
  backgroundColor: '#1A1A1A',
  textColor: '#FFFFFF',
  isDark: true
}

// 主题提供者(顶层组件)
@Entry
@Component
struct ThemeProvider {
  @Provide('theme') theme: ThemeConfig = lightTheme
  
  build() {
    Column() {
      // 应用头部
      AppHeader()
      
      // 应用内容
      AppContent()
      
      // 主题切换按钮
      Button(`切换到${this.theme.isDark ? '浅色' : '深色'}模式`)
        .onClick(() => {
          this.theme = this.theme.isDark ? lightTheme : darkTheme
        })
        .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.theme.backgroundColor)
  }
}

// 应用头部组件
@Component
struct AppHeader {
  @Consume('theme') theme: ThemeConfig  // 消费主题配置
  
  build() {
    Row() {
      Text('鸿蒙应用')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.theme.textColor)
    }
    .width('100%')
    .padding(15)
    .backgroundColor(this.theme.primaryColor)
  }
}

// 应用内容组件
@Component
struct AppContent {
  @Consume('theme') theme: ThemeConfig
  
  build() {
    Column() {
      Text('欢迎使用鸿蒙应用')
        .fontSize(24)
        .fontColor(this.theme.textColor)
        .margin({ bottom: 20 })
      
      Text('这是一个支持主题切换的示例应用')
        .fontSize(16)
        .fontColor(this.theme.isDark ? '#CCCCCC' : '#666666')
        .margin({ bottom: 15 })
      
      // 卡片组件
      Column() {
        Text('卡片标题')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.theme.textColor)
        
        Text('卡片内容描述')
          .fontSize(14)
          .fontColor(this.theme.isDark ? '#AAAAAA' : '#999999')
          .margin({ top: 10 })
      }
      .width('100%')
      .padding(15)
      .backgroundColor(this.theme.isDark ? '#2D2D2D' : '#F5F5F5')
      .borderRadius(8)
    }
    .padding(20)
  }
}

5.3 实战案例五:用户认证状态共享

// 用户信息接口
interface UserInfo {
  name: string
  avatar: string
  isLoggedIn: boolean
}

// 全局认证状态管理
@Entry
@Component
struct AuthApp {
  @Provide('userInfo') userInfo: UserInfo = {
    name: '游客',
    avatar: '👤',
    isLoggedIn: false
  }
  
  build() {
    Column() {
      // 导航栏(显示用户信息)
      NavBar()
      
      // 主要内容
      MainContent()
      
      // 底部标签栏
      TabBar()
    }
    .width('100%')
    .height('100%')
  }
}

// 导航栏组件
@Component
struct NavBar {
  @Consume('userInfo') userInfo: UserInfo
  
  build() {
    Row() {
      Text(this.userInfo.isLoggedIn ? `欢迎,${this.userInfo.name}` : '鸿蒙应用')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
    }
    .width('100%')
    .padding(15)
    .backgroundColor('#1890FF')
    .justifyContent(FlexAlign.Center)
  }
}

// 主要内容组件
@Component
struct MainContent {
  @Consume('userInfo') userInfo: UserInfo
  @State username: string = ''
  @State password: string = ''
  
  build() {
    Column() {
      if (this.userInfo.isLoggedIn) {
        // 已登录状态
        Column() {
          Text(`${this.userInfo.avatar}`)
            .fontSize(64)
            .margin({ bottom: 10 })
          
          Text(`欢迎回来,${this.userInfo.name}!`)
            .fontSize(24)
            .margin({ bottom: 20 })
          
          Button('退出登录')
            .onClick(() => {
              this.userInfo = {
                name: '游客',
                avatar: '👤',
                isLoggedIn: false
              }
            })
        }
      } else {
        // 未登录状态
        Column() {
          Text('请登录')
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 30 })
          
          TextInput({ placeholder: '请输入用户名' })
            .onChange((value: string) => {
              this.username = value
            })
            .margin({ bottom: 15 })
          
          TextInput({ placeholder: '请输入密码' })
            .type(InputType.Password)
            .onChange((value: string) => {
              this.password = value
            })
            .margin({ bottom: 25 })
          
          Button('登录')
            .width('100%')
            .onClick(() => {
              if (this.username && this.password) {
                // 模拟登录成功
                this.userInfo = {
                  name: this.username,
                  avatar: '😊',
                  isLoggedIn: true
                }
              }
            })
        }
        .padding(20)
      }
    }
    .flexGrow(1)
    .justifyContent(FlexAlign.Center)
    .padding(20)
  }
}

// 底部标签栏
@Component
struct TabBar {
  @Consume('userInfo') userInfo: UserInfo
  
  build() {
    Row() {
      Column() {
        Text('🏠')
          .fontSize(24)
        Text('首页')
          .fontSize(12)
      }
      .layoutWeight(1)
      .justifyContent(FlexAlign.Center)
      
      Column() {
        Text(this.userInfo.avatar)
          .fontSize(24)
        Text('我的')
          .fontSize(12)
      }
      .layoutWeight(1)
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .padding(10)
    .backgroundColor('#FFFFFF')
    .border({ width: { top: 1 }, color: '#EEEEEE' })
  }
}

六、四大装饰器对比总结

6.1 核心特性对比

特性

@State

@Prop

@Link

@Provide/@Consume

数据流向

组件内部

父→子(单向)

父↔子(双向)

跨层级(双向)

可修改性

可修改

只读

可修改

可修改

初始化方式

直接赋值

父组件赋值

$操作符

key匹配

适用场景

组件内部状态

子组件只读展示

父子双向交互

全局/跨层共享

性能影响

最小

较小

较小

中等

6.2 使用场景速查

场景

推荐装饰器

示例

组件内部状态

@State

按钮点击次数、输入框内容、展开/收起状态

父传子(只读)

@Prop

列表项数据、配置信息、显示状态

父子双向绑定

@Link

表单输入、开关状态、选中项

跨层级共享

@Provide/@Consume

主题、语言、用户信息、全局配置

6.3 选择决策流程

需要管理状态?
    ↓
是 → 状态只在当前组件使用?
    ↓
是 → 使用 @State
    ↓
否 → 需要子组件修改吗?
    ↓
是 → 使用 @Link(父组件用$传递)
    ↓
否 → 使用 @Prop
    ↓
如果需要跨多个层级 → 使用 @Provide/@Consume

七、实战案例汇总

本文包含的5个完整实战案例:

序号

案例名称

核心装饰器

学习要点

1

计数器应用

@State

基础状态管理、数组更新

2

商品列表

@Prop

父子数据传递、只读特性

3

设置页面

@Link

双向数据绑定、$操作符

4

主题切换

@Provide/@Consume

跨层级共享、一对多

5

用户认证

@Provide/@Consume

复杂状态共享、条件渲染


八、总结与最佳实践

8.1 核心要点回顾

  1. @State:组件内部状态管理,支持基础类型和引用类型,引用类型需要整体赋值才能触发更新

  2. @Prop:父子组件单向数据传递,子组件只能读取不能修改,父组件更新会同步到子组件

  3. @Link:父子组件双向数据绑定,必须使用$$操作符传递引用,任何一方修改都会同步

  4. @Provide/@Consume:跨组件层级数据共享,无需逐层传递,支持一对多和双向绑定

8.2 最佳实践

  1. 优先使用@State:如果状态只在组件内部使用,始终优先选择@State

  2. 明确数据流向:需要子组件修改时用@Link,不需要修改时用@Prop

  3. 避免过度使用@Provide:只在真正需要跨层级共享时使用,过度使用会增加调试难度

  4. 对象整体赋值:修改对象或数组时,始终创建新对象/数组,而不是修改原数据

  5. 使用唯一key:在ForEach中使用唯一key,提升列表渲染性能

8.3 常见错误

// 错误1:对象属性直接修改
@State user: UserInfo = { name: '张三', age: 25 }
this.user.name = '李四'  // 错误!不会触发UI更新
this.user = { ...this.user, name: '李四' }  // 正确

// 错误2:@Link忘记使用$
ChildComponent({ count: this.count })  // 错误
ChildComponent({ count: $count })      // 正确

// 错误3:数组push不触发更新
@State list: string[] = []
this.list.push('item')  // 错误
this.list = [...this.list, 'item']  // 正确

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

有问题欢迎在评论区讨论,我会及时回复!


鸿蒙NEXT开发实战系列 -- 全部文章


标签:鸿蒙状态管理 | @State | @Prop | @Link | @Provide | @Consume | ArkUI | ArkTS | 声明式UI | 数据绑定 | HarmonyOS NEXT

Logo

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

更多推荐