鸿蒙 ArkTS UI 复用装饰器全解:@Builder/@BuilderParam/@Styles/@Extend 实战避坑指南
摘要:在 HarmonyOS NEXT 开发中,UI 复用是提升开发效率的核心手段。本文深度解析 ArkTS 四大 UI 复用装饰器的原理、使用场景与易踩坑点,配套完整可运行代码,帮你彻底搞清楚"什么时候用哪个"。
前言:复用的困境
刚上手鸿蒙开发,很多同学会面临这样的困境:
同样的 Button 样式用了 10 次,每次都写一堆相同的 .backgroundColor().borderRadius().fontSize()……
改个颜色就得找 10 个地方,稍不注意就遗漏一处,改出 Bug。
ArkTS 提供了四个装饰器专门解决这个问题:@Builder、@BuilderParam、@Styles、@Extend。它们各司其职,却经常被混淆。今天我们从原理到实战,把这四个装饰器彻底搞清楚。
一、四者关系一图搞懂
UI 复用装饰器├── @Styles → 复用通用样式(链式属性)├── @Extend → 扩展特定组件样式(带参数)├── @Builder → 复用 UI 结构(组件片段)└── @BuilderParam → 组件插槽(父传子的UI片段)
一句话区别:
- 样式
- 结构复用用
@Builder、@BuilderParam
二、@Styles:提取公共样式
基本用法
@Styles 用于将多个通用样式属性提取成一个方法,减少重复代码。
// 定义通用卡片样式@StylesfunctioncardStyle() { .width('100%') .padding(16) .backgroundColor('#FFFFFF') .borderRadius(12) .shadow({ radius: 8, color: '#1A000000', offsetX: 0, offsetY: 2 })}@Entry@Componentstruct StylesDemo { build() { Column({ space: 16 }) { // 多个组件复用同一套样式 Column() { Text('订单卡片').fontSize(16).fontWeight(FontWeight.Bold) Text('¥299.00').fontSize(24).fontColor('#FF4D4F') } .cardStyle() // ✅ 直接调用 Column() { Text('地址卡片').fontSize(16).fontWeight(FontWeight.Bold) Text('广东省深圳市南山区...').fontSize(14).fontColor('#666666') } .cardStyle() // ✅ 复用相同样式 } .padding(16) }}
⚠️ 坑1:@Styles 不能带参数
// ❌ 错误!@Styles 方法不允许传参@Stylesfunction textStyle(color: string) { // 编译报错 .fontColor(color)}// ✅ 需要传参时,改用 @Extend
⚠️ 坑2:@Styles 只能用于通用属性
@Stylesfunction buttonStyle() { .width(120) .height(44) .stateEffect(true) // ✅ 通用属性 OK // .type(ButtonType.Capsule) // ❌ Button 专属属性,@Styles 里不能用}
关键原则:@Styles 里只能写任何组件都支持的公共属性(width/height/padding/margin/backgroundColor 等),不能写特定组件的专属属性。
三、@Extend:扩展特定组件样式
@Extend 专门用于扩展某个特定组件的样式,支持传参,能使用该组件的专属属性。
基本用法
// 扩展 Text 组件:带参数的标题样式@Extend(Text)function titleStyle(size: number, color: string = '#333333') { .fontSize(size) .fontColor(color) .fontWeight(FontWeight.Bold) .lineHeight(size * 1.5)}// 扩展 Button 组件:主按钮样式@Extend(Button)function primaryBtn(width: number = 200) { .type(ButtonType.Capsule) // ✅ Button 专属属性 .width(width) .height(44) .backgroundColor('#4A90E2') .fontColor('#FFFFFF') .fontSize(16) .stateEffect(true)}@Entry@Componentstruct ExtendDemo { build() { Column({ space: 20 }) { Text('页面主标题').titleStyle(24) Text('次级标题').titleStyle(18, '#666666') Text('警告提示').titleStyle(14, '#FF4D4F') Button('立即购买').primaryBtn() Button('加入购物车').primaryBtn(160).backgroundColor('#FF7A00') // ✅ 可链式覆盖 } .padding(24) }}
@Styles vs @Extend 对比
⚠️ 坑3:@Extend 只能写在全局
@Componentstruct MyComp { // ❌ 错误!@Extend 不能定义在组件内部 @Extend(Text) function innerStyle() { .fontSize(14) } build() { ... }}// ✅ 必须写在组件外(文件顶层)@Extend(Text)function innerStyle() { .fontSize(14) }
四、@Builder:可复用的 UI 片段
@Styles 和 @Extend 只能复用属性,而 @Builder 可以复用整段UI 结构(包含子组件、布局等)。
全局 @Builder
// 商品价格展示组件(全局定义)@BuilderfunctionPriceTag(price: number, originalPrice: number) { Row({ space: 8 }) { Text(`¥{originalPrice}`) .fontSize(14) .fontColor('#999999') .decoration({ type: TextDecorationType.LineThrough }) Text(`省¥r('app.media.heart_filled'): r('app.media.product_img')) .width('100%') .aspectRatio(1) .borderRadius(8) // 调用局部 Builder this.FavoriteIcon() .margin({ top: 8, right: 8 }) } }}
⚠️ 坑4:@Builder 中修改状态必须用 $$
ArkTS 中 @Builder 传参默认是值传递(按值),如果需要双向绑定,要用 ${num}`).onClick(() => { num++ // 不会触发父组件更新! }) } // ✅ 引用传递:使用
: { num: number }){ Button(`{this.count}`) } }}// 通用卡片容器(子组件)@Componentstruct GenericCard { @BuilderParam content: () => void // 声明插槽 @BuilderParam defaultContent() { // 默认内容 Text('暂无内容').fontColor('#999999') } build() { Column() { // 渲染外部传入的 UI this.content() } .width('100%') .padding(16) .backgroundColor('#FFFFFF') .borderRadius(12) .shadow({ radius: 8, color: '#1A000000', offsetX: 0, offsetY: 2 }) }}// 父组件使用@Entry@Componentstruct SlotDemo { build() { Column({ space: 16 }) { // 使用尾随闭包语法传入内容(单个 @BuilderParam 时可用) GenericCard() { Row({ space: 12 }) { Image({score}分`) .fontSize(13) .fontColor('#666666') .margin({ left: 4 }) }}// ========== 可复用区块组件 ==========@Componentstruct InfoSection { title: string = '' @BuilderParam content: () => void // 内容插槽 build() { Column() { // 区块标题 Row() { Divider() .vertical(true) .height(16) .color('#4A90E2') .strokeWidth(3) .margin({ right: 8 }) Text(this.title) .fontSize(16) .fontWeight(FontWeight.Bold) } .sectionPadding() .backgroundColor('#F8F8F8') // 插槽内容 Column() { this.content() } .sectionPadding() } .width('100%') .backgroundColor('#FFFFFF') .margin({ bottom: 8 }) }}// ========== 页面组装 ==========@Entry@Componentstruct ProductDetailPage { @State product: Record<string, string | number> = { name: '华为 Mate X6 折叠屏手机', price: 8999, originalPrice: 9999, score: 4.8, sales: 12563 } @Builder BasicInfo() { Column({ space: 8 }) { // 标签行 Row({ space: 8 }) { Text('官方正品').tagStyle() Text('7天无理由').tagStyle('#E8F5E9', '#52C41A') Text('48H发货').tagStyle('#E3F2FD', '#1890FF') } // 评分 StarRating(this.product.score as number) // 销量 Text(`已售 r('app.media.product_main')) .width('100%') .aspectRatio(1) // 价格区域 Row({ space: 12 }) { Text(`¥{this.product.originalPrice}`) .fontSize(16) .fontColor('#BBBBBB') .decoration({ type: TextDecorationType.LineThrough }) } .sectionPadding() .backgroundColor('#FFFFFF') .width('100%') // 基础信息区块 InfoSection({ title: '商品信息' }) { this.BasicInfo() } // 规格选择区块 InfoSection({ title: '选择规格' }) { this.Specs() } } } .width('100%') .height('100%') .backgroundColor('#F5F5F5') }}