ArkUI 自定义组件与 Modifier:把界面拆成可复用的单元

为什么需要自定义组件

页面一复杂,build() 里的代码就越堆越长。同一个 UI 片段(比如商品卡片、用户头像、评分条)在多处重复,每次都要复制粘贴。自定义组件就是解决"复用"和"拆分"问题的——把一组组件封装成一个独立单元,一次定义多次使用。

ArkUI 的自定义能力分层,从易到难:

自定义组合        ← 最常用,把系统组件拼成新组件
    ↑
自定义扩展(Modifier) ← 给组件扩展可复用属性
    ↑
自定义节点        ← FrameNode/RenderNode/BuilderNode,操作底层
    ↑
自定义渲染        ← 最底层,完全接管绘制(XComponent/Canvas/NDK)

自定义组合:@Component

最基础的封装用 @Component + build()

// 定义一个可复用的"评分条"组件
@Component
struct RatingBar {
  @Prop score: number;  // 评分,0-5
  @Prop maxScore: number = 5;

  build() {
    Row() {
      ForEach(Array.from({ length: this.maxScore }, (_, i) => i + 1), (star: number) => {
        // 小于等于 score 的星是实心的
        Text(star <= this.score ? '★' : '☆')
          .fontSize(20)
          .fontColor(star <= this.score ? '#FFB400' : '#CCCCCC')
      }, (star: number) => star.toString())
    }
  }
}

// 使用
@Entry
@Component
struct UseRating {
  build() {
    Column({ space: 16 }) {
      RatingBar({ score: 4 })  // 4 星
      RatingBar({ score: 5, maxScore: 10 })  // 5 星(满分 10)
    }
  }
}

一次定义 RatingBar,多处使用,通过 @Prop 传入不同参数。这是自定义组合的核心价值。

自定义组件的参数传递

自定义组件像函数一样接收参数。参数在组件内用 @Prop/@Link 声明,调用时传入:

// 组件定义:声明需要的参数
@Component
struct UserCard {
  @Prop name: string;      // 用户名
  @Prop avatar: Resource;  // 头像资源
  @Link following: boolean; // 是否关注(双向)

  build() {
    Row() {
      Image(this.avatar)
        .width(48).height(48).borderRadius(24)
      Column() {
        Text(this.name).fontSize(16)
      }
      .margin({ left: 12 })
      Blank()
      Button(this.following ? '已关注' : '关注')
        .onClick(() => {
          this.following = !this.following;  // 双向修改
        })
    }
    .padding(12)
  }
}

// 使用
@Entry
@Component
struct UseUserCard {
  @State followed: boolean = false;

  build() {
    Column() {
      UserCard({
        name: '张三',
        avatar: $r('app.media.avatar1'),
        following: $followed  // 注意 $ 前缀(@Link 双向)
      })
    }
  }
}

参数分两类:@Prop 传值(不加 $),@Link 传引用(加 $)。

@Builder 构建函数

@Builder 用于提取可复用的 UI 构建代码,比自定义组件更轻量(不涉及独立状态管理):

@Entry
@Component
struct BuilderDemo {
  build() {
    Column({ space: 16 }) {
      // 复用构建函数
      this.sectionTitle('最新消息')
      this.sectionTitle('热门推荐')
    }
  }

  // 构建函数:一个带样式的标题
  @Builder
  sectionTitle(title: string) {
    Row() {
      Text(title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
      Text('更多 >')
        .fontSize(12)
        .fontColor('#999999')
    }
    .width('100%')
    .padding({ left: 16, right: 16 })
    .justifyContent(FlexAlign.SpaceBetween)
  }
}

@Builder 适合提取"纯粹的结构 + 样式"片段,不带独立状态。带状态用 @Component,纯复用布局用 @Builder

@BuilderParam:插槽式传参

@BuilderParam 让父组件往子组件"塞"一段自定义 UI,类似 Vue 的 slot:

// 一个带"插槽"的卡片容器
@Component
struct CardShell {
  @BuilderParam content: () => void;  // 插槽内容

  build() {
    Column() {
      this.content()  // 渲染父组件传入的内容
    }
    .padding(16)
    .backgroundColor('#F5F5F5')
    .borderRadius(12)
  }
}

// 使用:传入不同的内容
@Entry
@Component
struct UseCardShell {
  build() {
    Column({ space: 16 }) {
      CardShell({ content: () => {
        Text('这是第一张卡片的内容')
      }})
      CardShell({ content: () => {
        Button('这是第二张卡片的内容')
      }})
    }
  }
}

@BuilderParam 让容器组件(卡片、弹窗、列表项壳)保持通用,具体内容由调用方决定。这是高级复用的关键。

自定义扩展:Modifier

Modifier 是把一组属性封装成可复用单元,然后像 .fontSize() 一样链式调用:

// 定义一个自定义 Modifier:统一的卡片样式
class CardStyleModifier implements AttributeModifier<ColumnAttribute> {
  // 在组件属性应用前或后,注入样式
  applyNormalAttribute(instance: ColumnAttribute): void {
    instance.backgroundColor('#FFFFFF');
    instance.borderRadius(12);
    instance.padding(16);
    // 阴影等
  }
}

// 使用
@Entry
@Component
struct ModifierDemo {
  build() {
    Column({ space: 16 }) {
      Column() {
        Text('卡片一')
      }
      .attributeModifier(new CardStyleModifier())  // 应用自定义 Modifier

      Column() {
        Text('卡片二')
      }
      .attributeModifier(new CardStyleModifier())  // 复用
    }
    .padding(16)
  }
}

Modifier 的价值是跨组件的样式复用——一套样式定义,作用在不同组件上。适合统一设计规范的场景(统一的卡片、统一的按钮、统一的间距)。

@Component 复用的是"结构和逻辑",Modifier 复用的是"样式属性",两者可以叠加使用。

组合式复用的小案例

做一个商品列表,综合用自定义组件、@Builder、Modifier:

// 商品卡片(复用结构 + 逻辑)
@Component
struct GoodsCard {
  @Prop name: string;
  @Prop price: number;
  @Prop image: Resource;

  build() {
    Row() {
      Image(this.image).width(80).height(80).borderRadius(8)
      Column() {
        Text(this.name).fontSize(16)
        Text(`¥${this.price}`).fontSize(18).fontColor('#FF6B35')
      }
      .alignItems(HorizontalAlign.Start)
      .margin({ left: 12 })
      Blank()
      Button('加入购物车').height(32).type(ButtonType.Capsule)
    }
    .padding(12)
  }
}

@Entry
@Component
struct GoodsListPage {
  build() {
    Column() {
      // 用 @Builder 复用标题
      this.titleBar('今日推荐')

      // 用 @Component 复用商品卡片
      GoodsCard({ name: '商品A', price: 19.9, image: $r('app.media.goods1') })
      GoodsCard({ name: '商品B', price: 29.9, image: $r('app.media.goods2') })
      GoodsCard({ name: '商品C', price: 39.9, image: $r('app.media.goods3') })
    }
  }

  @Builder
  titleBar(title: string) {
    Text(title).fontSize(20).fontWeight(FontWeight.Bold)
      .width('100%').padding(16)
  }
}

总结一下下

  1. 优先用 @Component。大部分复用场景,@Component 就够了。别一上来就 ModifierFrameNode,复杂但没必要。

  2. 带状态用组件,纯布局用 Builder。需要独立状态管理(有 @State@Prop)用 @Component,只是提取一段布局用 @Builder,更轻量。

  3. @BuilderParam 解决"容器不确定内容"。做通用卡片、弹窗、tab 容器时,用 @BuilderParam 留插槽,比在组件内部写死内容更灵活。

  4. 组件别太大也别太小。一个组件几百行说明该继续拆,一个组件只包一层 Text 说明拆得太碎,都不利于维护。

  5. 参数命名和类型要清晰。自定义组件的 @Prop 参数是组件的"接口",起有意义的名字、明确类型,别的开发者才能正确使用。

Logo

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

更多推荐