鸿蒙自定义组件开发:从@Component到@Builder,手把手教你封装可复用的UI组件
本文是鸿蒙NEXT开发实战系列第33篇,重点讲解ArkUI自定义组件的开发与应用。文章从基础概念入手,详细介绍了@Component装饰器、@Builder函数和组件通信机制,并通过三个实战案例展示如何封装可复用的UI组件。 主要内容包括: @Component装饰器详解:组件生命周期、状态管理 @Builder函数用法:全局与组件内Builder的区别 组件通信机制:@Prop单向数据流、@Li
📖 鸿蒙NEXT开发实战系列 | 第33篇 | 实战篇 🎯 适合人群:有ArkUI基础的开发者 ⏰ 阅读时间:约15分钟 | 💻 开发环境:DevEco Studio 5.0+
上一篇:32-ArkUI动画效果进阶 | 下一篇:34-自定义组件与页面路由
前言
在日常的鸿蒙应用开发中,你是否经常遇到这样的情况:同一个按钮样式在多个页面重复出现,卡片布局的代码被复制粘贴了十几次,弹窗组件的逻辑散落在各个页面中。这些重复的代码不仅增加了维护成本,还容易导致样式不一致的问题。
自定义组件就是解决这些问题的利器。通过将UI和逻辑封装成可复用的组件,我们可以:
-
减少代码重复,提高开发效率
-
统一组件样式,保持界面一致性
-
降低耦合度,便于维护和扩展
-
实现组件级别的状态管理
本文将从最基础的@Component装饰器开始,逐步讲解@Builder函数、组件通信机制,最后通过3个实战案例手把手教你如何封装可复用的UI组件。
目录
一、@Component装饰器详解
1.1 什么是@Component
@Component是ArkUI中用于定义自定义组件的装饰器。被@Component装饰的struct会成为一个可复用的UI组件,拥有自己的状态管理和生命周期。
1.2 基础语法
@Component
struct MyComponent {
// 组件内部状态
@State message: string = 'Hello';
// 组件生命周期
aboutToAppear() {
console.log('组件即将出现');
}
aboutToDisappear() {
console.log('组件即将消失');
}
// 必须包含build方法,描述组件的UI结构
build() {
Column() {
Text(this.message)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding(16)
}
}
1.3 组件的生命周期
|
生命周期 |
说明 |
常见用途 |
|---|---|---|
|
|
组件即将出现时调用 |
初始化数据、订阅事件 |
|
|
组件即将销毁时调用 |
清理资源、取消订阅 |
|
|
组件build完成后调用 |
获取组件尺寸等 |
1.4 组件的使用
定义好的组件可以直接在其他组件的build()方法中使用:
@Entry
@Component
struct Index {
build() {
Column() {
// 使用自定义组件
MyComponent()
MyComponent() // 可以多次复用
}
}
}
二、@Builder函数用法
2.1 什么是@Builder
@Builder是ArkUI提供的轻量级UI复用机制,它可以将一段UI描述封装成一个函数,在需要的地方调用。与@Component不同的是,@Builder函数不创建新的组件实例,而是直接嵌入到当前组件的UI树中。
2.2 全局Builder函数
使用@Builder装饰的全局函数,可以在任何组件中调用:
// 定义全局Builder函数
@Builder function IconText(icon: Resource, text: string) {
Row({ space: 8 }) {
Image(icon)
.width(24)
.height(24)
Text(text)
.fontSize(16)
.fontColor('#333333')
}
}
// 使用全局Builder函数
@Entry
@Component
struct Index {
build() {
Column() {
IconText($r('app.media.ic_home'), '首页')
IconText($r('app.media.ic_setting'), '设置')
}
}
}
2.3 组件内Builder函数
在组件内部定义的Builder函数,可以访问组件的状态:
@Component
struct UserCard {
@State userName: string = '张三'
@State userAge: number = 25
// 组件内Builder函数,可以访问组件状态
@Builder UserInfoRow(label: string, value: string) {
Row() {
Text(label)
.fontSize(14)
.fontColor('#666666')
.width(80)
Text(value)
.fontSize(14)
.fontColor('#333333')
}
.padding(8)
}
build() {
Column() {
this.UserInfoRow('姓名', this.userName)
this.UserInfoRow('年龄', this.userAge.toString())
}
}
}
2.4 @Builder与@Component的区别
|
特性 |
@Component |
@Builder |
|---|---|---|
|
是否创建新组件实例 |
是 |
否 |
|
是否有独立状态 |
是 |
否(共享调用者状态) |
|
是否有生命周期 |
是 |
否 |
|
适用场景 |
复杂的独立组件 |
轻量级UI复用片段 |
三、组件通信机制
3.1 @Prop - 单向数据流
@Prop装饰的变量允许父组件向子组件传递数据,子组件可以修改@Prop变量,但修改不会同步回父组件。
// 子组件
@Component
struct ChildComponent {
@Prop title: string = ''
build() {
Text(this.title)
.fontSize(18)
}
}
// 父组件
@Entry
@Component
struct ParentComponent {
@State parentTitle: string = '父组件标题'
build() {
Column() {
// 传递数据给子组件
ChildComponent({ title: this.parentTitle })
}
}
}
3.2 @Link - 双向数据绑定
@Link装饰的变量实现父子组件之间的双向数据同步:
// 子组件
@Component
struct SwitchComponent {
@Link isOn: boolean
build() {
Toggle({ type: ToggleType.Switch, isOn: this.isOn })
.onChange((isOn: boolean) => {
this.isOn = isOn
})
}
}
// 父组件
@Entry
@Component
struct ParentComponent {
@State switchState: boolean = false
build() {
Column() {
Text(`开关状态: ${this.switchState ? '开' : '关'}`)
// 使用$符号传递引用
SwitchComponent({ isOn: $switchState })
}
}
}
3.3 事件回调
通过回调函数实现子组件向父组件传递数据:
// 子组件
@Component
struct InputComponent {
@State inputText: string = ''
onTextChange: (text: string) => void = () => {}
build() {
TextInput({ text: this.inputText })
.onChange((value: string) => {
this.inputText = value
this.onTextChange(value) // 通知父组件
})
}
}
// 父组件
@Entry
@Component
struct ParentComponent {
@State receivedText: string = ''
build() {
Column() {
Text(`输入内容: ${this.receivedText}`)
InputComponent({
onTextChange: (text: string) => {
this.receivedText = text
}
})
}
}
}
四、样式定制与主题适配
4.1 通过参数定制样式
// 支持多种样式参数的组件
@Component
struct CustomText {
@Prop content: string = ''
@Prop fontSize: number = 16
@Prop fontColor: string = '#333333'
@Prop fontWeight: FontWeight = FontWeight.Normal
build() {
Text(this.content)
.fontSize(this.fontSize)
.fontColor(this.fontColor)
.fontWeight(this.fontWeight)
}
}
// 使用示例
CustomText({ content: '标题', fontSize: 24, fontWeight: FontWeight.Bold })
CustomText({ content: '正文', fontSize: 16, fontColor: '#666666' })
4.2 主题色适配
// 主题管理类
class AppTheme {
static primaryColor: string = '#007DFF'
static backgroundColor: string = '#FFFFFF'
static textColor: string = '#333333'
static secondaryTextColor: string = '#666666'
}
// 使用主题色的组件
@Component
struct ThemedButton {
@Prop text: string = ''
@State isPressed: boolean = false
build() {
Button(this.text)
.backgroundColor(this.isPressed ? '#005FC3' : AppTheme.primaryColor)
.fontColor('#FFFFFF')
.borderRadius(8)
.height(44)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.isPressed = true
} else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
this.isPressed = false
}
})
}
}
五、实战案例一:自定义按钮组件
5.1 需求分析
封装一个功能完整的自定义按钮组件,支持:
-
多种样式类型(主要、次要、文本按钮)
-
自定义尺寸
-
加载状态
-
点击事件回调
5.2 完整代码
// CustomButton.ets
// 按钮类型枚举
enum ButtonType {
Primary, // 主要按钮
Secondary, // 次要按钮
Text // 文本按钮
}
// 按钮尺寸枚举
enum ButtonSize {
Small,
Medium,
Large
}
@Component
export struct CustomButton {
@Prop text: string = ''
@Prop type: ButtonType = ButtonType.Primary
@Prop size: ButtonSize = ButtonSize.Medium
@Prop loading: boolean = false
@Prop disabled: boolean = false
onClick: () => void = () => {}
// 根据类型获取背景色
private getBackgroundColor(): string {
if (this.disabled) return '#CCCCCC'
switch (this.type) {
case ButtonType.Primary: return '#007DFF'
case ButtonType.Secondary: return '#F5F5F5'
case ButtonType.Text: return 'transparent'
}
}
// 根据类型获取文字颜色
private getFontColor(): string {
if (this.disabled) return '#999999'
switch (this.type) {
case ButtonType.Primary: return '#FFFFFF'
case ButtonType.Secondary: return '#007DFF'
case ButtonType.Text: return '#007DFF'
}
}
// 根据尺寸获取高度
private getHeight(): number {
switch (this.size) {
case ButtonSize.Small: return 32
case ButtonSize.Medium: return 44
case ButtonSize.Large: return 56
}
}
// 根据尺寸获取字体大小
private getFontSize(): number {
switch (this.size) {
case ButtonSize.Small: return 12
case ButtonSize.Medium: return 14
case ButtonSize.Large: return 16
}
}
build() {
Button(this.loading ? '加载中...' : this.text)
.width('100%')
.height(this.getHeight())
.fontSize(this.getFontSize())
.backgroundColor(this.getBackgroundColor())
.fontColor(this.getFontColor())
.borderRadius(8)
.enabled(!this.disabled && !this.loading)
.onClick(() => {
if (!this.disabled && !this.loading) {
this.onClick()
}
})
}
}
// 使用示例
@Entry
@Component
struct ButtonDemo {
build() {
Column({ space: 16 }) {
// 主要按钮
CustomButton({
text: '主要按钮',
type: ButtonType.Primary,
onClick: () => {
console.log('点击了主要按钮')
}
})
// 次要按钮
CustomButton({
text: '次要按钮',
type: ButtonType.Secondary
})
// 禁用状态
CustomButton({
text: '禁用按钮',
disabled: true
})
// 加载状态
CustomButton({
text: '提交',
loading: true
})
}
.padding(16)
.width('100%')
}
}
六、实战案例二:自定义卡片组件
6.1 需求分析
封装一个通用的信息卡片组件,支持:
-
标题和副标题
-
图片展示
-
自定义内容插槽
-
点击事件
6.2 完整代码
// CustomCard.ets
@Component
export struct CustomCard {
@Prop title: string = ''
@Prop subtitle: string = ''
@Prop imageUrl: string = ''
@Prop showImage: boolean = true
onCardClick: () => void = () => {}
// 使用@Builder定义内容插槽
@Builder ContentSlot() {
// 默认内容,可被覆盖
Text('卡片内容区域')
.fontSize(14)
.fontColor('#666666')
}
build() {
Column() {
// 图片区域
if (this.showImage && this.imageUrl) {
Image(this.imageUrl)
.width('100%')
.height(180)
.objectFit(ImageFit.Cover)
.borderRadius({ topLeft: 12, topRight: 12 })
}
// 内容区域
Column({ space: 8 }) {
// 标题行
Row() {
Column({ space: 4 }) {
Text(this.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
if (this.subtitle) {
Text(this.subtitle)
.fontSize(14)
.fontColor('#999999')
}
}
.layoutWeight(1)
Image($r('app.media.ic_more'))
.width(24)
.height(24)
.fillColor('#999999')
}
.width('100%')
// 插槽内容
this.ContentSlot()
}
.padding(16)
.width('100%')
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: 8,
color: 'rgba(0, 0, 0, 0.1)',
offsetX: 0,
offsetY: 2
})
.clip(true)
.onClick(() => {
this.onCardClick()
})
}
}
// 使用示例
@Entry
@Component
struct CardDemo {
build() {
Column({ space: 16 }) {
// 基础卡片
CustomCard({
title: '新闻标题',
subtitle: '2024-01-15',
imageUrl: 'https://example.com/image.jpg',
onCardClick: () => {
console.log('点击了卡片')
}
})
// 无图片卡片
CustomCard({
title: '系统通知',
subtitle: '您有3条未读消息',
showImage: false
})
}
.padding(16)
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
6.3 使用Builder函数扩展卡片内容
// 继承并扩展卡片组件
@Component
struct ArticleCard {
@Prop title: string = ''
@Prop summary: string = ''
@Prop author: string = ''
@Prop readCount: number = 0
@Builder ContentSlot() {
Column({ space: 8 }) {
Text(this.summary)
.fontSize(14)
.fontColor('#666666')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row({ space: 16 }) {
Row({ space: 4 }) {
Image($r('app.media.ic_person'))
.width(16)
.height(16)
Text(this.author)
.fontSize(12)
.fontColor('#999999')
}
Row({ space: 4 }) {
Image($r('app.media.ic_eye'))
.width(16)
.height(16)
Text(`${this.readCount} 阅读`)
.fontSize(12)
.fontColor('#999999')
}
}
}
}
build() {
// 复用CustomCard组件
CustomCard({
title: this.title,
showImage: false,
ContentSlot: () => {
this.ContentSlot()
}
})
}
}
七、实战案例三:自定义弹窗组件
7.1 需求分析
封装一个功能完整的弹窗组件,支持:
-
标题和内容
-
自定义按钮
-
遮罩层点击关闭
-
弹出/关闭动画
7.2 完整代码
// CustomDialog.ets
@Component
export struct CustomDialogComponent {
@Prop visible: boolean = false
@Prop title: string = '提示'
@Prop content: string = ''
@Prop showCancel: boolean = true
@Prop confirmText: string = '确定'
@Prop cancelText: string = '取消'
@State dialogScale: number = 0
@State overlayOpacity: number = 0
onConfirm: () => void = () => {}
onCancel: () => void = () => {}
onClose: () => void = () => {}
// 监听visible变化,触发动画
aboutToAppear() {
if (this.visible) {
this.showAnimation()
}
}
// 显示动画
private showAnimation() {
animateTo({
duration: 200,
curve: Curve.EaseOut
}, () => {
this.dialogScale = 1
this.overlayOpacity = 0.5
})
}
// 关闭动画
private hideAnimation() {
animateTo({
duration: 150,
curve: Curve.EaseIn
}, () => {
this.dialogScale = 0
this.overlayOpacity = 0
})
}
build() {
if (this.visible) {
Stack() {
// 遮罩层
Column()
.width('100%')
.height('100%')
.backgroundColor('#000000')
.opacity(this.overlayOpacity)
.onClick(() => {
this.hideAnimation()
this.onClose()
})
// 弹窗主体
Column({ space: 20 }) {
// 标题
Text(this.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
// 内容
Text(this.content)
.fontSize(16)
.fontColor('#666666')
.textAlign(TextAlign.Center)
.padding({ left: 16, right: 16 })
// 分割线
Divider()
.color('#EEEEEE')
// 按钮区域
Row() {
if (this.showCancel) {
// 取消按钮
Text(this.cancelText)
.fontSize(16)
.fontColor('#666666')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.height(48)
.onClick(() => {
this.hideAnimation()
this.onCancel()
})
}
// 确定按钮
Text(this.confirmText)
.fontSize(16)
.fontColor('#007DFF')
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.height(48)
.onClick(() => {
this.hideAnimation()
this.onConfirm()
})
}
.width('100%')
}
.width(280)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding({ top: 24, bottom: 0 })
.scale({ x: this.dialogScale, y: this.dialogScale })
}
.width('100%')
.height('100%')
}
}
}
// 使用示例
@Entry
@Component
struct DialogDemo {
@State showDialog: boolean = false
build() {
Stack() {
Column() {
Button('显示弹窗')
.onClick(() => {
this.showDialog = true
})
}
.width('100%')
.height('100%')
// 弹窗组件
CustomDialogComponent({
visible: this.showDialog,
title: '确认删除',
content: '确定要删除这条记录吗?删除后无法恢复。',
showCancel: true,
confirmText: '确认删除',
cancelText: '再想想',
onConfirm: () => {
console.log('点击了确认')
this.showDialog = false
},
onCancel: () => {
console.log('点击了取消')
this.showDialog = false
},
onClose: () => {
this.showDialog = false
}
})
}
.width('100%')
.height('100%')
}
}
八、总结与最佳实践
8.1 组件封装原则
-
单一职责:每个组件只负责一个功能
-
可配置性:通过Props提供足够的定制能力
-
可复用性:避免硬编码,使用参数化设计
-
类型安全:使用枚举定义固定的选项值
8.2 性能优化建议
-
合理使用
@State和@Prop,避免不必要的重渲染 -
对于复杂组件,使用
@Builder函数拆分UI片段 -
避免在
build()方法中执行耗时操作 -
使用
@Watch监听状态变化时注意防抖
8.3 何时使用@Component vs @Builder
|
场景 |
推荐方案 |
|---|---|
|
需要独立状态管理 |
@Component |
|
复杂的生命周期逻辑 |
@Component |
|
简单的UI片段复用 |
@Builder |
|
需要访问父组件状态 |
@Builder |
|
需要多次实例化 |
@Component |
8.4 项目组织建议
src/main/ets/
├── components/ # 公共组件目录
│ ├── CustomButton.ets
│ ├── CustomCard.ets
│ ├── CustomDialog.ets
│ └── index.ets # 统一导出
├── builders/ # Builder函数目录
│ └── CommonBuilders.ets
├── pages/ # 页面目录
└── resources/ # 资源文件
系列文章推荐
参考资料
标签:自定义组件 @Component @Builder 鸿蒙开发 组件封装 ArkUI HarmonyOS NEXT
本文为鸿蒙NEXT开发实战系列第33篇,持续更新中。如有问题或建议,欢迎在评论区留言交流!
更多推荐



所有评论(0)