技术栈:HarmonyOS NEXT (API 24) · ArkTS · ArkUI
核心主题:深入解析
@Builder装饰器的工作机制,掌握参数化复用布局的设计模式与最佳实践
项目演示



目录
- 1. 引言:为什么需要 @Builder
- 2. @Builder 基础概念与语法
- 3. 参数传递深度解析:值传递 vs 引用传递
- 4. 实战案例:从基础到进阶
- 5. @Builder + @BuilderParam:跨组件复用
- 6. 性能优化与最佳实践
- 7. 常见问题与避坑指南
- 8. 架构设计模式
- 9. 总结与展望
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.userName 和 this.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 的表现:
- 打开 Profile 面板
- 选择 Memory Profiler 监控内存
- 检查组件树深度和节点数量
- 监控列表滚动时的帧率变化
如果发现性能问题,可以考虑:
- 用 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 模式的综合示例
应用功能
运行示例应用后,您将看到:
- 卡片列表:5 个不同颜色、不同徽标数量的功能卡片
- 标题联动:点击任意卡片,顶部标题实时更新
- 动态增删:点击底部按钮可以添加新卡片或清空列表
- 多种 Builder:展示了参数化、无参数、可选参数等多种 @Builder 用法
关键代码结构
Index.ets
├── interface CardItem // 数据模型
├── @State cardList // 列表数据
├── @State sectionTitle // 标题状态
├── @Builder CardItemBuilder // 参数化卡片构建器
├── @Builder SectionHeaderBuilder // 标题构建器
├── @Builder EmptyPlaceholderBuilder // 空状态构建器
├── @Builder DividerBuilder // 分隔线构建器
├── 辅助方法 // getTitleFirstChar, updateSectionTitle
└── build() // 页面构建

238

被折叠的 条评论
为什么被折叠?



