写ArkUI代码最烦的就是重复——卡片样式、按钮样式、列表项模板,复制粘贴一多维护就崩。HarmonyOS NEXT给了三种复用机制:@Styles管属性、@Builder管组件、@Extend管扩展,各有各的适用场景和限制。

@Styles — 属性级复用

@Styles把一组通用属性抽取出来,当成一个属性方法调用:

@Styles
fancyCard(): void {
  .width('100%')
  .padding(16)
  .borderRadius(12)
  .backgroundColor('#ffffff')
  .shadow({ radius: 4, color: '#1a000000', offsetY: 2 })
  .margin({ bottom: 12 })
}

// 使用
Column() {
  Text('内容')
}
.fancyCard()

在这里插入图片描述

核心限制

  1. 只能用通用属性 — .padding、.borderRadius、.backgroundColor、.width、.margin、.shadow这些属于CommonAttribute的属性可以用。组件特有属性(如Text的.fontSize、.fontColor)不能出现在@Styles里。

  2. 返回void,不能链式调用 — .fancyCard()返回void,后面不能再接.width()或.margin()。所有需要的属性都要写在@Styles定义内部。

  3. 可以定义在struct内或全局 — struct内定义的@Styles只能在该组件使用;全局定义的可以被所有组件共享。

  4. 支持无参和带参数 — 带参数版本可以实现动态样式:

@Styles
cardWithRadius(radius: number): void {
  .borderRadius(radius)
  .padding(16)
  .backgroundColor('#ffffff')
}

// 使用
Column() { ... }.cardWithRadius(8)
Column() { ... }.cardWithRadius(16)

常见错误

// 错误1: 在@Styles里用组件特有属性
@Styles
titleText(): void {
  .fontSize(16)        // Text特有属性,不属于CommonAttribute
  .fontWeight(FontWeight.Bold)
}

// 错误2: 在@Styles后链式调用
Column() { ... }
  .fancyCard()
  .width('100%')       // 编译报错:Property 'width' does not exist on type 'void'

// 正确: width写在@Styles内部
@Styles
fancyCard(): void {
  .width('100%')
  .padding(16)
  .borderRadius(12)
  .backgroundColor('#ffffff')
}

@Builder — 组件级复用

@Builder能封装完整的UI片段,包含子组件和布局,这是@Styles做不到的:

@Builder
InfoRow(label: string, value: string): void {
  Row() {
    Text(label)
      .fontSize(13)
      .fontColor('#888888')
      .width(80)
    Text(value)
      .fontSize(13)
      .fontColor('#333333')
      .layoutWeight(1)
  }
  .width('100%')
  .padding({ top: 6, bottom: 6 })
}

// 使用
Column() {
  this.InfoRow('名称', 'HarmonyOS')
  this.InfoRow('版本', 'API 12')
  this.InfoRow('语言', 'ArkTS')
}

在这里插入图片描述

按值传递 vs 按引用传递

@Builder的参数传递有两种方式,行为不同:

按值传递(基本类型参数):值在调用时拷贝,后续修改不影响已渲染的UI:

@Builder
PresetButton(preset: StylePreset, index: number): void {
  Button(preset.name)
    .backgroundColor(this.currentPreset === index ? '#1a73e8' : '#cccccc')
}

this.PresetButton(this.presets[0], 0)

按引用传递(对象参数加:前缀):传递引用,UI能响应对象变化:

@Builder
ItemBuilder(item: Item): void {
  // item是引用,变化时会更新
  Text(item.name)
}

this.ItemBuilder({ name: 'test' })  // 传对象字面量

注意:按引用传递的对象必须是class/interface类型,不能用基本类型。

全局@Builder vs 成员@Builder

  • 成员@Builder — 定义在struct内,用this.builderName()调用,可以访问组件的@State变量
  • 全局@Builder — 定义在struct外,用builderName()调用,不能访问组件状态

成员@Builder能直接用this访问状态,这是最大的优势:

@Component
struct MyPage {
  @State isActive: boolean = false;

  @Builder
  StatusBadge(): void {
    Row() {
      Text(this.isActive ? '激活' : '未激活')  // 直接用this
        .fontColor(this.isActive ? '#43a047' : '#e53935')
    }
  }
}

@Extend — 扩展原生组件

@Extend给原生组件添加自定义属性方法:

@Extend(Text)
function titleText(size: number): void {
  .fontSize(size)
  .fontWeight(FontWeight.Bold)
  .fontColor('#333333')
}

// 使用
Text('标题').titleText(20)
Text('副标题').titleText(16)

核心限制

  1. 只能在全局作用域定义 — 不能写在struct内部,这是跟@Styles/@Builder最大的区别
  2. 只能扩展指定组件@Extend(Text)只能给Text组件添加方法,不能给Column用
  3. 只支持属性方法 — 跟@Styles类似,只能加属性链,不能加子组件
  4. 支持参数 — 可以传参实现动态样式
// 按钮变体
@Extend(Button)
function primaryButton(): void {
  .backgroundColor('#1a73e8')
  .fontColor('#ffffff')
  .borderRadius(8)
  .fontSize(14)
}

@Extend(Button)
function dangerButton(): void {
  .backgroundColor('#e53935')
  .fontColor('#ffffff')
  .borderRadius(8)
  .fontSize(14)
}

Button('确认').primaryButton()
Button('删除').dangerButton()

在这里插入图片描述

三种机制对比

特性 @Styles @Builder @Extend
定义位置 struct内或全局 struct内或全局 仅全局
包含子组件 不可以 可以 不可以
支持参数 支持 支持 支持
适用范围 所有组件 所有组件 指定组件
返回值 void void void
调用方式 .styleName() this.builderName() .extendName()
属性链式 定义内部链式 自由 定义内部链式

实际选型指南

卡片样式统一 → @Styles

@Styles
cardStyle(): void {
  .width('100%')
  .padding(16)
  .borderRadius(12)
  .backgroundColor('#ffffff')
  .shadow({ radius: 4, color: '#1a000000', offsetY: 2 })
}

列表项模板 → @Builder

@Builder
ListItemTemplate(title: string, desc: string, tag: string): void {
  Row() {
    Text(title).fontSize(14).layoutWeight(1)
    Text(tag).fontSize(10).fontColor('#1a73e8')
  }
  .width('100%').padding(12).borderRadius(8).backgroundColor('#ffffff')
}

按钮风格变体 → @Extend

@Extend(Button)
function btnPrimary(): void {
  .backgroundColor('#1a73e8').fontColor('#ffffff').borderRadius(6)
}

组合使用

实际项目中三种机制经常组合使用:

@Styles
cardBase(): void {
  .width('100%')
  .padding(16)
  .borderRadius(12)
  .backgroundColor('#ffffff')
}

@Builder
InfoCard(title: string, items: string[]): void {
  Column() {
    Text(title)
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .fontColor('#333333')
    ForEach(items, (item: string) => {
      this.InfoRow(item.substring(0, item.indexOf(':')), item.substring(item.indexOf(':') + 1))
    }, (item: string, index: number) => index.toString())
  }
  .cardBase()    // @Styles + @Builder组合
}

@Builder里调用@Styles完全没问题——@Styles是属性方法,@Builder是组件方法,它们在不同层级工作。

踩坑清单

问题 原因 解决
@Styles里用.fontSize报错 fontSize不是CommonAttribute 改用@Builder
@Styles后链式.width报错 @Styles返回void 把属性移入@Styles定义
@Extend写在struct内报错 @Extend只能全局定义 移到struct外面
@Builder里ForEach不刷新 key函数返回值重复 确保key唯一
全局@Builder访问不到this 全局没有组件上下文 改用成员@Builder
@Styles参数不生效 参数类型不支持 检查参数类型

样式复用的核心原则:能用@Styles解决的不用@Builder,能用@Builder解决的不用@Extend。@Styles最轻量,@Builder最灵活,@Extend最精确。三者各有领地,别跨着用——用@Builder做属性复用是大材小用,用@Styles做组件模板是做不到。

Logo

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

更多推荐