【学习目标】

  1. 理解 @Builder 设计初衷,明确与 @Component 核心差异;
  2. 掌握 @Builder 两种定义方式、参数传递(按值/按引用)规则;
  3. 掌握 @Builder 高级场景(嵌套、this指向)实战用法;
  4. 掌握 @BuilderParam UI 插槽原理与使用方法,实现组件 UI 内容灵活定制;
  5. 掌握 @LocalBuilder 用法、参数传递与限制,理解其固定this、维持组件父子关系的核心特性;
  6. 能够清晰对比 @Builder 与 @LocalBuilder,在实际开发中正确选型。

一、Builder 核心认知

1.1 什么是 @Builder

@Builder 用于封装一段UI结构,仅负责UI渲染,无独立实例、无状态、无生命周期,不会加入组件树,编译阶段会直接内联展开,是组件内/全局复用UI片段的最优轻量方案。

1.2 为什么需要 @Builder

我们已经掌握 @Component 封装可复用UI,适用于独立功能、带业务逻辑、带生命周期的页面/组件。

但开发中存在大量纯UI结构、无状态、无业务、无生命周期的复用片段例如 列表内固定骨架图,页面内重复UI区块。

这类片段不需要独立实例、状态、生命周期,使用 @Component 会造成额外实例开销,粒度过细。因此 ArkTS 提供最轻量UI复用方案@Builder 自定义构建函数,专注细粒度UI复用,几乎无额外性能消耗。

1.3 @Builder 与 @Component 核心区别

特性 @Builder @Component
本质 UI构建函数 独立自定义组件
实例 无实例 有独立组件实例
状态支持 不能声明@State 支持@State、@Prop等状态
生命周期 无生命周期 拥有完整生命周期
调用方式 直接调用 作为组件使用
适用场景 纯UI结构复用 功能组件/页面/业务模块
刷新范围 随所属组件整体刷新,无独立刷新 可独立响应式刷新
调用位置 只能在build()内部使用 任意生命周期与场景均可使用

1.4 @Builder 两种定义形式

  • 组件内 @Builder:仅当前组件内使用,可直接访问组件成员变量与状态,跟随组件刷新而刷新;
  • 全局 @Builder:整个工程通用,不依赖任何组件实例,不能访问this,所有数据必须通过参数传递。

二、项目工程

本节工程BuilderDemo基于 API 20+,所有案例页面完整对应如下:

BuilderDemo/
├── AppScope/
├── entry/
│   ├── src/main/ets
│   │   ├── entryability/EntryAbility.ets
│   │   ├── pages/
│   │   │   ├── Index.ets                     # 入口:页面跳转
│   │   │   ├── InnerBuilderPage.ets          # 组件内Builder
│   │   │   ├── GlobalBuilderPage.ets         # 全局Builder
│   │   │   ├── BuilderParamPage.ets          # @BuilderParam UI插槽
│   │   │   ├── AdvancedBuilderPage.ets       # 高级:嵌套、this指向、Binding
│   │   │   └── LocalBuilderPage.ets          # @LocalBuilder 专属案例

三、Index.ets 入口页面

import router from '@ohos.router';

// 定义页面配置接口
interface PageConfig {
  title: string;
  url: string;
}

@Entry
@Component
struct Index {

  pageList: PageConfig[] = [
    { title: "1. 组件内 Builder", url: "pages/InnerBuilderPage" },
    { title: "2. 全局 Builder", url: "pages/GlobalBuilderPage" },
    { title: "3. BuilderParam 插槽", url: "pages/BuilderParamPage" },
    { title: "4. this指向问题", url: "pages/AdvancedBuilderPage" },
    { title: "5. @LocalBuilder", url: "pages/LocalBuilderPage" }
  ];

  // 自定义Builder按钮封装
  @Builder builderButton(title: string, onClick: () => void) {
    Button(title)
      .width(280)
      .onClick(onClick);
  }

  build() {
    Column({ space: 20 }) {
      Text("@Builder & @LocalBuilder 实战")
        .fontSize(26)
        .fontWeight(FontWeight.Bold);

      // ForEach 循环渲染按钮
      ForEach(
        this.pageList,
        (item: PageConfig) => {
          this.builderButton(item.title, () => {
            router.pushUrl({ url: item.url });
          });
        }
      );
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center);
  }
}

四、组件内 @Builder 示例演示

4.1 规则说明

  1. 组件内 @Builder 直接访问 this 状态变量时,会跟随组件整体刷新,不受传参规则限制,自动刷新;
  2. @Builder 默认按值传递参数,传递基础类型、对象变量、多参数时,外部状态更新不会触发 @Builder 刷新;
  3. @Builder 实现响应式刷新的标准方式:使用对象字面量形式传入参数,可建立响应式关联,状态更新自动刷新;
  4. 多层 @Builder 嵌套时,每一层都必须使用对象字面量传参,才能保证全链路刷新;
  5. 禁止在 @Builder 内部修改传入的参数对象,会导致运行异常;
  6. 从 API version 20 开始,可通过 UIUtils.makeBinding()、Binding / MutableBinding 实现 @Builder 内状态变量刷新;
    • 使用 UIUtils.makeBinding() 包装读状态回调,支持 @Builder 内 UI 自动刷新;
    • 额外传入写状态回调,可将 @Builder 内部的参数修改同步到外部组件。

4.2 说明

@Builder被用来展示组件,不会参与动态UI刷新。组件中值的变化是通过使用装饰器的特性,监听到值的改变触发的UI刷新,而不是通过@Builder的能力触发的。

4.3 示例代码

示例 1:无参 @Builder + 直接访问 this → 自动刷新
class UserInfo {
  name: string = ''
  age: number = 0
}

@Entry
@Component
struct InnerBuilderPage {
  @State message: string = '组件内 @Builder 实战'
  @State count: number = 0
  @State user: UserInfo = {name:"Tom", age:25}

  @Builder
  buildTitle() {
    Text(this.message)
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
      .fontColor('#333333')
  }

  build() {
    Column({ space: 15 }) {
      this.buildTitle()
      Button('更新标题')
        .onClick(() => {
          this.message = "组件内 @Builder 实战-更新"
        })
    }
    .width('100%')
  }
}
示例 2:按值传递 → 不刷新
@Builder
buildButton(label: string) {
  Text("基础类型传参 → 不刷新:" + label)
}

// 调用
this.buildButton(`count:${this.count}`)
示例 3:直接传递对象变量(按值传递) → 不刷新
@Builder
buildUserCardByObj(params: UserInfo) {
  Text(`姓名:${params.name} 年龄:${params.age}`)
}

// 调用
this.buildUserCardByObj(this.user)
示例 4:多参数传递(按值传递) → 不刷新
@Builder
buildUserCardByValue(name:string, age:number) {
  Text(`姓名:${name} 年龄:${age}`)
}

// 调用
this.buildUserCardByValue(this.user.name, this.user.age)
示例 5:对象字面量 → 按引用传递 → 刷新
@Builder
buildUserCardByRef(params: UserInfo) {
  Text(`姓名:${params.name} 年龄:${params.age}`)
}

// 调用
this.buildUserCardByRef({ name: this.user.name, age: this.user.age })
示例 6:嵌套 @Builder + 子组件刷新对比
@Component
struct ChildComponent1 {
  @Prop info: UserInfo = new UserInfo();
  build() {
    Row() {
      Text(`ChildComponent1不刷新===${this.info.name}===${this.info.age}`)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
    }
  }
}

@Component
struct ChildComponent2 {
  @Prop childName: string = '';
  @Prop childAge: number = 0;
  build() {
    Row() {
      Text(`ChildComponent2=刷新==${this.childName}===${this.childAge}`)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
    }
  }
}

@Builder
parentBuilder(params: UserInfo) {
  Text(`parentBuilder刷新===${params.name}===${params.age}`)
    .fontSize(20)
    .fontWeight(FontWeight.Bold)
  ChildComponent1({ info: params })        // 不刷新
  ChildComponent2({ childName: params.name, childAge: params.age }) // 刷新
}
示例 7:错误示范:Builder 内部修改参数
@Builder
errorModifyParam(params: UserInfo) {
  Button("修改")
  .onClick(() => {
    params.name = "错误修改" // 运行异常
  })
}
示例 8:API20 新特性 → Binding 绑定传参(可修改+可刷新)
@Builder
ModifyParam(params: Binding<UserInfo>) {
  Text(`姓名:${params.value.name}`)
  Button("内部修改").onClick(() => {
    params.value.name = "新名字"
  })
}

// 调用
this.ModifyParam(UIUtils.makeBinding(
  () => this.user,
  (user) => { this.user = user }
))
运行结果

@Builder

五、全局 @Builder 实战

5.1 核心功能

  • 定义全局通用UI片段,多页面共享;
  • 无组件依赖,不绑定this,所有数据必须通过参数传递。

5.2 规则

  • 必须加 function 关键字
  • 跨页面使用需要加 export

5.3 示例代码

interface  GlobalInfo {
  globalContent:string
  callback?: () => void
}

@Builder
export function globalButton(globalInfo: GlobalInfo) {
  Button(globalInfo.globalContent)
    .width(180)
    .height(46)
    .backgroundColor('#34C85E')
    .margin(5)
    .onClick(globalInfo?.callback)
}

@Builder
export function globalCard(title: string, content: string) {
  Column() {
    Text(title).fontSize(20).fontWeight(FontWeight.Bold)
    Text(content).fontSize(16).margin({ top: 10 })
  }
  .width('100%')
  .padding(20)
  .backgroundColor('#fff')
  .borderRadius(12)
  .margin(10)
}

@Entry
@Component
struct GlobalBuilderPage {
  @State content: string = '全局复用,跨页面统一样式'
  @State globalInfo: GlobalInfo = {globalContent:'全局按钮1'}
  
  build() {
    Column({ space: 20 }) {
      globalCard('全局 @Builder 实战', this.content)
      // 只能通过引用传递 一一传递对应参数 否则无法刷新。
      globalButton({
        globalContent:this.globalInfo.globalContent,
       callback:()=>{
          this.globalInfo.globalContent = '全局按钮点击更新'
        }
      })
    }
    .width('100%')
    .padding(20)
  }
}

5.4 运行结果

全局@Builder

六、@BuilderParam UI插槽

6.1 核心功能

@BuilderParam 用于接收外部传入的 @Builder 函数,实现UI插槽(slot) 能力,让组件结构固定、内容可灵活定制。

6.2 说明

@BuilderParam 是组件属性装饰器,专门用于接收 @Builder 函数类型的参数。
使用场景:父组件向子组件传递一段UI结构,子组件只负责布局,不关心具体内容。

6.3 示例代码

@Component
struct CardComponent {
  @BuilderParam header?: () => void
  @BuilderParam content?: () => void

  build() {
    Column() {
      if (this.header){
        this.header()
      }
      Divider().margin(10).color(Color.Red).strokeWidth(2)

      if (this.content){
        this.content()
      }
    }
    .width('100%')
    .backgroundColor('#fff')
    .padding(20)
    .borderRadius(12)
  }
}

@Entry
@Component
struct BuilderParamPage {
  @Builder
  customHeader() {
    Text('自定义头部').fontSize(22).fontWeight(FontWeight.Bold)
  }
  
  @Builder
  customContent() {
    Text('这是插槽内容,灵活传入任意UI')
    Button('插槽内按钮').width(150).margin(10)
  }
  
  build() {
    Column({ space: 20 }) {
      CardComponent({
        header: this.customHeader,
        content:this.customContent
      })
    }
    .padding(20)
  }
}

6.4 运行结果

@BuilderParam

七、关于this 指向(重点)

我们在使用 @Builder + @BuilderParam 跨组件传递时,会出现一个非常关键的问题:this 指向会变导致UI刷新不符合预期。

7.1 this 指向核心规则

  1. @Builder 直接作为引用传递时,this 由「调用它的组件」决定
  2. 箭头函数没有自己的 this,会继承外层作用域的 this
  3. @BuilderParam 场景:
    • 直接传递:customBuilderParam: this.componentBuilder
      → 函数在 Child 里执行,this 指向 Child
    • 箭头函数包裹:customBuilderParam: () => { this.componentBuilder() }
      → 继承父组件 this,this 指向 Parent

7.2 示例代码

/**
 * 问题:@Builder + @BuilderParam this 指向问题
 * 页面运行结果:Parent  Child  Parent
 * 核心规则:
 * 1. 直接传递函数引用 → this 由【调用者】决定
 * 2. 箭头函数包裹 → this 继承【定义时的组件】
 */
@Component
struct ChildComponent {
  label: string = "Child";

  // 默认占位Builder,无实际UI
  @Builder
  customBuilder() {}

  @Builder
  customChangeThisBuilder() {}

  // 接收外部传入的Builder函数
  @BuilderParam customBuilderParam: () => void = this.customBuilder;
  @BuilderParam customChangeThisBuilderParam: () => void = this.customChangeThisBuilder;

  build() {
    Row({ space: 15 }) {
      // 执行外部传入的构建函数
      this.customBuilderParam()
      this.customChangeThisBuilderParam()
    }
  }
}

@Entry
@Component
struct AdvancedBuilderPage {
  label: string = "Parent";

  // 普通 @Builder:this 由【运行时调用者】决定,不固定!
  @Builder
  componentBuilder() {
    Text(`${this.label}`)
      .fontSize(26)
      .fontWeight(FontWeight.Bold)
  }

  build() {
    Column({ space: 20 }) {
      Text("运行结果:").fontSize(22)
      Row({ space: 15 }) {
        // 1️⃣ 父组件自己调用 → this 指向Parent → 显示 Parent
        this.componentBuilder()

        ChildComponent({
          // 2️⃣ 直接传递函数引用:函数在子组件内执行 → this 指向Child → 显示 Child
          customBuilderParam: this.componentBuilder,

          // 3️⃣ 箭头函数包裹:继承父组件this → this 固定指向Parent → 显示 Parent
          customChangeThisBuilderParam: () => { this.componentBuilder() }
        })
      }
    }
    .width('100%')
    .padding(30)
  }
}

7.3 运行结果

最终页面显示:Parent Child Parent

this指向问题

7.4 一句话总结

this 是函数执行时的上下文对象,它的指向不是在函数定义时确定的,而是在函数调用时才最终确定。

  • 普通函数:谁调用,this 就指向谁(默认规则,也是最容易错乱的场景);
  • 箭头函数:没有自己的 this,继承外层作用域的 this(永远不会错乱,优先推荐);

八、@LocalBuilder 装饰器

@Builder 在跨组件传递时,this 会被改变,这会导致严重问题UI 刷新不符合预期,为了彻底解决这个问题,ArkTS提供了 @LocalBuilder
@LocalBuilder可以固定this指向当前

8.1 设计目的

@LocalBuilder 拥有和局部 @Builder 完全一样的UI构建能力,但可以:

  • 固定 this 指向
  • 保证组件父子关系不变
  • 保证状态管理父子关系不变
    从根源解决 @Builder 的上下文错乱问题。

8.2 核心特性

  1. 只能在组件内部定义,不支持全局;
  2. this 永远指向声明它的组件,不会被任何方式改变;
  3. 跨组件传递时,组件归属固定,不会被修改;
  4. 调用方式与 @Builder 完全一致:this.xxx()

8.3 @LocalBuilder与@Builder对比示例

@Component
struct Child {
  label: string = 'Child';
  @BuilderParam customBuilderParam?: () => void;

  build() {
    if (this.customBuilderParam){
      this.customBuilderParam()
    }else {
      Text('@BuilderParam undefined' )
    }
  }
}

@Entry
@Component
struct LocalBuilderPage {
  label: string = 'Parent';

  // @Builder:this 指向调用方 → Child
  @Builder
  componentBuilder() {
    Text(this.label)
  }

  // @LocalBuilder:this 永远指向 Parent
  @LocalBuilder
  componentLocalBuilder() {
    Text(this.label)
  }

  build() {
    Column({ space: 20 }) {
      // 显示 Child
      Child({ customBuilderParam: this.componentBuilder })
      // 显示 Parent
      Child({ customBuilderParam: this.componentLocalBuilder })
    }
  }
}

8.4 限制条件

  1. 只能在组件内声明,不允许全局
  2. 不能与其他任何装饰器一起使用;
  3. 不能修饰静态方法;
  4. 函数内部不允许修改参数
  5. 作为参数传递时推荐:直接传函数:this.func 或箭头函数:() => { this.func() } this指向都不会变.
  6. 禁止直接传执行结果,会导致布局错乱。

8.5 参数传递规则(与 @Builder 一致)

  1. 按值传递:基础类型、多参数、直接对象 → 不刷新
  2. 按引用传递:单个对象字面量 → 可刷新
  3. 按回调传递:API20 使用 UIUtils.makeBinding → 可刷新可修改

注意:子组件调用父组件的 @LocalBuilder 并传参时,子组件状态变化不会触发 @LocalBuilder 刷新;推荐:@LocalBuilder 内直接访问父组件自身状态。

九、代码仓库

  • 工程名称:BuilderDemo
  • 仓库地址:https://gitee.com/HarmonyOS-UI-Basics/harmony-os-ui-basics.git

十、下节预告

下一节我们将深入学习鸿蒙UI样式复用三大核心方案:

  • 掌握 @Styles 通用样式复用,实现多组件基础样式统一;
  • 掌握 @Extend 组件专属样式扩展,支持参数与私有属性;
  • 掌握 stateStyles 多态状态样式,实现按压/禁用/选中自动切换;
  • 清晰区分三者适用场景,写出简洁、可维护、高性能样式代码;
Logo

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

更多推荐