鸿蒙 HarmonyOS 6|ArkUI(03):状态管理

前言

我们这篇文章会把 ArkUI 在鸿蒙 6 里的状态管理讲清楚。

我们会按照三个层次来拆,也就是组件内局部状态、父子之间的同步、跨层的共享。我们会配上可运行的最小片段,并在最后给出一张决策表和一份常见误用清单。

一、先把三个层次的全景图说清楚

在 HarmonyOS 6 的 ArkUI 里,状态管理有一套清晰的装饰器体系。组件自己的可变数据用 @State 来承载,父子之间的单向同步用 @Prop 来承载,父子之间需要双向同步时用 @Link 来承载,跨多层的共享用 @Provide@Consume 来承载。

二、组件内局部状态:@State

@State 表示这个组件的本地可变数据。我们只要更新它,ArkUI 就会定位到依赖它的节点并触发重渲染。这个特性让我们可以放心把组件的细小交互放在本地管理,不必把所有变量都提到共享层。

TypeScript 复制代码
@Component
export struct CounterCard {
  @State count: number = 0;

  build() {
    Column({ space: 10 }) {
      Text(`当前计数是 ${this.count}`).fontSize(16)
      Button('加一').onClick(() => {
        this.count += 1;
      })
    }
    .padding(12)
  }
}

@Entry
@Component
struct Index {
  build() {
    Column({ space: 16 }) {
      CounterCard()
    }
    .justifyContent(FlexAlign.Center) // 让组件在竖直方向居中
    .width('100%')
    .height('100%')
    .padding(24)
  }
}

这段代码体现了状态驱动的基本套路。我们把界面写成状态的投影,用户点一次按钮,count 改一次,界面就会跟着更新。官方的状态章节明确说明了这种渲染绑定的机制和适用范围,我们保持就近封装就可以。

三、父子单向同步:@Prop

当父组件有一个数据需要交给子组件展示,但不希望子组件反向修改父组件时,我们使用 @Prop。父组件的状态变化会单向同步到子组件,而子组件对 @Prop 的修改不会再写回父组件的数据源。

TypeScript 复制代码
@Component
export struct TitleView {
  @Prop title: string

  build() {
    Text(this.title)
      .fontSize(18)
      .fontWeight(FontWeight.Medium)
  }
}

@Component
export struct PageHeader {
  @State pageTitle: string = '我的页面'

  build() {
    Column({ space: 8 }) {
      TitleView({ title: this.pageTitle })
      Button('改标题')
        .onClick(() => {
          this.pageTitle = '新的标题'
        })
    }
    .padding(12)
  }
}

@Entry
@Component
struct Index {
  build() {
    Column({ space: 16 }) {
      PageHeader()
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
    .padding(24)
  }
}

在这段代码里,PageHeader 持有真实状态,TitleView 只接收并展示。官方的 @Prop 文档还强调了初始化规则和与其他装饰器的组合关系,我们按文档理解数据流,就能避免意外的双向写回。

当子组件需要既接到父组件的值,又要把自己的修改同步回父组件时,我们使用 @Link。这相当于在父子之间建立一条双向的通道,父变子跟,子改父也变,非常适合输入框这类场景。(Medium)

TypeScript 复制代码
@Component
export struct NameInput {
  @Link name: string

  build() {
    Column({ space: 8 }) {
      Text('请输入名字').fontSize(14)
      TextInput({ text: this.name })
        .onChange((v: string) => {
          this.name = v
        })
        .width('100%')
    }
    .padding(12)
  }
}

@Component
export struct ProfileForm {
  @State userName: string = '小雨'

  build() {
    Column({ space: 10 }) {
      // 关键:@Link 需要按"引用"传参
      NameInput({ name: $userName })
      Text(`欢迎你,${this.userName}`).fontSize(16)
    }
    .padding(12)
  }
}

@Entry
@Component
struct Index {
  build() {
    Column({ space: 16 }) {
      ProfileForm()
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
    .padding(24)
  }
}

这里的 NameInput 收到初始值后,可以把输入变化回写到 ProfileForm

五、跨层共享:@Provide@Consume

当多个层级的组件都需要访问同一份数据,而中间层只是路过时,我们使用 @Provide@Consume。父辈通过 @Provide 暴露一个可共享的状态,后代通过 @Consume 直接拿到这份状态,并且形成双向同步的关系。

TypeScript 复制代码
// 顶层提供共享状态
@Component
export struct AppShell {
  @Provide theme: 'light' | 'dark' = 'light'

  build() {
    Column({ space: 12 }) {
      ThemeSwitcher()
      ContentArea()
    }
    .padding(12)
  }
}

// 任意后代直接消费共享状态
@Component
export struct ThemeSwitcher {
  @Consume theme: 'light' | 'dark'

  build() {
    Row({ space: 8 }) {
      Text(`当前主题是 ${this.theme}`)
      Button('切到浅色').onClick(() => this.theme = 'light')
      Button('切到深色').onClick(() => this.theme = 'dark')
    }
  }
}

这段代码说明了共享的真正含义。我们在顶层放一次 @Provide,就能让深层组件通过 @Consume 直接拿到同一份变量。官方文档明确指出两者之间是双向同步关系,所以后代的修改也会反馈到提供者的变量上,我们写共享逻辑时要有这个意识。

六、把状态和导航配合起来更顺手

在鸿蒙 6 的推荐导航范式里,我们通常会在入口页通过 @Provide 暴露一个 NavPathStack,让子页在需要返回时直接 pop,或者在需要进入下一个子页时 pushPath。这种写法把页面状态和页面栈都放在一个可控的范围里,跳转和返回更容易理解。

TypeScript 复制代码
@Entry
@Component
export struct Index {
  @Provide('stack') stack: NavPathStack = new NavPathStack()

  @Builder pageMap(name: string) {
    if (name === 'Detail') {
      DetailPage()
    }
  }

  build() {
    Navigation(this.stack) {
      Button('进入详情').onClick(() => this.stack.pushPath({ name: 'Detail' }))
    }
    .navDestination(this.pageMap)
  }
}

@Component
export struct DetailPage {
  @Consume('stack') stack: NavPathStack

  build() {
    NavDestination() {
      Button('返回').onClick(() => this.stack.pop())
    }
  }
}

我们把页面栈作为共享状态提供给后代,这样子页就不需要额外拿路由句柄了。官方的导航文档和相关问答都建议用这条思路来组织页面,代码更简洁,也更符合鸿蒙 6 的范式。(华为开发者官方网站)

七、选型决策表:先局部,后父子,再跨层

我们现在把日常开发里的常见情景放进一张表,方便快速决策。

场景 推荐装饰器 数据方向 说明
组件内部的小交互 @State 组件内单点更新 变更只影响本组件,渲染范围最小,成本最低。
父传子展示 @Prop 父到子单向 子组件不回写,稳定可控,利于定位问题。
父子联动输入 @Link 父子双向 仅在确需回写时使用,避免到处双向同步。
多层共享主题或会话 @Provide/@Consume 祖先与后代双向 避免层层传参,控制好共享的边界与变更频率。

这张表背后的原则很直白。共享范围越小,优先级越高;能局部就局部,能单向就不双向,能父子就不要全局。等到确实需要跨层共享时,我们再引入 @Provide/@Consume,并把更新频率和依赖范围都控制住。

八、最佳实践清单

第一条建议是控制更新面。我们尽量在计算阶段使用临时变量,把最终结果一次性写回状态变量,这样可以减少多次写状态带来的重复渲染。

第二条建议是分层管理状态。我们把组件内状态与共享状态分开,避免把所有变量都提升为共享,这样渲染边界会更清楚。

第三条建议是谨慎使用双向同步。我们在能用单向数据流完成需求的情况下,就不要轻易引入 @Link,因为它会扩大可变范围。只有在输入控件确实需要回写到父组件时,才使用双向。

第四条建议是把跨层共享和导航解耦。我们让 @Provide/@Consume 只处理需要跨层的状态,让 Navigation 只处理页面栈,这样两个系统各司其职,问题更容易定位。

九、总结

我们已经把 HarmonyOS6 的 ArkUI 状态管理拆成了三个层次,并且用示例把每个层次都跑了起来。

相关推荐
2301_800256113 小时前
关系数据库小测练习笔记(1)
1024程序员节
GLAB-Mary3 小时前
HCIE最优规划路线:如何系统性学习华为认证?
学习·华为·华为认证·hcie·数通
lqj_本人4 小时前
鸿蒙Cordova插件架构与OnsenUI组件适配机制深度解析
华为·架构·harmonyos
猫林老师7 小时前
Flutter for HarmonyOS开发指南(六):测试、调试与质量保障体系
flutter·华为·harmonyos
小Mei数码说12 小时前
华为WATCH 5:连接心与心,让生活更美好
华为·智能手表
爱笑的眼睛1112 小时前
HarmonyOS 应用开发中的内存泄漏检测与修复:深入探索与实践指南
华为·harmonyos
金融小师妹12 小时前
基于多源政策信号解析与量化因子的“12月降息预期降温”重构及黄金敏感性分析
人工智能·深度学习·1024程序员节
kangyouwei13 小时前
鸿蒙开发:19-本地开发配置bash环境执行hvigorw命令
前端·harmonyos
爱笑的眼睛1113 小时前
HarmonyOS 应用开发:系统权限申请与管理深度解析
华为·harmonyos
DIY机器人工房15 小时前
科普:华为星闪是什么?华为星闪(英文名 NearLink)是国际星闪无线短距通信联盟发布的新型无线短距通信标准技术。
stm32·嵌入式硬件·华为·嵌入式·diy机器人工房·嵌入式面试题