HarmonyOS 自定义构建函数:@Builder 与 @BuilderParam 的组件复用范式

前言

在 ArkUI 里,当多个页面的某块 UI 结构高度相似、只有内部细节(标题、图标、点击行为)不同时,直接复制粘贴组件会迅速失控。@Builder@BuilderParam 是 ArkTS 提供的「自定义构建函数」机制:前者把一段 UI 抽成可复用函数,后者允许父组件向子组件注入一段 UI 插槽。两者组合,是构建「高内聚、低耦合」通用容器组件(卡片、列表项、对话框、导航栏)的基石。本文用可运行示例讲透它们的用法、参数传递、this 绑定陷阱与插槽注入模式。

问题描述

常见痛点:

  1. 写了三个「信息卡片」,只有标题和图标不同,却复制了三份 Column 结构,改样式要改三处。
  2. 想做一个通用 Dialog,但「确认按钮区域」希望由调用方自定义,不知道怎么传一段 UI 进去。
  3. @Builder 里调用 this.someState 拿到的是 undefined——因为自定义构建函数默认不绑定组件实例的 this
  4. @Builder 当回调传给子组件时,函数内的状态读取错乱。

本质问题:ArkUI 的 UI 复用有两种层次——「整段 UI 复用」用 @Builder「组件内部留一个可替换的 UI 插槽」用 @BuilderParam

细节解析

1. @Builder:局部 UI 函数

  • 全局 @Builder function 与组件内 成员 @Builder 两种写法。
  • 成员 @Builder 需要通过 this.myBuilder() 调用才能访问组件状态;全局 @Builderthis
  • 可带参数,参数支持普通值与「按引用传递的状态」(传状态可实现联动刷新)。

2. @BuilderParam:组件插槽

  • 在子组件里声明 @BuilderParam content: () => void,父组件通过 content={this.xxxBuilder} 注入一段 UI。
  • 注入的 @Builder 若在父组件定义,其 this 指向父组件,天然能读到父组件状态。
  • 这是 ArkUI 实现「容器组件 + 内容插槽」的标准方式,等价于其他框架的 children / slot。

3. this 绑定规则(关键坑)

写法this 指向能否读状态
全局 @Builder function无(全局)
成员 @Builderthis.x() 调用当前组件
@BuilderParam 由父组件传入父组件能(父的状态)

4. 参数按值 vs 按引用

@Builder 传普通参数按值捕获;若传 @State/@Local 变量并希望联动刷新,需直接传该状态变量(引用语义),框架会建立依赖。

示例代码(可运行 ArkTS/ArkUI)

示例 1:成员 @Builder 复用卡片

@ComponentV2
struct InfoCard {
  @Local title: string = '';
  @Local icon: ResourceStr = $r('app.media.startIcon');

  @Builder
  cardHeader() {
    // 成员 Builder,this 指向 InfoCard,能读 this.title
    Row({ space: 8 }) {
      Image(this.icon).width(24).height(24)
      Text(this.title).fontSize(16).fontWeight(FontWeight.Bold)
    }
  }

  build() {
    Column({ space: 10 }) {
      this.cardHeader()
      Text('这里是卡片正文内容。')
    }
    .padding(16)
    .backgroundColor('#F5F5F5')
    .borderRadius(12)
  }
}

示例 2:@BuilderParam 做可定制插槽

// 通用容器:标题栏 + 可注入的内容插槽
@ComponentV2
struct Panel {
  @Param title: string = '面板';
  @BuilderParam body: () => void = () => {};   // 默认空实现

  build() {
    Column({ space: 12 }) {
      Text(this.title).fontSize(18).fontWeight(FontWeight.Bold)
      this.body()   // 父组件注入的 UI 在这里渲染
    }
    .padding(16)
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .shadow({ radius: 6, color: '#10000000' })
  }
}

@Entry
@ComponentV2
struct Page {
  @Local count: number = 0;

  @Builder
  customBody() {
    // 这里 this 指向 Page,能读 this.count
    Column({ space: 8 }) {
      Text(`当前计数:${this.count}`)
      Button('+1').onClick(() => { this.count++; })
    }
  }

  build() {
    Column({ space: 16 }) {
      Panel({ title: '统计面板' }) {
        this.customBody()
      }
    }
    .padding(20)
  }
}

示例 3:带参数的 @Builder 与状态联动

@ComponentV2
struct TagList {
  @Local tags: string[] = ['HarmonyOS', 'ArkTS', 'ArkUI'];

  @Builder
  tagItem(text: string, active: boolean) {
    Text(text)
      .padding({ left: 12, right: 12, top: 4, bottom: 4 })
      .backgroundColor(active ? '#0A59F7' : '#E5E5E5')
      .fontColor(active ? Color.White : Color.Black)
      .borderRadius(14)
  }

  build() {
    Row({ space: 8 }) {
      ForEach(this.tags, (t: string) => {
        this.tagItem(t, t === 'ArkTS') // 第二个参数按值,可改为状态联动
      })
    }
  }
}

总结

  • @Builder 抽「重复 UI」:成员写法用 this.x() 调用以绑定组件状态;全局写法适合纯展示、无状态的片段。
  • @BuilderParam 做「组件插槽」:容器组件对外暴露 body/footer 等插槽,父组件注入自定义 UI,实现高复用。
  • this 绑定是最大陷阱:成员 @Builder 必须 this.x() 调用;@BuilderParam 注入的函数 this 属于父组件。
  • 状态联动要传引用:想让 @Builder 内部随状态刷新,就直接传状态变量而非快照值。
  • 组合使用@Builder 负责「内部复用」,@BuilderParam 负责「外部定制」,两者配合即可用少量代码搭出一套整齐的组件库。

掌握这套范式,你写的「列表项 / 卡片 / 弹窗 / 导航条」就能从「复制粘贴」升级为「一套组件 + N 种插槽」,后期维护成本直线下降。

Logo

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

更多推荐