【HarmonyOS实战】 @Builder构建函数:UI复用的正确姿势
文章目录
- 前言
- 一、@Builder 是什么?
- 二、@Builder vs @Component
- 三、项目中的三个 @Builder
- 3.1 titleBuilder:标题栏
- 3.2 bindBuilder:底部弹窗内容
- 3.3 stationInfoCard:加油站卡片(带参数)
- 四、全局 @Builder
- 五、@Builder 的限制
- 5.1 不能有独立的 @State
- 5.2 参数传递限制
- 六、什么时候用 @Builder,什么时候用 @Component?
- 七、实际建议:如何拆分复杂页面
- 总结
前言
打开GasStationPage.ets,你会发现有三个@Builder装饰的方法:stationInfoCard、bindBuilder、titleBuilder。这些方法都返回 UI,但又不是独立的@Component组件。
@Builder到底是什么?和组件有什么区别?什么时候用@Builder,什么时候用@Component?这篇文章讲清楚这些问题。
项目预览
一、@Builder 是什么?
@Builder是一个轻量级的 UI 复用机制,可以把一段 UI 代码抽取成一个"可调用的 UI 片段"。
@Entry@Componentstruct MyPage{// 定义 @Builder:把一段 UI 抽取出来@BuilderheaderBuilder(){Row(){Text('标题').fontSize(20).fontWeight(700)}.width('100%').height(50)}build(){Column(){this.headerBuilder()// 调用 @Builder,就像调用函数一样// 其他内容...}}}二、@Builder vs @Component
| 特性 | @Builder | @Component |
|---|---|---|
| 定义方式 | 函数(方法) | 结构体(struct) |
| 状态管理 | 共享父组件的状态 | 独立的状态作用域 |
| 参数传递 | 直接函数参数 | 通过 @Prop/@Link 等 |
| 生命周期 | 无独立生命周期 | 有完整生命周期 |
| 复用粒度 | UI 片段,轻量 | 独立组件,重量 |
| 适用场景 | 复杂页面内部拆分 | 跨页面复用的独立组件 |
核心区别:@Builder里的 UI 是父组件的一部分,共享父组件的this(即可以直接访问父组件的状态变量);@Component是独立的组件,有自己的状态作用域。
三、项目中的三个 @Builder
3.1 titleBuilder:标题栏
@BuildertitleBuilder(){Row({space:Constants.SPACE_8}){Image($r('app.media.back')).width(Constants.IMAGE_BACK_WIDTH)// 40.height(Constants.IMAGE_BACK_HEIGHT)// 40.onClick(()=>{this.pageInfos.pop();// 直接访问父组件的 pageInfos!});Text($r('app.string.car_life')).fontWeight(Constants.FONT_WEIGHT_700).fontSize(Constants.FONT_SIZE_20).lineHeight(Constants.LINE_HEIGHT_27).fontColor($r('app.color.gas_station_name_color'));}.width(Constants.FULL_PERCENT).padding({left:Constants.PADDING_RIGHT_16}).position({top:Constants.POSITION_TOP});}注意this.pageInfos.pop()——@Builder里直接访问父组件(GasStationPage)的pageInfos,这是@Component做不到的(子组件需要通过@Prop/@Link接收数据)。
为什么用 @Builder 而不是 @Component?
因为标题栏只在这一个页面用,而且需要直接访问页面的路由栈(pageInfos),用@Builder是最简单的方式。
3.2 bindBuilder:底部弹窗内容
@BuilderbindBuilder(){Column(){Scroll(){if(this.stationInfoList&&this.stationInfoList.length>0){List(){ForEach(this.stationInfoList,(station:StationData)=>{// 直接用 stationInfoListListItem(){Row({space:Constants.SPACE_12}){this.stationInfoCard(station);// 调用另一个 @Builder}.width(Constants.FULL_PERCENT);};},(station:StationData)=>{returnstation.id+station.name;});}.width(Constants.FULL_PERCENT);}}// ...样式}.height(Constants.MY_BUILDER_COLUMN_HEIGHT);}@Builder里可以调用另一个 @Builder:this.stationInfoCard(station)就是在bindBuilder里调用了stationInfoCard。
3.3 stationInfoCard:加油站卡片(带参数)
@BuilderstationInfoCard(gasStation:StationData):void{// 注意:有参数!Column({space:Constants.SPACE_12}){// 卡片内容....onClick(()=>{if(gasStation){this.openOrCloseMap(true);// 访问父组件方法this.moveToGasStation(gasStation.latitude,gasStation.longitude);// 访问父组件方法}else{this.getUIContext().getPromptAction().showToast({message:$r('app.string.Stay_tuned')});}});}}@Builder可以有参数:stationInfoCard接收一个StationData参数,每次调用传入不同的数据,渲染不同的卡片。
调用时:
this.stationInfoCard(station)// 传入当前循环的加油站数据四、全局 @Builder
上面三个都是组件内部的局部 @Builder(作为组件的方法)。@Builder也可以定义在组件外部,成为全局 @Builder:
// 文件顶层定义(全局)@BuilderexportfunctionGasStationPageBuilder(){GasStationPage();}// 可以在任何地方调用GasStationPageBuilder();为什么GasStationPageBuilder是全局 @Builder?
因为它是 Navigation 路由系统的入口。route_map.json里配置了buildFunction: "GasStationPageBuilder",框架需要通过这个全局函数来创建页面,所以必须是全局可访问的(export function)。
全局@Builder和组件方法的区别:
- 全局:不属于任何组件,没有
this,不能访问组件状态 - 局部(组件方法):属于组件,有
this,可以访问组件的属性和方法
五、@Builder 的限制
5.1 不能有独立的 @State
@BuildermyBuilder(){@Statecount:number=0;// ❌ 报错!@Builder 里不能定义 @StateText(`${count}`)}@Builder不能定义自己的状态变量。如果需要状态,要么:
- 用父组件的状态变量(共享状态)
- 把 UI 抽成真正的
@Component
5.2 参数传递限制
// 简单类型参数:按值传递,父组件状态变化不会同步到 Builder 里@BuildermyBuilder(text:string){Text(text)// text 变化不会自动刷新(除非父组件重新调用)}// 对象类型参数:传引用可以观察到变化@BuildermyBuilder(config:{text:string}){Text(config.text)// config 对象里的属性变化可以被观察到}提示:如果
@Builder里的 UI 需要响应父组件某个值的变化,建议直接用this.xxx访问父组件的@State变量,而不是通过参数传递。
六、什么时候用 @Builder,什么时候用 @Component?
用 @Builder 当:
- UI 片段只在当前组件内使用
- 需要直接访问父组件的状态变量
- UI 片段不需要独立的状态和生命周期
- 只是为了让代码更整洁,拆分大
build()函数
用 @Component 当:
- UI 组件需要在多个页面复用
- 组件需要独立的状态管理
- 组件有复杂的生命周期(
aboutToAppear、onWillDisappear等) - 组件需要接受外部的数据绑定(
@Prop、@Link、@ObjectLink)
七、实际建议:如何拆分复杂页面
面对一个复杂页面,推荐这样的拆分策略:
GasStationPage(@Component @Entry) ├── titleBuilder(@Builder) ← 只此页面用,需要访问页面路由 ├── bindBuilder(@Builder) ← 只此页面用,需要访问页面数据 │ └── stationInfoCard(@Builder) ← 复杂卡片,提取为 Builder如果stationInfoCard未来需要在其他地方复用,就把它抽成独立的@Component:
// StationCard.ets@Componentstruct StationCard{@PropgasStation:StationData;onClickCallback:(station:StationData)=>void=()=>{};build(){// 卡片 UI}}总结
@Builder是 ArkUI 中轻量的 UI 复用机制:
- 局部 @Builder(组件方法):共享父组件状态,可有参数,适合拆分同一页面的 UI 片段
- 全局 @Builder(文件顶层函数):无
this,适合路由入口等全局调用场景 - vs @Component:@Builder 轻量但功能有限,@Component 独立但较重
GasStationPage用三个 @Builder 把复杂的build()方法拆分成几个有意义的部分,代码可读性大大提升。
下一篇讲LocationKit 定位服务——geoLocationManager是怎么获取用户位置的,异步获取位置有哪些注意事项。
