在这里插入图片描述
在这里插入图片描述

一、引言

状态管理是 ArkUI 声明式 UI 框架的核心机制。在传统的命令式 UI 开发中,我们需要手动操作 DOM 或视图来更新界面;而在 ArkUI 的声明式开发范式中,我们只需要声明"状态"与"UI"的绑定关系,当状态变化时,框架会自动刷新关联的 UI 组件。

HarmonyOS 提供了丰富多样的状态管理装饰器,包括 @State@Prop@Link@Provide@Consume@Watch@Observed@ObjectLink 等。本文将以一个卡片式暖色风格的演示页面为主线,深入讲解最核心的状态管理机制,并通过大量代码示例帮助读者理解不同装饰器的区别与适用场景。

二、声明式 UI 与状态管理

2.1 命令式 vs 声明式

命令式 UI(如传统 Android View、iOS UIKit):

// 伪代码:命令式
const textView = findViewById(R.id.text);
textView.setText("新值");  // 手动更新

声明式 UI(如 ArkUI、SwiftUI、Jetpack Compose):

// ArkUI 声明式
@State count: number = 0;
// 当 count 变化时,Text 自动刷新
Text(`${this.count}`)

声明式 UI 的核心思想是:UI 是状态(State)的函数。开发者只需要维护状态,框架负责将状态映射到 UI。

2.2 状态管理的层次

HarmonyOS 状态管理分为多个层次:

层次 装饰器 作用范围
组件内状态 @State 组件内部
父子组件通信 @Prop / @Link 父组件与子组件
跨组件共享 @Provide / @Consume 祖先与后代组件
应用级状态 AppStorage 整个应用
页面级状态 LocalStorage 单个页面
持久化状态 PersistentStorage 应用重启后保留

三、核心装饰器详解

3.1 @State:组件内状态

@State 是使用最频繁的状态装饰器,用于声明组件内部的可变状态。

特点:

  1. @State 装饰的变量必须是组件内的私有变量。
  2. 变量变化时,会自动触发依赖该变量的 UI 刷新。
  3. 支持基本类型、对象、数组等。
@State count: number = 0;
@State user: User = { name: '张三', age: 18 };
@State list: string[] = ['a', 'b', 'c'];

3.2 @Prop:单向同步

@Prop 用于父子组件之间的单向同步。父组件状态变化会同步到子组件,但子组件内部不能修改 @Prop 变量。

特点:

  1. 父组件数据变化,子组件自动更新。
  2. 子组件不能反向修改父组件数据。
  3. @Prop 变量在子组件内是只读的。

3.3 @Link:双向同步

@Link 用于父子组件之间的双向同步。父组件和子组件共享同一个状态源,任何一方修改都会同步到另一方。

特点:

  1. 父组件数据变化,子组件自动更新。
  2. 子组件修改 @Link 变量,父组件数据也会更新。
  3. 适合需要子组件反向修改父组件状态的场景。

3.4 @Provide / @Consume:跨层级共享

@Provide@Consume 用于祖先组件与后代组件之间的数据共享,可以跨越多个组件层级,避免逐层传递。

// 祖先组件
@Provide theme: string = 'light';

// 后代组件
@Consume theme: string;

四、实战代码:父子组件状态联动

下面我们实现一个演示父子组件状态联动的页面。页面采用卡片式暖色风格,包含父组件计数器和子组件展示器。

4.1 定义子组件

// 子组件:演示 @Prop 单向同步
@Component
struct ChildCounter {
  @Prop count: number = 0;
  @State label: string = '子组件';

  build() {
    Column({ space: 6 }) {
      Text(this.label)
        .fontSize(11)
        .fontColor('#B8860B')
      Text(`${this.count}`)
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#8B4513')
      Text('@Prop 单向同步')
        .fontSize(10)
        .fontColor('#A0522D')
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFF8E7')
    .borderRadius(16)
    .border({ width: 1, color: '#F5DEB3' })
  }
}

代码说明:

ChildCounter 是一个子组件,用于演示 @Prop 单向同步机制:

  1. @Prop 声明@Prop count: number = 0 声明一个 @Prop 装饰的变量。@Prop 变量接收父组件传入的值,并与之保持单向同步。

  2. @State 声明@State label: string = '子组件' 是子组件内部的本地状态,与父组件无关。

  3. UI 展示:子组件显示 labelcount 两个值。count 是父组件传入的,当父组件修改 count 时,这里会自动更新。

  4. 关键理解@Prop 变量在子组件内是只读的。如果子组件尝试修改 this.count,会编译报错或不会同步到父组件。这种设计保证了数据流向的单向性,便于调试和维护。

4.2 定义父组件

@Entry
@Component
struct StateManagementPage {
  @State parentCount: number = 0;
  @State decorators: DecoratorRow[] = [
    { name: '@State', scope: '组件内', desc: '组件内部可变状态,驱动 UI 刷新' },
    { name: '@Prop', scope: '父→子', desc: '单向同步,子组件内只读' },
    { name: '@Link', scope: '父↔子', desc: '双向同步,父子共享同一状态' },
    { name: '@Provide', scope: '祖先→后代', desc: '跨层级向下提供数据' },
    { name: '@Consume', scope: '后代取用', desc: '后代组件接收 Provide 数据' },
    { name: '@Watch', scope: '监听', desc: '监听状态变化触发回调' }
  ];

代码说明:

父组件 StateManagementPage 中:

  • @State parentCount:父组件的核心状态,是数据流的源头。
  • @State decorators:装饰器对照表数据,用于展示各装饰器的区别。

4.3 构建 UI:父子联动演示

build() {
  Scroll() {
    Column({ space: 14 }) {
      // 顶部暖色标题
      Column() {
        Text('STATE')
          .fontSize(12)
          .fontColor('#FFE4B5')
          .letterSpacing(6)
        Text('状态管理')
          .fontSize(26)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
          .margin({ top: 6 })
        Text('@State / @Prop / @Link 联动演示')
          .fontSize(12)
          .fontColor('#FFE4B5')
          .margin({ top: 6 })
      }
      .width('100%')
      .padding({ top: 48, bottom: 28 })
      .backgroundColor('#D2691E')
      .borderRadius({ bottomLeft: 28, bottomRight: 28 })

代码说明:

顶部标题区使用暖橙色(#D2691E)背景,配合米色文字,形成卡片式暖色风格。.borderRadius({ bottomLeft: 28, bottomRight: 28 }) 只设置底部圆角,形成"悬挂"的视觉效果。

      // 父卡片
      Column({ space: 12 }) {
        Row() {
          Column({ space: 4 }) {
            Text('父组件')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#8B4513')
            Text('@State parentCount')
              .fontSize(10)
              .fontColor('#A0522D')
          }
          .alignItems(HorizontalAlign.Start)

          Text(`${this.parentCount}`)
            .fontSize(36)
            .fontWeight(FontWeight.Bold)
            .fontColor('#D2691E')
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .padding(16)
        .backgroundColor('#FFF3D6')
        .borderRadius(20)
        .shadow({ radius: 10, color: '#33D2691E', offsetY: 4 })

        // 子卡片(演示 @Prop)
        ChildCounter({ count: this.parentCount })

代码说明:

父卡片区域:

  1. 父组件状态展示:左侧显示"父组件"标签和装饰器名称,右侧用大号文字显示 parentCount 的当前值。.justifyContent(FlexAlign.SpaceBetween) 让左右两端对齐。

  2. 子组件传参ChildCounter({ count: this.parentCount }) 是组件实例化语法,通过参数传入 count。这里 count 就是 @Prop 变量的初始化值。

  3. 数据流分析:当用户点击按钮修改 this.parentCount 时:

    • 父组件 UI 中显示 parentCount 的 Text 自动刷新。
    • 子组件 ChildCounter@Prop count 同步更新,子组件 UI 自动刷新。
    • 这就是 @Prop 单向同步的完整流程。
        // 胶囊按钮组
        Row({ space: 10 }) {
          Button('+ 增加')
            .height(42)
            .layoutWeight(1)
            .fontSize(14)
            .fontColor(Color.White)
            .backgroundColor('#D2691E')
            .borderRadius(21)
            .onClick(() => { this.parentCount++; })
          Button('- 减少')
            .height(42)
            .layoutWeight(1)
            .fontSize(14)
            .fontColor('#D2691E')
            .backgroundColor('#FFE4B5')
            .borderRadius(21)
            .onClick(() => { this.parentCount--; })
          Button('重置')
            .height(42)
            .layoutWeight(1)
            .fontSize(14)
            .fontColor('#8B4513')
            .backgroundColor('#FFF8E7')
            .borderRadius(21)
            .border({ width: 1, color: '#D2691E' })
            .onClick(() => { this.parentCount = 0; })
        }
        .width('100%')

代码说明:

胶囊按钮组包含三个操作按钮:

  • “+ 增加”:实心橙色胶囊按钮,点击后 this.parentCount++
  • “- 减少”:浅橙背景、橙色文字,点击后 this.parentCount--
  • “重置”:米色背景带描边,点击后 this.parentCount = 0

三个按钮都通过 .borderRadius(21)(高度 42 的一半)形成胶囊形状。每次点击都会修改父组件的 @State 状态,进而触发父子组件 UI 的联动刷新。

        // 装饰器对照表:带表头的三列表格
        Column() {
          Row() {
            Text('装饰器').layoutWeight(1).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#8B4513')
            Text('作用域').layoutWeight(1).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#8B4513')
            Text('说明').layoutWeight(2).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#8B4513')
          }
          .width('100%')
          .padding(10)
          .backgroundColor('#F5DEB3')

          ForEach(this.decorators, (row: DecoratorRow) => {
            Row() {
              Text(row.name)
                .layoutWeight(1)
                .fontSize(12)
                .fontWeight(FontWeight.Medium)
                .fontColor('#D2691E')
                .fontFamily('monospace')
              Text(row.scope)
                .layoutWeight(1)
                .fontSize(12)
                .fontColor('#666666')
              Text(row.desc)
                .layoutWeight(2)
                .fontSize(11)
                .fontColor('#555555')
            }
            .width('100%')
            .padding(10)
            .border({ width: { bottom: 1 }, color: '#F5DEB3' })
          })
        }
        .width('100%')
        .backgroundColor('#FFFDF7')
        .borderRadius(12)
        .border({ width: 1, color: '#F5DEB3' })

代码说明:

装饰器对照表是一个带表头的三列表格:

  1. 表头行Row 中包含"装饰器"、“作用域”、"说明"三个表头单元格,背景色为 #F5DEB3(浅麦色),突出表头。

  2. 数据行ForEach 遍历 decorators 数组,每行三个单元格:

    • 装饰器名称:等宽字体、橙色,突出显示。
    • 作用域:灰色文字,说明数据流向。
    • 说明:深灰色文字,解释装饰器用途。
  3. 列宽控制:通过 .layoutWeight() 控制列宽比例,第一列和第二列各占 1 份,第三列占 2 份,让说明文字有更多空间。

  4. 表格分隔:每行通过底部边框分隔,形成表格线效果。

五、@Link 双向同步实战

@Prop 演示了单向同步,下面我们看看 @Link 双向同步的用法:

// 子组件:演示 @Link 双向同步
@Component
struct LinkChild {
  @Link value: number;

  build() {
    Button(`子组件修改: ${this.value}`)
      .onClick(() => {
        this.value++;  // 修改 @Link 变量,父组件同步更新
      })
  }
}

// 父组件中使用
@State parentValue: number = 0;
// 使用 @Link 需要传入 $ 符号引用
LinkChild({ value: $parentValue })

代码说明:

@Link@Prop 的关键区别:

  1. 初始化语法@Link 变量必须通过 $ 符号引用传入:LinkChild({ value: $parentValue })$ 表示引用绑定,而不是值传递。

  2. 双向同步:子组件修改 this.value,父组件的 parentValue 会同步更新,反之亦然。两者共享同一份数据。

  3. 适用场景:当子组件需要修改父组件的数据时,使用 @Link;当子组件只需要展示父组件数据时,使用 @Prop

六、@Provide / @Consume 跨层级共享

当组件层级较深时,使用 @Prop/@Link 逐层传递会非常繁琐。此时可以使用 @Provide/@Consume

// 祖先组件
@Component
struct GrandParent {
  @Provide theme: string = 'dark';

  build() {
    Column() {
      Child()
    }
  }
}

// 中间层组件(无需传递)
@Component
struct Child {
  build() {
    Column() {
      GrandChild()
    }
  }
}

// 后代组件(直接消费)
@Component
struct GrandChild {
  @Consume theme: string;

  build() {
    Text(`当前主题: ${this.theme}`)
  }
}

代码说明:

  • @Provide theme:祖先组件提供数据,所有后代组件都可以访问。
  • @Consume theme:后代组件消费数据,无需中间组件逐层传递。
  • 当祖先修改 theme 时,所有 @Consume 该变量的后代组件都会自动刷新。
  • 这非常适合主题切换、用户信息等全局性数据。

七、@Watch 状态监听

@Watch 用于监听状态变化并执行回调,非常适合在数据变化时执行副作用操作(如持久化、网络请求):

@State @Watch('onCountChange') count: number = 0;

onCountChange(propName: string): void {
  console.info(`属性 ${propName} 变化为 ${this.count}`);
  // 在这里执行持久化、日志等副作用
  saveToStorage(this.count);
}

代码说明:

  • @Watch('onCountChange') 装饰器参数是回调方法名。
  • count 变化时,自动调用 onCountChange 方法。
  • 回调参数 propName 是发生变化的属性名。
  • @Watch 可以与 @State@Prop@Link 等装饰器组合使用。

八、@Observed / @ObjectLink 对象属性监听

当状态是复杂对象时,@State 只能监听对象引用变化,无法监听对象内部属性的变化。此时需要 @Observed@ObjectLink

@Observed
class User {
  name: string = '';
  age: number = 0;
}

@Component
struct UserView {
  @ObjectLink user: User;

  build() {
    Text(`${this.user.name} ${this.user.age}`)
  }
}

代码说明:

  • @Observed 装饰类,使其属性变化可以被监听。
  • @ObjectLink 装饰对象属性,监听对象内部属性的变化。
  • user.age 变化时,UserView 会自动刷新。

九、应用级状态管理

9.1 AppStorage

AppStorage 是应用级的 UI 状态存储,可以在整个应用范围内共享状态:

// 初始化
AppStorage.setOrCreate('userName', '张三');

// 在组件中使用
@StorageProp('userName') userName: string = '';
@StorageLink('userName') userNameLink: string = '';

9.2 PersistentStorage

PersistentStorage 可以将状态持久化到磁盘,应用重启后状态依然保留:

// 持久化状态
PersistentStorage.persistProp('loginCount', 0);

十、状态管理最佳实践

10.1 数据流向单向化

尽量保持数据流单向(父→子),只有在子组件确实需要修改父组件数据时才使用 @Link。单向数据流更容易调试和维护。

10.2 合理选择装饰器

场景 推荐装饰器
组件内部状态 @State
父传子展示 @Prop
父子双向修改 @Link
跨层级共享 @Provide / @Consume
状态变化监听 @Watch
复杂对象监听 @Observed / @ObjectLink
应用级共享 AppStorage

10.3 避免过度使用 @State

不是所有变量都需要 @State。只有需要驱动 UI 刷新的变量才使用 @State,普通成员变量直接用 private 声明即可,减少不必要的 UI 刷新开销。

10.4 注意状态更新的粒度

@State 数组的更新需要注意:直接修改数组元素(如 this.arr[0] = x)不会触发 UI 刷新,需要使用新的数组替换:

// 错误:不会触发刷新
this.arr[0] = 'new';

// 正确:创建新数组
this.arr = [...this.arr.slice(0, 0), 'new', ...this.arr.slice(1)];

十一、常见问题

11.1 @Prop 修改报错

原因@Prop 变量在子组件内是只读的,不能直接赋值。

解决:如果需要子组件修改数据,改用 @Link

11.2 @Link 初始化报错

原因@Link 必须通过 $ 符号引用传入,直接传值会报错。

解决:使用 Child({ value: $parentValue }) 语法。

11.3 数组元素修改不刷新

原因@State 数组直接修改元素不触发刷新。

解决:使用不可变更新方式,创建新数组替换。

十二、总结

本文深入讲解了 HarmonyOS 状态管理机制,从声明式 UI 的基本概念到各种状态装饰器的详细用法,并通过一个卡片式暖色风格的父子组件联动页面进行了实战演示。

核心要点回顾:

  1. 声明式 UI 的核心是"UI 是状态的函数",状态变化自动刷新 UI。
  2. @State 是组件内状态,是最基础的装饰器。
  3. @Prop 实现父到子的单向同步,子组件内只读。
  4. @Link 实现父子双向同步,通过 $ 引用绑定。
  5. @Provide/@Consume 实现跨层级数据共享。
  6. @Watch 监听状态变化,@Observed/@ObjectLink 监听对象属性。
  7. 合理选择装饰器,保持数据流单向化是良好实践。

掌握状态管理是 ArkUI 开发的精髓所在,理解它才能真正发挥声明式 UI 框架的威力。下一篇我们将讲解自定义组件与 @Builder 复用技巧。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐