前言
刚开始学 ArkUI 的时候,很多人喜欢把所有 UI 代码堆在一个build()方法里——反正能跑就行。但随着页面越来越复杂,同样的卡片结构复制了三次、四次,改一处还要同步改三处,维护起来非常痛苦。
解决这个问题的方法就是自定义组件。ArkUI 允许你把任何一段可复用的 UI 抽成独立的@Component,通过@Prop传入配置参数,就像搭积木一样在页面里随意复用。本文以两个实用的自定义组件——统计数据卡片(StatCard)和快捷操作按钮(ActionButton)——为例,完整演示组件封装的思路和写法。
为什么需要自定义组件?
想象一个仪表盘首页,需要展示 4 个统计数据:总用户、活跃度、转化率、满意度,每个数据的展示结构完全一致(标题、数值、单位、趋势),只是内容不同。
如果不抽组件,代码会是这样(伪代码):
// 不好的写法:重复代码Column(){Text('总用户');Row(){Text('12.8');Text('万')};Text('+12.5%')}Column(){Text('活跃度');Row(){Text('86.5');Text('%')};Text('+3.2%')}Column(){Text('转化率');Row(){Text('5.2');Text('%')};Text('-0.8%')}Column(){Text('满意度');Row(){Text('4.8');Text('分')};Text('+0.3')}四段几乎一模一样的代码,改个颜色要改四处,这就是"代码臭味"。
有了自定义组件,整个页面变成:
StatCard({title:'总用户',value:'12.8',unit:'万',color:'#4D96FF',trend:'+12.5%'})StatCard({title:'活跃度',value:'86.5',unit:'%',color:'#6BCB77',trend:'+3.2%'})清晰、简洁、好维护。
StatCard:统计数据卡片组件
组件定义
@Componentstruct StatCard{@Proptitle:string=''@Propvalue:string=''@Propunit:string=''@Propcolor:string='#4D96FF'@Proptrend:string=''build(){Column(){Text(this.title).fontSize(12).fontColor('#888888')Row(){Text(this.value).fontSize(24).fontWeight(FontWeight.Bold).fontColor(this.color)Text(this.unit).fontSize(12).fontColor('#888888').margin({left:2,top:8})}.margin({top:4})if(this.trend){Text(this.trend).fontSize(11).fontColor(this.trend.startsWith('+')?'#6BCB77':'#FF6B6B').margin({top:2})}}.layoutWeight(1).height(80).backgroundColor('#F8F9FA').borderRadius(10).padding(12).justifyContent(FlexAlign.Center)}}@Prop的作用:@Prop修饰的变量是从父组件传入的属性,父组件改变传入值时,子组件会自动更新。注意@Prop是单向数据流——父组件可以更新子组件,但子组件修改@Prop变量不会反向影响父组件。
趋势颜色的条件判断:
Text(this.trend).fontSize(11).fontColor(this.trend.startsWith('+')?'#6BCB77':'#FF6B6B')用startsWith('+')判断趋势是正增长还是负增长,正增长显示绿色#6BCB77,负增长显示红色#FF6B6B。这是一个简洁但实用的小技巧:通过字符串前缀判断业务含义。
if (this.trend)条件渲染:如果没有传入trend属性(默认是空字符串),这行就不渲染。避免页面上出现多余的空行。
使用方式
Row({space:8}){StatCard({title:'总用户',value:'12.8',unit:'万',color:'#4D96FF',trend:'+12.5%'})StatCard({title:'活跃度',value:'86.5',unit:'%',color:'#6BCB77',trend:'+3.2%'})}.width('100%')Row({space:8}){StatCard({title:'转化率',value:'5.2',unit:'%',color:'#FFA500',trend:'-0.8%'})StatCard({title:'满意度',value:'4.8',unit:'分',color:'#9B59B6',trend:'+0.3'})}.width('100%').margin({top:8})两个Row,每个Row放两张卡片,space: 8控制间距,layoutWeight(1)让每张卡片平分行内空间。四张卡片,代码非常清晰。
ActionButton:快捷操作按钮组件
@Componentstruct ActionButton{@Proplabel:string=''@Propcolor:string='#4D96FF'@Propicon:string=''build(){Column(){Text(this.icon).fontSize(28)Text(this.label).fontSize(12).fontColor('#888888').margin({top:4})}.width(70).height(70).backgroundColor('#F8F9FA').borderRadius(12).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)}}ActionButton是一个图标 + 文字标签的方形按钮,常见于应用首页的快捷入口区域(就像微信首页的"扫一扫"、"付款码"那种布局)。
用 emoji 作为图标是一种轻量级的做法,不需要引入图标库,开发效率高:
Row({space:12}){ActionButton({icon:'📊',label:'统计'})ActionButton({icon:'📝',label:'笔记'})ActionButton({icon:'📷',label:'拍照'})ActionButton({icon:'⚙️',label:'设置'})}.width('100%').justifyContent(FlexAlign.SpaceEvenly)四个按钮,FlexAlign.SpaceEvenly让它们均匀分布,一行代码搞定对齐。
完整代码
@Componentstruct StatCard{@Proptitle:string=''@Propvalue:string=''@Propunit:string=''@Propcolor:string='#4D96FF'@Proptrend:string=''build(){Column(){Text(this.title).fontSize(12).fontColor('#888888')Row(){Text(this.value).fontSize(24).fontWeight(FontWeight.Bold).fontColor(this.color)Text(this.unit).fontSize(12).fontColor('#888888').margin({left:2,top:8})}.margin({top:4})if(this.trend){Text(this.trend).fontSize(11).fontColor(this.trend.startsWith('+')?'#6BCB77':'#FF6B6B').margin({top:2})}}.layoutWeight(1).height(80).backgroundColor('#F8F9FA').borderRadius(10).padding(12).justifyContent(FlexAlign.Center)}}@Componentstruct ActionButton{@Proplabel:string=''@Propcolor:string='#4D96FF'@Propicon:string=''build(){Column(){Text(this.icon).fontSize(28)Text(this.label).fontSize(12).fontColor('#888888').margin({top:4})}.width(70).height(70).backgroundColor('#F8F9FA').borderRadius(12).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)}}@Entry@Componentstruct Demo{@StateisShow:boolean=truebuild(){Column(){if(this.isShow){Scroll(){Column(){Text('自定义组件').fontSize(18).fontWeight(FontWeight.Bold).margin({bottom:8})Column(){Text('统计数据卡片 (StatCard)').fontSize(14).fontWeight(FontWeight.Medium).margin({bottom:8})Row({space:8}){StatCard({title:'总用户',value:'12.8',unit:'万',color:'#4D96FF',trend:'+12.5%'})StatCard({title:'活跃度',value:'86.5',unit:'%',color:'#6BCB77',trend:'+3.2%'})}.width('100%')Row({space:8}){StatCard({title:'转化率',value:'5.2',unit:'%',color:'#FFA500',trend:'-0.8%'})StatCard({title:'满意度',value:'4.8',unit:'分',color:'#9B59B6',trend:'+0.3'})}.width('100%').margin({top:8})}.width('100%').backgroundColor('#FFFFFF').borderRadius(12).padding(16)Column(){Text('快捷操作 (ActionButton)').fontSize(14).fontWeight(FontWeight.Medium).margin({bottom:8}).margin({top:4})Row({space:12}){ActionButton({icon:'📊',label:'统计'})ActionButton({icon:'📝',label:'笔记'})ActionButton({icon:'📷',label:'拍照'})ActionButton({icon:'⚙️',label:'设置'})}.width('100%').justifyContent(FlexAlign.SpaceEvenly)}.width('100%').backgroundColor('#FFFFFF').borderRadius(12).padding(16).margin({top:10})}.width('100%')}.layoutWeight(1)}}.width('100%').height('100%').backgroundColor('#F5F6FA').padding(16)}}@Propvs@State:用哪个?
| 场景 | 使用 |
|---|---|
| 数据在本组件内部产生和修改 | @State |
| 数据从父组件传入,子组件只读不改 | @Prop |
| 父子双向同步,子组件也要改 | @Link |
StatCard和ActionButton里的数据都是从外部传入的配置,子组件不需要修改,所以用@Prop是正确的选择。
自定义组件的设计原则
设计一个好的自定义组件,有几个值得遵守的原则:
单一职责:一个组件只做一件事。StatCard只负责展示一个统计数据,不掺杂网络请求或业务逻辑。
通过@Prop暴露接口:所有外部可配置的内容都通过@Prop暴露,外部不能访问组件内部状态。
提供合理的默认值:每个@Prop属性都给了默认值(空字符串或默认颜色),这样即使调用者忘记传某个参数,组件也不会崩溃。
保持样式的内聚性:组件自己管好自己的内部样式(圆角、背景色、内边距),外部只控制内容和颜色,不要让外部来设置组件内部布局。
总结
自定义组件是 ArkUI 开发中的核心能力之一,它让代码从"面条式堆叠"变成"积木式组合"。通过@Component定义组件结构,@Prop暴露配置接口,父组件只需要传入参数就能复用完整的 UI 片段,页面代码的可读性和可维护性大幅提升。
本文展示的StatCard和ActionButton是两个非常典型的自定义组件案例,覆盖了带条件渲染、多属性配置、颜色动态绑定等实际开发中常见的需求。掌握这种组件封装思路后,面对任何复杂页面,你都能先识别出可复用的 UI 单元,把它们抽成独立组件,再用"积木"的方式组合出整个页面。
ArkUI 的声明式 UI + 自定义组件,是提升鸿蒙开发效率最重要的两个工具,二者相辅相成,学好了可以覆盖 90% 以上的日常开发场景。