鸿蒙原生 ArkTS 布局深度解析:@Builder 参数化复用布局实战

技术栈:HarmonyOS NEXT (API 24) · ArkTS · ArkUI

核心主题:深入解析 @Builder 装饰器的工作机制,掌握参数化复用布局的设计模式与最佳实践

项目演示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


目录


1. 引言:为什么需要 @Builder

1.1 问题的提出

在 HarmonyOS 应用开发中,随着项目规模的增长,我们经常面临以下挑战:

第一,重复的布局代码。 同一个页面中,多个列表项的布局结构高度相似,每次都要完整书写 Row/Column 的嵌套结构,既浪费时间又容易出错。例如一个消息列表,每条消息都包含头像、标题、内容摘要、时间戳四个元素,若有 20 条消息就要重复写 20 遍几乎相同的代码。

第二,跨页面的样式一致性。 一个电商应用有首页、分类页、购物车页、个人中心页,这些页面都需要"商品卡片"这个 UI 组件。如果每个页面都独立实现,不仅样式难以保持一致,而且当设计规范变更时需要逐页修改,维护成本巨大。

第三,动态内容的灵活渲染。 使用 ForEach 循环渲染列表时,不同项需要展示不同的数据内容和样式变体。如果全部在循环体内写死判断逻辑,代码会变得臃肿不堪,可读性和可维护性急剧下降。

这些问题的本质都是同一个:缺乏一种轻量级、灵活的 UI 复用机制

1.2 ArkUI 复用机制全景

在 ArkUI 框架中,存在四种主要的复用手段,它们各有侧重:

机制核心能力是否创建组件实例性能开销适合场景
@Builder复用 UI 结构片段否(编译期内联)极低轻量级布局片段,高频调用
@Component复用完整组件是(创建实例)较低有独立状态和生命周期的模块
@Styles复用样式属性集最低统一视觉风格,样式一致化
@Extend扩展内置组件给内置组件附加自定义行为

@Builder 与其他三种机制的最大区别在于:它 不创建新的组件实例,而是在编译阶段直接将 @Builder 内部的 UI 代码内联展开到调用位置。这种设计带来了极低的运行时开销。

1.3 一个真实的开发困境

让我们通过一个实际开发场景来感受 @Builder 的价值。假设我们正在开发一个移动端学习应用,需要在首页展示用户卡片、学习进度卡片、推荐课程列表等内容。如果不使用 @Builder,每一个卡片都要完整地书写 Row/Column 的嵌套结构,包括 padding、borderRadius、backgroundColor 等样式属性。而使用 @Builder 后,我们可以把卡片的"骨架"抽取出来,每一张卡片只需要一行调用代码就能渲染完成。


2. @Builder 基础概念与语法

2.1 什么是 @Builder

@Builder 是 ArkUI 框架提供的装饰器,用于将一段可复用的 UI 结构声明为独立的构建函数。被 @Builder 修饰的函数称为 自定义构建函数(Custom Builder Function),它的作用类似于一个轻量的模板,调用时会将内部的 UI 代码内联到调用位置。

2.2 两种定义方式

@Builder 有两种定义方式,它们的作用域和使用场景有所不同。

2.2.1 组件内 @Builder(私有)

组件内定义的 @Builder 只能在当前组件内部使用,通过 this.BuilderName() 调用。

@Component
struct ProfilePage {
  @State userName: string = '张三';
  @State avatarColor: string = '#2196F3';

  // 组件私有 @Builder
  @Builder
  AvatarBadge(size: number) {
    Column() {
      Text(this.userName.charAt(0))
        .fontSize(size * 0.45)
        .fontColor(Color.White)
        .fontWeight(FontWeight.Bold)
    }
    .width(size)
    .height(size)
    .borderRadius(size / 2)
    .backgroundColor(this.avatarColor)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }

  build() {
    Column() {
      this.AvatarBadge(80)  // 大头像
      this.AvatarBadge(48)  // 中头像
      this.AvatarBadge(32)  // 小头像
    }
    .padding(20)
  }
}

私有 @Builder 的核心优势在于可以直接访问组件的状态变量。在上面的例子中,AvatarBadge 内部通过 this.userNamethis.avatarColor 引用了组件的 @State 变量,这是一种非常方便的数据传递方式。

2.2.2 全局 @Builder 函数

全局 @Builder 在组件外定义,不依赖任何组件状态,可以在整个应用中复用。

// 全局 @Builder:可在任意组件中使用
@Builder
function GlobalHeader(title: string, subtitle: string) {
  Column() {
    Text(title)
      .fontSize(22)
      .fontWeight(FontWeight.Bold)
      .fontColor('#333333')
    Text(subtitle)
      .fontSize(14)
      .fontColor('#999999')
      .margin({ top: 6 })
  }
  .width('100%')
  .alignItems(HorizontalAlign.Center)
}

@Component
struct PageA {
  build() {
    Column() {
      GlobalHeader('页面A标题', '页面A副标题')
    }
  }
}

@Component
struct PageB {
  build() {
    Column() {
      GlobalHeader('页面B标题', '页面B副标题')
    }
  }
}
2.2.3 两种方式对比
维度组件内 @Builder全局 @Builder
定义位置组件内部组件外部
调用方式this.BuilderName()BuilderName()
访问状态可通过 this 访问不可
跨组件复用仅当前组件任意组件
适用场景组件内部布局片段跨页面公共布局

选择策略:如果只在一个组件内使用,用私有 @Builder;如果需要跨组件复用,用全局 @Builder。

2.3 参数规则详解

@Builder 的参数设计有几个重要规则,理解这些规则对于正确使用至关重要。

2.3.1 只能有一个参数

这是最容易被误解的规则。@Builder 只能声明一个参数,如果需要传递多个值,必须将它们封装成一个对象或接口。

// 正确:单个参数
@Builder
function ProductCard(product: Product) {
  // product 是一个对象,包含所有需要的字段
}

// 正确:无参数(也是可以的)
@Builder
function EmptyCard() {
  Column() { /* ... */ }
}

// 错误:多个独立参数
@Builder
function WrongCard(title: string, price: number) {
  // 编译错误!不能有多个参数
}

为什么要限制参数数量?这与 @Builder 的状态追踪机制有关。当参数是对象且以对象字面量形式传入时,框架可以精确追踪到内部哪个属性发生了变化,从而只更新受影响的 UI 部分。

2.3.2 参数类型规范

在 ArkTS 严格模式下,参数类型必须显式声明,不能使用 any:

// 错误:使用 any
@Builder
function BadBuilder(data: any) {
  Text(data.name) // 无法类型检查
}

// 正确:使用明确的接口
interface CardData {
  title: string
  description: string
  count?: number
}

@Builder
function GoodBuilder(data: CardData) {
  Text(data.title) // 类型安全
}
2.3.3 可选参数与默认值

接口中的可选字段可以实现灵活的参数传递:

interface ButtonConfig {
  text: string
  type?: 'primary' | 'secondary' | 'danger'
  icon?: string
  disabled?: boolean
}

@Builder
function ConfigurableButton(config: ButtonConfig) {
  Button() {
    Row() {
      if (config.icon) {
        Text(config.icon).margin({ right: 8 })
      }
      Text(config.text)
    }
  }
  .backgroundColor(
    config.type === 'danger' ? '#F44336' :
    config.type === 'secondary' ? '#9E9E9E' : '#2196F3'
  )
  .enabled(config.disabled !== true)
}

// 调用时灵活传参
ConfigurableButton({ text: '提交' })           // 全部默认
ConfigurableButton({ text: '删除', type: 'danger' })  // 覆盖部分
ConfigurableButton({ text: '设置', icon: '⚙️' })       // 带图标

2.4 调用时机与位置

@Builder 只能在 UI 构建上下文中被调用,即 build() 方法或另一个 @Builder 内部。

@Component
struct CallDemo {
  @Builder
  MyBuilder() {
    Text('Hello')
  }

  build() {
    Column() {
      this.MyBuilder()           // 正确:在 build() 中
      if (true) { this.MyBuilder() }  // 正确:在条件中
      ForEach([1, 2, 3], (item: number) => {
        this.MyBuilder()         // 正确:在循环中
      }, (item: number) => `${item}`)
    }
  }

  aboutToAppear() {
    // 错误:在生命周期中调用
    // this.MyBuilder() 编译错误
  }
}

这个限制是合理的:@Builder 的返回值是 UI 节点,必须被包含在组件树中才能生效。

2.5 与 @Component 的区别

这是初学者最容易混淆的知识点。

特性@Builder@Component
创建实例否(编译期内联)
状态管理只能读取父组件状态有独立的 @State
生命周期有(aboutToAppear 等)
适用复杂度轻量级 UI 片段完整功能组件
开销极低较低

选择建议:如果需要独立状态或生命周期,用 @Component;否则优先用 @Builder。


3. 参数传递深度解析:值传递 vs 引用传递

3.1 为什么这很重要

参数传递方式直接决定了一个关键行为:当外部数据变化时,@Builder 内部的 UI 是否会自动刷新? 这是实际开发中遇到频率最高的问题之一。

3.2 值传递(By Value)

值传递是最简单直接的方式。参数在传入 @Builder 时被复制一份,@Builder 内部使用的是副本,与原始数据断开联系。

@Component
struct ValuePassDemo {
  @State count: number = 0

  @Builder
  CountDisplay(value: number) {
    Text(`当前计数:${value}`).fontSize(20)
  }

  build() {
    Column() {
      this.CountDisplay(this.count)  // 值传递
      Button('增加计数')
        .onClick(() => {
          this.count++
          // CountDisplay 不会刷新!因为 value 是快照
        })
        .margin({ top: 20 })
    }
  }
}

3.3 引用传递(By Reference)

引用传递通过对象字面量语法实现。当以 { property: stateVar } 形式传参时,框架会建立追踪关系。

@Component
struct ReferencePassDemo {
  @State count: number = 0

  @Builder
  CountDisplay(config: { value: number }) {
    Text(`当前计数:${config.value}`).fontSize(20)
  }

  build() {
    Column() {
      this.CountDisplay({ value: this.count })  // 引用传递
      Button('增加计数')
        .onClick(() => {
          this.count++
          // CountDisplay 会刷新!
        })
        .margin({ top: 20 })
    }
  }
}

3.4 判断规则总结

调用形式传递方式是否响应变化
Builder(someStateVar)值传递
Builder({ field: someStateVar })引用传递
Builder({ field: "常量" })值传递否(常量不变)

核心规则:参数的形式决定传递方式。基本类型直接传参等于值传递;对象字面量形式等于引用传递。

3.5 推荐实践

默认使用引用传递(对象字面量形式),即使只有一个字段。因为在绝大多数场景下,我们都需要 UI 随数据变化而更新。

// 推荐模式:统一用对象参数
interface DisplayConfig {
  label: string
  value: string | number
}

@Builder
function DisplayWidget(config: DisplayConfig) {
  Row() {
    Text(config.label).fontSize(14)
    Text(`${config.value}`).fontSize(14).fontWeight(FontWeight.Bold)
  }
}

// 调用
this.DisplayWidget({ label: '总价', value: this.totalPrice })

4. 实战案例:从基础到进阶

理论知识只有结合实战才能真正掌握。本章将通过四个递进的案例,展示 @Builder 在不同场景下的应用方式。

4.1 案例一:统一风格的按钮系统

需求分析

一个应用的按钮可能有多种类型(主要、次要、危险)、多种尺寸(大、中、小)、以及多种状态(普通、禁用、加载中)。如果每种组合都独立实现,会产生大量重复代码。

实现思路
  • 定义 ButtonConfig 接口,包含所有配置项
  • 用映射表关联类型与颜色、尺寸与数值
  • 一个全局 @Builder 处理所有变体
完整代码
interface ButtonConfig {
  text: string
  type?: 'primary' | 'secondary' | 'outline' | 'danger'
  size?: 'small' | 'medium' | 'large'
  icon?: string
  fullWidth?: boolean
  disabled?: boolean
  loading?: boolean
  onClick?: () => void
}

// 类型到颜色的映射
const BUTTON_TYPES: Record<string, { bg: string; text: string; border?: string }> = {
  primary: { bg: '#2196F3', text: '#FFFFFF' },
  secondary: { bg: '#9E9E9E', text: '#FFFFFF' },
  outline: { bg: 'transparent', text: '#2196F3', border: '#2196F3' },
  danger: { bg: '#F44336', text: '#FFFFFF' }
}

const BUTTON_SIZES: Record<string, { h: number; fs: number }> = {
  small: { h: 32, fs: 14 },
  medium: { h: 44, fs: 16 },
  large: { h: 56, fs: 18 }
}

@Builder
function AppButton(config: ButtonConfig) {
  const typeCfg = BUTTON_TYPES[config.type ?? 'primary']
  const sizeCfg = BUTTON_SIZES[config.size ?? 'medium']
  const isDisabled = config.disabled === true || config.loading === true

  Button() {
    Row() {
      if (config.loading) {
        LoadingProgress().width(20).height(20).margin({ right: 8 })
      } else if (config.icon) {
        Text(config.icon).fontSize(sizeCfg.fs).margin({ right: 8 })
      }
      Text(config.text).fontSize(sizeCfg.fs).fontColor(typeCfg.text)
    }
  }
  .width(config.fullWidth ? '100%' : undefined)
  .height(sizeCfg.h)
  .backgroundColor(isDisabled ? '#CCCCCC' : typeCfg.bg)
  .border(typeCfg.border ? { width: 1, color: typeCfg.border } : undefined)
  .borderRadius(sizeCfg.h / 2)
  .enabled(!isDisabled)
  .onClick(() => {
    if (!isDisabled && config.onClick) {
      config.onClick()
    }
  })
}
使用示例
@Component
struct ButtonDemo {
  build() {
    Column() {
      AppButton({ text: '主要按钮', type: 'primary' })
      AppButton({ text: '次要按钮', type: 'secondary', icon: '🔔' })
      AppButton({ text: '轮廓按钮', type: 'outline', fullWidth: true })
      AppButton({ text: '危险按钮', type: 'danger' })
      AppButton({ text: '小按钮', size: 'small' })
      AppButton({ text: '大按钮', size: 'large' })
      AppButton({ text: '禁用按钮', disabled: true })
    }
    .padding(20)
    .width('100%')
  }
}

这个案例展示了配置驱动的设计思想:通过映射表,我们可以轻松支持 4 种类型 × 3 种尺寸 = 12 种按钮变体,而不需要写 12 个不同的 @Builder。

4.2 案例二:多状态列表项

需求分析

任务管理应用中,每条任务可能处于不同状态(待开始、进行中、已完成、已逾期),每种状态需要不同的视觉表现。

完整代码
enum TaskStatus {
  PENDING = 'pending',
  IN_PROGRESS = 'in_progress',
  COMPLETED = 'completed',
  OVERDUE = 'overdue'
}

interface TaskItem {
  id: number
  title: string
  description: string
  status: TaskStatus
  progress: number
  deadline?: string
}

const STATUS_MAP: Record<TaskStatus, { color: string; icon: string; text: string }> = {
  [TaskStatus.PENDING]: { color: '#9E9E9E', icon: '⏳', text: '待开始' },
  [TaskStatus.IN_PROGRESS]: { color: '#2196F3', icon: '🔄', text: '进行中' },
  [TaskStatus.COMPLETED]: { color: '#4CAF50', icon: '✅', text: '已完成' },
  [TaskStatus.OVERDUE]: { color: '#F44336', icon: '⚠️', text: '已逾期' }
}

@Builder
function TaskListItem(task: TaskItem) {
  Column() {
    // 第一行:标题 + 状态
    Row() {
      Text(task.title)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor(task.status === TaskStatus.COMPLETED ? '#9E9E9E' : '#333333')
        .decoration({
          type: task.status === TaskStatus.COMPLETED ? TextDecorationType.LineThrough : TextDecorationType.None
        })
        .layoutWeight(1)

      Row() {
        Text(STATUS_MAP[task.status].icon).fontSize(12)
        Text(STATUS_MAP[task.status].text)
          .fontSize(12)
          .fontColor(STATUS_MAP[task.status].color)
          .margin({ left: 4 })
      }
    }
    .width('100%')

    // 第二行:描述
    Text(task.description)
      .fontSize(14)
      .fontColor('#666666')
      .width('100%')
      .margin({ top: 8 })

    // 第三行:进度条(仅进行中/逾期显示)
    if (task.status === TaskStatus.IN_PROGRESS || task.status === TaskStatus.OVERDUE) {
      Row() {
        Progress({ value: task.progress, total: 100 })
          .width('80%')
          .color(STATUS_MAP[task.status].color)
        Text(`${task.progress}%`).fontSize(12).fontColor('#999999').margin({ left: 8 })
      }
      .width('100%')
      .margin({ top: 8 })
    }

    // 第四行:截止时间
    if (task.deadline) {
      Row() {
        Text('📅').fontSize(12).margin({ right: 4 })
        Text(task.deadline)
          .fontSize(12)
          .fontColor(task.status === TaskStatus.OVERDUE ? '#F44336' : '#999999')
      }
      .width('100%')
      .margin({ top: 8 })
    }
  }
  .width('100%')
  .padding(16)
  .backgroundColor(Color.White)
  .borderRadius(12)
  .margin({ bottom: 12 })
}
使用示例
@Component
struct TaskListPage {
  @State tasks: TaskItem[] = [
    { id: 1, title: '设计首页', description: '完成首页设计稿', status: TaskStatus.IN_PROGRESS, progress: 60, deadline: '12月25日' },
    { id: 2, title: '修复 Bug', description: '解决登录问题', status: TaskStatus.COMPLETED, progress: 100, deadline: '12月20日' },
    { id: 3, title: '编写文档', description: '整理架构文档', status: TaskStatus.OVERDUE, progress: 30, deadline: '12月18日' },
    { id: 4, title: '代码 Review', description: '审查代码变更', status: TaskStatus.PENDING, progress: 0 }
  ]

  build() {
    List() {
      ForEach(this.tasks, (task: TaskItem) => {
        ListItem() {
          TaskListItem(task)
        }
      }, (task: TaskItem) => `${task.id}`)
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }
}

4.3 案例三:统一的状态视图

需求分析

几乎每个数据列表页面都需要处理三种状态:加载中、空数据、加载失败。如果每个页面都独立实现,不仅重复而且不统一。

完整代码
enum PageState {
  LOADING = 'loading',
  SUCCESS = 'success',
  EMPTY = 'empty',
  ERROR = 'error'
}

interface StateViewConfig {
  state: PageState
  loadingText?: string
  emptyIcon?: string
  emptyText?: string
  errorText?: string
  onRetry?: () => void
}

@Builder
function StatePlaceholder(config: StateViewConfig) {
  Column() {
    if (config.state === PageState.LOADING) {
      LoadingProgress().width(40).height(40).color('#2196F3')
      Text(config.loadingText ?? '加载中...').fontSize(14).fontColor('#999999').margin({ top: 16 })
    } else if (config.state === PageState.EMPTY) {
      Text(config.emptyIcon ?? '📭').fontSize(64)
      Text(config.emptyText ?? '暂无数据').fontSize(16).fontColor('#666666').margin({ top: 12 })
    } else if (config.state === PageState.ERROR) {
      Text('😢').fontSize(64)
      Text(config.errorText ?? '加载失败').fontSize(16).fontColor('#333333').margin({ top: 12 })
      Button('重新加载').margin({ top: 20 }).onClick(() => config.onRetry?.())
    }
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
}
配合 @BuilderParam 使用
@Component
struct StateContainer {
  @BuilderParam contentBuilder: () => void
  state: PageState = PageState.LOADING
  config: Partial<StateViewConfig> = {}

  build() {
    Stack() {
      Column() {
        this.contentBuilder()
      }
      .width('100%')
      .height('100%')
      .visibility(this.state === PageState.SUCCESS ? Visibility.Visible : Visibility.None)

      StatePlaceholder({ state: this.state, ...this.config })
    }
    .width('100%')
    .height('100%')
  }
}
使用示例
@Component
struct UserListPage {
  @State pageState: PageState = PageState.LOADING
  @State users: User[] = []

  aboutToAppear() {
    this.loadUsers()
  }

  loadUsers() {
    this.pageState = PageState.LOADING
    setTimeout(() => {
      this.users = [] // 模拟空数据
      this.pageState = PageState.EMPTY
    }, 1500)
  }

  build() {
    StateContainer({
      state: this.pageState,
      config: {
        emptyIcon: '👥',
        emptyText: '还没有用户',
        loadingText: '正在加载用户...'
      }
    }) {
      List() {
        ForEach(this.users, (user: User) => {
          ListItem() {
            UserCard(user)
          }
        }, (user: User) => `${user.id}`)
      }
      .width('100%')
      .height('100%')
    }
  }
}

4.4 案例四:完整的 Index.ets 示例

最后,让我们看看在实际项目中如何综合运用这些模式。以下是我们为您创建的示例应用:

文件位置:[Index.ets](file:///e:/MyApplication59/entry/src/main/ets/pages/Index.ets)

这个示例包含了:

  • CardItemBuilder(item: CardItem) - 参数化卡片构建器
  • SectionHeaderBuilder(title: string, showAction: boolean) - 带条件参数的标题构建器
  • EmptyPlaceholderBuilder() - 无参数的空状态构建器
  • DividerBuilder(text?: string) - 可选参数的分隔线构建器

所有这些构建器都在同一个页面中使用,通过 ForEach 实现列表渲染,通过条件语句实现动态内容切换,完美展示了 @Builder 的多种用法。


5. @Builder + @BuilderParam:跨组件复用

5.1 什么是 @BuilderParam

@BuilderParam 是 @Builder 的进阶用法,用于实现跨组件的布局注入。它允许父组件将自定义的 @Builder 内容传给子组件,子组件内部通过调用 @BuilderParam 装饰的属性来渲染。

5.2 基本语法

@Component
struct CardContainer {
  // 声明一个 @BuilderParam,用于接收外部布局
  @BuilderParam contentBuilder: () => void
  title: string = ''

  build() {
    Column() {
      Text(this.title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 12 })
      // 调用外部传入的 @Builder
      this.contentBuilder()
    }
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(12)
    .width('100%')
  }
}

5.3 三种初始化方式

方式一:引用组件的 @Builder
@Component
struct ParentPage {
  @Builder
  CustomContent() {
    Row() {
      Text('内容一')
      Text('内容二').margin({ left: 8 }).fontColor('#2196F3')
    }
  }

  build() {
    CardContainer({
      title: '卡片标题',
      contentBuilder: this.CustomContent  // 引用
    })
  }
}
方式二:引用全局 @Builder
@Builder
function GlobalContent() {
  Column() {
    Text('全局内容').fontSize(16)
    Text('来自全局 Builder').fontSize(12).fontColor('#999999')
  }
}

@Component
struct ParentPage {
  build() {
    CardContainer({
      title: '全局 Builder 卡片',
      contentBuilder: GlobalContent  // 全局引用
    })
  }
}
方式三:尾随闭包语法(最灵活)
@Component
struct ParentPage {
  build() {
    // 使用尾随闭包:将 @Builder 内容直接写在组件调用后面
    CardContainer({ title: '内联内容卡片' }) {
      Row() {
        Text('这是内联的')
        Text('自定义内容').fontColor('#F44336').margin({ left: 4 })
      }
    }
  }
}

5.4 实战:可配置的弹窗组件

@Component
struct CustomDialog {
  @BuilderParam headerBuilder: () => void
  @BuilderParam bodyBuilder: () => void
  @BuilderParam footerBuilder: () => void
  visible: boolean = false

  build() {
    if (this.visible) {
      Column() {
        // 头部区域
        Column() {
          this.headerBuilder()
        }
        .width('100%')
        .padding(16)
        .border({ width: { bottom: 1 }, color: '#E0E0E0' })

        // 内容区域
        Column() {
          this.bodyBuilder()
        }
        .width('100%')
        .layoutWeight(1)
        .padding(16)

        // 底部区域
        Row() {
          this.footerBuilder()
        }
        .width('100%')
        .padding(16)
        .border({ width: { top: 1 }, color: '#E0E0E0' })
      }
      .width('80%')
      .backgroundColor(Color.White)
      .borderRadius(12)
    }
  }
}

// 使用
@Component
struct DialogDemo {
  @State showDialog: boolean = false
  @State inputText: string = ''

  build() {
    Column() {
      Button('打开弹窗')
        .onClick(() => { this.showDialog = true })

      CustomDialog({ visible: this.showDialog }) {
        // Header
        Text('确认操作').fontSize(18).fontWeight(FontWeight.Bold)
      } {
        // Body
        Column() {
          TextInput({ text: this.inputText, placeholder: '请输入内容' })
            .onChange((value: string) => { this.inputText = value })
            .width('100%')
            .height(44)
            .backgroundColor('#F5F5F5')
            .borderRadius(8)
        }
        .width('100%')
      } {
        // Footer
        Button('取消')
          .layoutWeight(1)
          .margin({ right: 8 })
          .backgroundColor('#9E9E9E')
          .onClick(() => { this.showDialog = false })
        Button('确认')
          .layoutWeight(1)
          .backgroundColor('#2196F3')
          .onClick(() => {
            this.showDialog = false
            console.info('用户输入:', this.inputText)
          })
      }
    }
    .padding(20)
  }
}

5.5 注意事项

限制一:@BuilderParam 只能被 @Builder 初始化。 你不能用普通函数或箭头函数来初始化它。

限制二:@BuilderParam 不能带参数。 它的签名必须是 () => void,如果需要参数,可以通过外部闭包捕获。

// 正确:通过闭包捕获外部变量
@Component
struct Parent {
  @State title: string = 'Hello'

  @Builder
  HeaderBuilder() {
    // 闭包捕获了 this.title
    Text(this.title).fontSize(18)
  }

  build() {
    ChildComponent({ headerBuilder: this.HeaderBuilder })
  }
}

6. 性能优化与最佳实践

6.1 @Builder vs @Component 的选择决策

选择的核心依据是:是否需要独立状态和生命周期。

开始
  │
  ├─ 需要独立状态(@State)?
  │    ├─ 是 → @Component
  │    └─ 否 ↓
  │
  ├─ 需要生命周期(aboutToAppear)?
  │    ├─ 是 → @Component
  │    └─ 否 ↓
  │
  ├─ 跨页面复用?
  │    ├─ 是 → 全局 @Builder 或 @Component
  │    └─ 否 ↓
  │
  └─ 默认 → @Builder(优先)

6.2 减少不必要的重建

6.2.1 ForEach 的 key
// 错误:使用索引
ForEach(this.items, (item: Item, index: number) => {
  this.ItemBuilder(item)
}, (item: Item, index: number) => `${index}`)

// 正确:使用唯一 ID
ForEach(this.items, (item: Item) => {
  this.ItemBuilder(item)
}, (item: Item) => `${item.id}`)

使用索引作为 key 会导致列表增删时所有元素重建,使用唯一 ID 可以让框架精确追踪每个元素的变化。

6.2.2 合理拆分 @Builder
// 错误:一个巨大的 @Builder
@Builder
BigBuilder(data: HugeData) {
  // 500 行代码...
}

// 正确:拆分为多个小 @Builder
@Builder
HeaderBuilder(data: HeaderData) { /* ... */ }
@Builder
ContentBuilder(data: ContentData) { /* ... */ }
@Builder
FooterBuilder(data: FooterData) { /* ... */ }

// 组合使用
@Builder
PageBuilder(data: PageData) {
  Column() {
    this.HeaderBuilder(data.header)
    this.ContentBuilder(data.content)
    this.FooterBuilder(data.footer)
  }
}

6.3 参数优化

只传递需要的字段
// 错误:传递整个大对象
@Builder
UserCard(user: CompleteUserData) {
  // 只用了 user.name 和 user.avatar
}

// 正确:只传递需要的字段
interface CardInfo {
  name: string
  avatar: string
}

@Builder
UserCard(info: CardInfo) {
  // 更清晰,减少不必要的数据传递
}
使用可选字段
interface ProductCard {
  name: string
  price: number
  originalPrice?: number  // 可选:有原价才显示折扣
  tag?: string            // 可选:有标签才显示
}

@Builder
ProductCardBuilder(product: ProductCard) {
  Column() {
    Text(product.name).fontSize(16)
    Row() {
      Text(`¥${product.price}`).fontSize(18).fontColor('#F44336')
      if (product.originalPrice) {
        Text(`¥${product.originalPrice}`)
          .fontSize(12)
          .fontColor('#999999')
          .decoration({ type: TextDecorationType.LineThrough })
          .margin({ left: 8 })
      }
    }
    if (product.tag) {
      Text(product.tag)
        .fontSize(11)
        .fontColor('#FF9800')
        .backgroundColor('#FFF3E0')
        .padding({ left: 6, right: 6 })
        .margin({ top: 4 })
    }
  }
}

6.4 避免在 @Builder 中的危险操作

不要在 @Builder 中修改状态:

// 危险:可能在渲染中触发无限循环
@Builder
function DangerousBuilder(data: Config) {
  Column() {
    Button('点击')
      .onClick(() => {
        data.count++ // 可能导致多次渲染
      })
  }
}

不要在 @Builder 中做复杂计算:

// 危险:每次渲染都执行复杂计算
@Builder
function SlowBuilder(data: Config) {
  Column() {
    Text(`结果: ${this.veryExpensiveCalculation(data)}`)
  }
}

// 正确:缓存计算结果到变量
class CachedData {
  result: string
  constructor(data: Config) {
    this.result = this.veryExpensiveCalculation(data)
  }
}

6.5 性能监控

使用 DevEco Studio 的性能分析工具来监控 @Builder 的表现:

  1. 打开 Profile 面板
  2. 选择 Memory Profiler 监控内存
  3. 检查组件树深度和节点数量
  4. 监控列表滚动时的帧率变化

如果发现性能问题,可以考虑:

  • 用 LazeForEach 替代 ForEach 实现懒加载
  • 减少 @Builder 的嵌套层级
  • 避免在列表项中使用复杂的条件渲染

7. 常见问题与避坑指南

7.1 @Builder 参数不触发刷新

问题描述:修改了数据,但 UI 没有更新。

原因:使用了值传递而非引用传递。

解决方案:将参数改为对象形式。

// 错误
@Builder
MyBuilder(text: string) { Text(text) }
this.MyBuilder(this.stateText) // 值传递

// 正确
@Builder
MyBuilder(config: { text: string }) { Text(config.text) }
this.MyBuilder({ text: this.stateText }) // 引用传递

7.2 数组修改不生效

问题描述:修改数组元素后 UI 不更新。

原因:直接修改数组引用,框架检测不到变化。

解决方案:使用展开运算符创建新数组。

// 错误
this.list.push(newItem)
this.list[0].name = 'new name'

// 正确
this.list = [...this.list, newItem]
this.list = this.list.map((item, index) =>
  index === 0 ? { ...item, name: 'new name' } : item
)

7.3 多个参数如何处理

问题描述:需要传递多个值,但 @Builder 只允许一个参数。

解决方案:封装为对象。

// 错误
@Builder
Card(title: string, desc: string, color: string) { /* ... */ }

// 正确
interface CardConfig {
  title: string
  desc: string
  color: string
}
@Builder
Card(config: CardConfig) { /* ... */ }

7.4 全局 @Builder 中访问 this

问题描述:全局 @Builder 中无法使用 this。

原因:全局 @Builder 不属于任何组件,没有 this 上下文。

解决方案:通过参数传递需要的数据。

// 全局 @Builder:通过参数传值
@Builder
function GlobalCard(data: CardData) {
  Text(data.title).fontSize(18)
}

// 组件内 @Builder:可以用 this
@Component
struct MyComponent {
  @State title: string = ''

  @Builder
  LocalCard() {
    Text(this.title).fontSize(18) // 直接访问
  }
}

7.5 @Builder 内部使用 if-else

问题描述:在 @Builder 中使用条件语句有什么限制?

解答:可以正常使用 if-else、switch-case,但要注意:

  • 条件判断结果会影响 UI 结构,框架会正确处理
  • 复杂的条件逻辑建议放在外部计算好结果再传入
@Builder
function StatusBadge(config: StatusConfig) {
  if (config.status === 'online') {
    Text('在线').fontColor('#4CAF50')
  } else if (config.status === 'busy') {
    Text('忙碌').fontColor('#F44336')
  } else {
    Text('离线').fontColor('#9E9E9E')
  }
}

7.6 ArkTS 严格模式限制

在 API 24 中,ArkTS 有更严格的限制:

  • 不允许 any 类型
  • 不允许解构赋值
  • 不允许 as 类型断言
  • 不允许 in 运算符
  • 不允许 try-catch 外的异常处理

编写 @Builder 时要确保符合这些限制。


8. 架构设计模式

8.1 原子化设计

将复杂的 @Builder 拆分为最小的可复用单元:

// 原子:最小的样式单元
@Builder
function PrimaryText(content: string) {
  Text(content).fontSize(14).fontColor('#333333')
}

@Builder
function SecondaryText(content: string) {
  Text(content).fontSize(12).fontColor('#999999')
}

@Builder
function IconWrapper(icon: string) {
  Text(icon).fontSize(24)
}

// 组合:将原子组合为功能单元
@Builder
function UserInfo(user: User) {
  Row() {
    IconWrapper(user.avatar)
    Column() {
      PrimaryText(user.name)
      SecondaryText(user.email)
    }
    .margin({ left: 8 })
  }
}

// 页面:将功能单元组合为完整页面
@Builder
function ProfilePage(user: User) {
  Column() {
    UserInfo(user)
    // 更多组件...
  }
}

8.2 配置驱动

通过配置对象控制 @Builder 的行为:

type ButtonVariant = 'primary' | 'secondary' | 'outline'
type ButtonSize = 'small' | 'medium' | 'large'

interface StyledButtonConfig {
  text: string
  variant?: ButtonVariant
  size?: ButtonSize
  onClick?: () => void
}

// 配置映射表
const VARIANT_STYLES: Record<ButtonVariant, { bg: string; textColor: string }> = {
  primary: { bg: '#2196F3', textColor: '#FFFFFF' },
  secondary: { bg: '#9E9E9E', textColor: '#FFFFFF' },
  outline: { bg: 'transparent', textColor: '#2196F3' }
}

const SIZE_STYLES: Record<ButtonSize, { height: number; fontSize: number }> = {
  small: { height: 32, fontSize: 14 },
  medium: { height: 44, fontSize: 16 },
  large: { height: 56, fontSize: 18 }
}

@Builder
function StyledButton(config: StyledButtonConfig) {
  const variant = config.variant ?? 'primary'
  const size = config.size ?? 'medium'
  const variantStyle = VARIANT_STYLES[variant]
  const sizeStyle = SIZE_STYLES[size]

  Button(config.text)
    .height(sizeStyle.height)
    .fontSize(sizeStyle.fontSize)
    .backgroundColor(variantStyle.bg)
    .fontColor(variantStyle.textColor)
    .onClick(() => config.onClick?.())
}

8.3 主题化

利用配置驱动实现多主题支持:

interface Theme {
  primaryColor: string
  secondaryColor: string
  backgroundColor: string
  textPrimary: string
  textSecondary: string
}

const LIGHT_THEME: Theme = {
  primaryColor: '#2196F3',
  secondaryColor: '#9E9E9E',
  backgroundColor: '#FFFFFF',
  textPrimary: '#333333',
  textSecondary: '#999999'
}

const DARK_THEME: Theme = {
  primaryColor: '#64B5F6',
  secondaryColor: '#757575',
  backgroundColor: '#1A1A1A',
  textPrimary: '#FFFFFF',
  textSecondary: '#AAAAAA'
}

@Component
struct ThemedComponent {
  @State isDarkMode: boolean = false

  private get currentTheme(): Theme {
    return this.isDarkMode ? DARK_THEME : LIGHT_THEME
  }

  @Builder
  ThemedCard(content: string) {
    Column() {
      Text(content).fontColor(this.currentTheme.textPrimary)
    }
    .padding(16)
    .backgroundColor(this.currentTheme.backgroundColor)
    .borderRadius(12)
  }

  build() {
    Column() {
      this.ThemedCard('卡片内容')
      Button('切换主题')
        .onClick(() => { this.isDarkMode = !this.isDarkMode })
    }
  }
}

8.4 组件库模式

将常用的 @Builder 收集为组件库:

// builders/CommonBuilders.ets
// 收集项目中所有的通用 @Builder

export interface CardConfig {
  title: string
  content: string
  icon?: string
}

@Builder
export function CommonCard(config: CardConfig) {
  Column() {
    if (config.icon) {
      Text(config.icon).fontSize(32)
    }
    Text(config.title).fontSize(16).fontWeight(FontWeight.Bold).margin({ top: 8 })
    Text(config.content).fontSize(14).fontColor('#666666').margin({ top: 4 })
  }
  .padding(16)
  .backgroundColor(Color.White)
  .borderRadius(12)
}

export interface ListSectionConfig {
  title: string
  items: string[]
}

@Builder
export function CommonListSection(config: ListSectionConfig) {
  Column() {
    Text(config.title).fontSize(18).fontWeight(FontWeight.Bold)
    ForEach(config.items, (item: string) => {
      Text(item).fontSize(14).padding({ top: 8 })
    }, (item: string, index: number) => `${index}`)
  }
  .padding(16)
}

9. 总结与展望

9.1 核心知识点回顾

知识点核心要点
语法分类组件内私有 @Builder 与全局 @Builder 函数
参数规则只能一个参数,多值需封装对象
传递方式对象字面量 = 引用传递(触发刷新)
调用位置仅在 build() 或其他 @Builder 中
跨组件配合 @BuilderParam 实现布局注入
性能优势编译期内联,无组件创建开销

9.2 使用决策流程

是否需要独立状态?
  ├─ 是 → @Component
  └─ 否 ↓

是否需要生命周期?
  ├─ 是 → @Component
  └─ 否 ↓

是否跨页面复用?
  ├─ 是 → 全局 @Builder
  └─ 否 ↓

默认使用 → 组件内 @Builder

9.3 API 24 新特性

HarmonyOS NEXT API 24 与 @Builder 相关的改进:

  • 更好的类型推断:@Builder 参数类型推断更精确
  • 性能优化:编译期内联策略改进
  • V2 装饰器协同:@Builder 可更灵活配合 @ComponentV2
  • 跨 Ability 支持:支持跨 Ability 的 @Builder 调用

9.4 最佳实践清单

  • 优先用对象形式传参(引用传递)
  • 参数数量合理控制(建议 ≤ 5 个字段)
  • 不做 .5 内部复杂计算
  • 合理拆分 @Builder(3-4 层嵌套)
  • ForEach 使用唯一 ID 作为 key
  • 配合 @BuilderParam 实现跨组件注入
  • 主题化支持:将颜色值集中管理
  • 组件库化:收集通用 @Builder

9.5 进阶学习资源

资源链接
官方 @Builder 文档https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-builder-V5
@BuilderParam 指南https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-builderparam-V5
ArkTS 语言规范https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-get-started-V5
示例工程https://gitee.com/harmonyos/samples

附录:完整示例代码

项目文件

  • 示例应用:[Index.ets](file:///e:/MyApplication59/entry/src/main/ets/pages/Index.ets) - 包含所有 @Builder 模式的综合示例

应用功能

运行示例应用后,您将看到:

  1. 卡片列表:5 个不同颜色、不同徽标数量的功能卡片
  2. 标题联动:点击任意卡片,顶部标题实时更新
  3. 动态增删:点击底部按钮可以添加新卡片或清空列表
  4. 多种 Builder:展示了参数化、无参数、可选参数等多种 @Builder 用法

关键代码结构

Index.ets
├── interface CardItem        // 数据模型
├── @State cardList           // 列表数据
├── @State sectionTitle       // 标题状态
├── @Builder CardItemBuilder  // 参数化卡片构建器
├── @Builder SectionHeaderBuilder // 标题构建器
├── @Builder EmptyPlaceholderBuilder // 空状态构建器
├── @Builder DividerBuilder   // 分隔线构建器
├── 辅助方法                  // getTitleFirstChar, updateSectionTitle
└── build()                   // 页面构建

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值