鸿蒙 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 状态管理拆成了三个层次,并且用示例把每个层次都跑了起来。

相关推荐
墨理学AI4 小时前
Kylin Linux Advanced Server V10 上成功安装 NVIDIA Container Toolkit
1024程序员节
御承扬4 小时前
编程素养提升之EffectivePython(Builder篇)
python·设计模式·1024程序员节
麦麦大数据4 小时前
F032 材料科学文献知识图谱可视化分析系统(四种知识图谱可视化布局) | vue + flask + echarts + d3.js 实现
vue.js·flask·知识图谱·数据可视化·论文文献·1024程序员节·科研图谱
gs801404 小时前
pnpm + webpack + vue 项目依赖缺失错误排查与解决
pnpm·1024程序员节
独自破碎E4 小时前
双堆法求数据流的中位数
1024程序员节
心态还需努力呀5 小时前
异构多活数据架构支持下的医疗业务高可用实践——全栈信创样本分析
1024程序员节
keven-wang5 小时前
网路基础-设备ip地址忘记,有哪些办法可找回设备IP地址?
ip地址·arp·1024程序员节·找设备ip·网络地址解析协议·网络基础协议
墨利昂5 小时前
Git与Gitee使用中的几个问题
1024程序员节
清风6666665 小时前
基于单片机的故障检测自动保护智能防夹自动门设计及LCD状态显示系统
单片机·毕业设计·课程设计·1024程序员节·期末大作业