问题描述

页面中有很多重复的 UI 结构(导航栏、列表项、卡片等),如何使用 @Builder 实现组件复用,减少代码重复?

关键字: @Builder、组件复用、自定义组件、代码复用、UI 封装

解决方案

完整代码

/**
 * 全局Builder函数
 */
@Builder
function buildHeader(title: string, showBack: boolean = true, onBack?: () => void) {
  Row() {
    if (showBack) {
      Image($r('app.media.ic_back'))
        .width(24)
        .height(24)
        .onClick(() => {
          if (onBack) {
            onBack();
          } else {
            router.back();
          }
        })
    }
    
    Text(title)
      .fontSize(20)
      .fontWeight(FontWeight.Bold)
      .layoutWeight(1)
      .textAlign(TextAlign.Center)
    
    // 占位,保持标题居中
    if (showBack) {
      Row().width(24).height(24)
    }
  }
  .width('100%')
  .height(56)
  .padding({ left: 16, right: 16 })
  .backgroundColor(Color.White)
}
​
/**
 * 组件内Builder函数
 */
@Entry
@Component
struct BuilderDemo {
  @State records: Array<{name: string, amount: number, type: string}> = [
    { name: '张三', amount: 500, type: 'income' },
    { name: '李四', amount: 300, type: 'expense' }
  ];
  
  build() {
    Column() {
      // 使用全局Builder
      buildHeader('记录列表', true)
      
      List({ space: 12 }) {
        ForEach(this.records, (record: any) => {
          ListItem() {
            // 使用组件内Builder
            this.buildRecordItem(record)
          }
        })
      }
      .layoutWeight(1)
      .padding(16)
    }
  }
  
  /**
   * 组件内Builder:记录列表项
   */
  @Builder
  buildRecordItem(record: {name: string, amount: number, type: string}) {
    Row() {
      Column({ space: 4 }) {
        Text(record.name)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
        Text(record.type === 'income' ? '收入' : '支出')
          .fontSize(14)
          .fontColor('#999')
      }
      .alignItems(HorizontalAlign.Start)
      
      Blank()
      
      Text(`¥${record.amount}`)
        .fontSize(18)
        .fontColor(record.type === 'income' ? '#f56c6c' : '#67c23a')
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(8)
  }
  
  /**
   * 组件内Builder:空状态
   */
  @Builder
  buildEmptyState(message: string = '暂无数据') {
    Column({ space: 12 }) {
      Image($r('app.media.ic_empty'))
        .width(120)
        .height(120)
        .opacity(0.3)
      
      Text(message)
        .fontSize(14)
        .fontColor('#999')
    }
    .width('100%')
    .padding(40)
  }
  
  /**
   * 组件内Builder:统计卡片
   */
  @Builder
  buildStatCard(title: string, value: string, color: string) {
    Column({ space: 8 }) {
      Text(value)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor(color)
      Text(title)
        .fontSize(14)
        .fontColor('#999')
    }
    .width('100%')
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }
}
​
/**
 * BuilderParam传递Builder
 */
@Component
struct CustomCard {
  @BuilderParam content: () => void;
  title: string = '';
  
  build() {
    Column({ space: 12 }) {
      // 标题
      Text(this.title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .width('100%')
      
      // 自定义内容
      this.content()
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }
}
​
/**
 * 使用BuilderParam
 */
@Entry
@Component
struct BuilderParamDemo {
  build() {
    Column({ space: 12 }) {
      CustomCard({ title: '统计信息' }) {
        Column({ space: 8 }) {
          Text('总收入: ¥5000')
          Text('总支出: ¥3000')
        }
      }
      
      CustomCard({ title: '最近记录' }) {
        List({ space: 8 }) {
          ListItem() {
            Text('记录1')
          }
          ListItem() {
            Text('记录2')
          }
        }
        .height(100)
      }
    }
    .padding(16)
  }
}
​
/**
 * @Extend扩展组件样式
 */
@Extend(Text)
function primaryButton() {
  .fontSize(16)
  .fontColor(Color.White)
  .backgroundColor('#ff6b6b')
  .padding({ left: 24, right: 24, top: 12, bottom: 12 })
  .borderRadius(8)
}
​
@Extend(Text)
function secondaryButton() {
  .fontSize(16)
  .fontColor('#333')
  .backgroundColor('#f5f5f5')
  .padding({ left: 24, right: 24, top: 12, bottom: 12 })
  .borderRadius(8)
}
​
@Component
struct ExtendDemo {
  build() {
    Column({ space: 12 }) {
      Text('确定').primaryButton()
      Text('取消').secondaryButton()
    }
  }
}

原理解析

1. @Builder 全局函数

@Builder
function buildHeader(title: string) {
  // UI代码
}
  • 定义在组件外,可被多个组件使用
  • 支持参数传递
  • 不能访问组件状态

2. @Builder 组件内方法

@Builder
buildRecordItem(record: any) {
  // UI代码
}
  • 定义在组件内,可访问组件状态
  • 使用 this.buildRecordItem()调用
  • 支持参数传递

3. @BuilderParam

@BuilderParam content: () => void;
  • 接收 Builder 作为参数
  • 实现插槽功能
  • 提高组件灵活性

4. @Extend 样式扩展

@Extend(Text) function primaryButton() {}
  • 扩展组件样式
  • 避免重复样式代码
  • 只能扩展系统组件

最佳实践

  1. 全局 Builder: 通用 UI(导航栏、空状态)用全局 Builder
  2. 组件 Builder: 页面特定 UI 用组件内 Builder
  3. BuilderParam: 需要自定义内容的组件用 BuilderParam
  4. Extend: 重复样式用 @Extend
  5. 命名规范: Builder 函数以 build 开头

避坑指南

  1. this 访问: 全局 Builder 不能访问 this
  2. 参数类型: Builder 参数要明确类型
  3. 状态更新: Builder 内修改状态会触发重绘
  4. 性能: 避免在 Builder 内执行复杂计算
  5. Extend 限制: @Extend 只能用于系统组件

效果展示

  • 代码复用率提升 60% 以上
  • 统一的 UI 风格
  • 易于维护和修改
  • 提高开发效率
Logo

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

更多推荐