声明式 UI 构建范式——ArkTS 的 @Builder 装饰器深入

在这里插入图片描述

引言

在 HarmonyOS 的 ArkTS 声明式 UI 框架中,@Builder 装饰器是一个极其重要的构建工具。它允许开发者将 UI 片段封装为可复用的构建函数,从而避免在 build() 方法中编写臃肿的嵌套代码。本文将通过我们英语学习 App 的实际源码,深入剖析 @Builder 的四种使用场景:组件内 Builder、全局 Builder、参数传递、以及导出复用。


一、@Builder 的基本概念

@Builder 是 ArkTS 提供的 UI 构建函数装饰器,它的核心特性有:

  • 封装 UI 片段:将一个独立的 UI 区块封装成一个函数,在 build() 中直接调用
  • 支持两种定义位置:组件内部定义为成员方法,或组件外部定义为全局函数
  • 支持参数传递:可以通过参数向 Builder 传入数据
  • 具备响应式能力:Builder 内部引用组件的 @Local/@Param 变量时,变化会自动触发重绘

在我们的 MainPage.ets 中,总共使用了 6 个 @Builder 来组织首页的各个 UI 区块,形成了一套清晰的 UI 分层结构:

HomePage (主组件)
  ├── topBarBuilder()       — 顶部导航栏
  ├── todayStudyBuilder()   — 今日学习卡片
  ├── swiperBuilder()       — 轮播 Banner
  ├── functionBarBuilder()  — 5 宫格功能栏
  ├── practiceModeBuilder() — 2×2 练习模式
  └── subjectPracticeBuilder() — 学习进度区

二、组件内 @Builder

2.1 基础用法

组件内的 @Builder 方法可以直接访问当前组件的所有属性和状态变量。以下是我们 HomePage 中的 topBarBuilder()

@ComponentV2
export struct HomePage {
  // ...

  build() {
    Column() {
      // 直接调用 Builder
      this.topBarBuilder();

      Scroll() {
        Column({ space: 10 }) {
          this.todayStudyBuilder();
          this.swiperBuilder();
          this.functionBarBuilder();
          this.practiceModeBuilder();
          this.subjectPracticeBuilder();
        }
      }
      // ...
    }
  }

  @Builder
  topBarBuilder() {
    Row() {
      Text('英语学习')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .layoutWeight(1);
      Image($r('app.media.ic_practice_camera'))
        .width(28)
        .height(28)
        .onClick(() => {
          RouterModule.push({ url: RouterMap.SEARCH_INPUT_PAGE });
        });
    }
    .width('100%')
    .height(56)
    .padding({ left: 4, right: 4 });
  }

  // ...
}

关键点解析:

  • Builder 内部直接使用 this.functionsthis.dataSource 等组件状态变量
  • 调用时只需写 this.topBarBuilder(),无需传递任何参数
  • Builder 内引用的状态变量变化时,Builder 会自动触发重新渲染

2.2 访问组件状态的典型示例

todayStudyBuilder() 展示了如何通过 Builder 读取组件数据并渲染 UI:

@Builder
todayStudyBuilder() {
  Column() {
    Row() {
      Text('今日学习')
        .fontSize(16)
        .fontWeight(FontWeight.Medium);
      Text('坚持 7 天')
        .fontSize(12)
        .fontColor($r('sys.color.font_secondary'))
        .layoutWeight(1)
        .textAlign(TextAlign.End);
    }
    .width('100%')
    .margin({ bottom: 8 });

    Row() {
      Text('已学 15 词 / 目标 30 词')
        .fontSize(13)
        .fontColor($r('sys.color.font_secondary'))
        .layoutWeight(1);
      Text('50%')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('sys.color.comp_background_emphasize'));
    }
    .width('100%')
    .margin({ bottom: 8 });

    Progress({ value: 15, total: 30, type: ProgressType.Linear })
      .color($r('sys.color.comp_background_emphasize'))
      .height(8)
      .width('100%');
  }
  .padding(16)
  .borderRadius(16)
  .backgroundColor($r('sys.color.background_primary'));
}

这个 Builder 封装了一个完整的学习进度卡片,包含标题行、数据行和进度条,形成了一个独立的视觉模块。


三、全局 @Builder

当一个 Builder 需要被多个组件复用时,可以将其定义在组件外部,成为全局 Builder。

3.1 全局 Builder 的定义

MainPage.ets 的末尾,我们定义了多个全局 Builder:

// 全局 Builder:直接返回一个组件实例
@Builder
export function HomePageBuilder() {
  HomePage();
}

// 全局 Builder:封装核心功能图标
@Builder
export function CourseCoreBuilder(mainFunctionItem: CourseCoreBar) {
  Column() {
    Image(mainFunctionItem.showIcon)
      .height($r('app.float.vp_24'))
      .width($r('app.float.vp_24'))
      .margin({ bottom: $r('app.float.vp_2') });
    Text(mainFunctionItem.description)
      .fontSize($r('sys.float.Caption_M'));
  }
  .width('100%')
  .height(72)
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
  .stateStyles({ pressed: { .scale({ x: 0.97, y: 0.97 }) } })
  .onClick(() => {
    // 路由跳转逻辑
    // ...
  });
}

// 全局 Builder:封装课程练习卡片
@Builder
export function CoursePracticeBuilder(questions: CourseQuestions) {
  Column() {
    Row() {
      Text(questions.subjectName)
        .fontSize($r('sys.float.Subtitle_S'))
        .fontWeight(FontWeight.Medium);
      // ...
    }
    // ...
  }
}

3.2 全局 Builder 的导出与导入

使用 export 关键字可以将 Builder 导出,供其他模块使用:

// 在 RouterTable 注册页面
@Builder
export function HomePageBuilder() {
  HomePage();
}

在路由表中,可以这样注册:

RouterTable.register('HomePage', wrapBuilder(HomePageBuilder));

同样在 CourseHomePage.ets 中也有类似用法:

@Builder
export function CourseHomePageBuilder() {
  CourseHomePage();
}

四、@Builder 的参数传递

4.1 传递数据对象

全局 Builder CourseCoreBuilder 展示如何传递一个自定义对象:

@Builder
export function CourseCoreBuilder(mainFunctionItem: CourseCoreBar) {
  Column() {
    Image(mainFunctionItem.showIcon)
      .height($r('app.float.vp_24'))
      .width($r('app.float.vp_24'));
    Text(mainFunctionItem.description)
      .fontSize($r('sys.float.Caption_M'));
  }
  // ...
}

functionBarBuilder 中调用:

@Builder
functionBarBuilder() {
  Grid() {
    ForEach(this.functions, (item: CourseCoreBar) => {
      GridItem() {
        // 传递 item 参数给全局 Builder
        CourseCoreBuilder(item);
      };
    }, (item: CourseCoreBar, index: number) => item.description + index);
  }
  .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
  // ...
}

4.2 传递基础类型

全局 Builder CoursePracticeBuilder 接收 CourseQuestions 对象,展示了更复杂的数据传递:

@Builder
export function CoursePracticeBuilder(questions: CourseQuestions) {
  Column() {
    Row() {
      Text(questions.subjectName)
        .fontSize($r('sys.float.Subtitle_S'))
        .fontWeight(FontWeight.Medium);
    };

    Row() {
      Text('开始学习')
        .fontSize($r('sys.float.Caption_L'))
        .fontWeight(FontWeight.Regular)
        .fontColor($r('sys.color.font_secondary'));
      Image($r('app.media.icon_right'))
        .width($r('app.float.vp_16'))
        .height($r('app.float.vp_16'));
    }.layoutWeight(1)
    .justifyContent(FlexAlign.End);

    Row() {
      Text('已掌握')
        .margin({ right: 8 })
        .fontSize($r('sys.float.Body_S'));
      Text() {
        Span(questions.masteredWords + '')
          .fontColor($r('sys.color.comp_background_emphasize'));
        Span(' / ' + questions.totalWords + '');
      }
      .fontSize($r('sys.float.Body_S'));
    }
    // ...
  }
}

4.3 参数传递规范

参数类型 示例 说明
自定义对象 CourseCoreBuilder(item) 推荐,结构清晰
基础类型 Text('hello') 适用于简单场景
Resource 类型 $r('app.media.ic_home') 用于资源引用

五、复用粒度控制

5.1 何时使用 Builder vs 子组件

场景 推荐方案 原因
UI 片段 < 30 行 @Builder 轻量、无额外上下文开销
UI 片段 30-50 行 @Builder 或子组件 视复用范围而定
UI 片段 > 50 行 独立 @Component 便于维护和测试
需要在多页面复用 全局 @Builder + export 模块化共享
需要独立生命周期 独立 @Component Builder 不拥有生命周期

5.2 实际项目中的粒度选择

在我们的项目中,Banner 组件因为需要独立的响应式逻辑和生命周期,所以抽成了独立组件:

@Builder
swiperBuilder() {
  Banner();  // 直接复用 Banner 子组件
}

topBarBuildertodayStudyBuilder 等 UI 片段因为相对简单(20-40 行),且仅在 HomePage 中使用,所以保留为组件内 Builder,保持了良好的封装性。


六、最佳实践

6.1 Builder 命名规范

  • 组件内 Builder 使用 camelCase 命名,以 Builder 结尾:topBarBuildertodayStudyBuilder
  • 全局 Builder 使用 PascalCase 命名,以 Builder 结尾:HomePageBuilderCourseCoreBuilder

6.2 避免 Builder 嵌套过深

如果 Builder 内部逻辑变得复杂,应将其拆解为多个小型 Builder:

// ❌ 不推荐:一个 Builder 包含太多逻辑
@Builder
complexBuilder() { /* 100 行代码 */ }

// ✅ 推荐:拆分为多个小型 Builder
@Builder
partA() { /* 30 行 */ }

@Builder
partB() { /* 30 行 */ }

6.3 导出 Builder 的最佳实践

在路由注册场景中,习惯使用全局 Builder 包装页面组件:

// HomePage.ets 末尾
@Builder
export function HomePageBuilder() {
  HomePage();
}

// CourseHomePage.ets 末尾
@Builder
export function CourseHomePageBuilder() {
  CourseHomePage();
}

这种方式比直接导出组件更灵活,允许在实际调用时注入参数或包装逻辑。


总结

@Builder 装饰器是 ArkTS 声明式 UI 开发的核心工具之一。通过本文的源码分析,我们可以看到:

  1. 组件内 Builder 适合封装单页面内的 UI 片段,直接访问组件状态
  2. 全局 Builder 支持跨页面复用,通过 export 导出
  3. 参数传递 让 Builder 具备数据驱动能力,支持自定义对象和基础类型
  4. 粒度控制 根据复杂度和复用范围,在 Builder 和子组件之间做合理选择

用好 @Builder,能让你的 ArkTS 代码保持清爽、模块化和可维护。

Logo

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

更多推荐