鸿蒙应用开发中@Builder与@LocalBuilder有何区别示例演示?

摘要:【学习目标】 理解 @Builder 设计初衷,明确与 @Component 核心差异; 掌握 @Builder 两种定义方式、参数传递(按值按引用)规则; 掌握 @Builder 高级场景(嵌套、this指向)实战用法; 掌握 @Bui
【学习目标】 理解 @Builder 设计初衷,明确与 @Component 核心差异; 掌握 @Builder 两种定义方式、参数传递(按值/按引用)规则; 掌握 @Builder 高级场景(嵌套、this指向)实战用法; 掌握 @BuilderParam UI 插槽原理与使用方法,实现组件 UI 内容灵活定制; 掌握 @LocalBuilder 用法、参数传递与限制,理解其固定this、维持组件父子关系的核心特性; 能够清晰对比 @Builder 与 @LocalBuilder,在实际开发中正确选型。 一、Builder 核心认知 1.1 什么是 @Builder @Builder 用于封装一段UI结构,仅负责UI渲染,无独立实例、无状态、无生命周期,不会加入组件树,编译阶段会直接内联展开,是组件内/全局复用UI片段的最优轻量方案。 1.2 为什么需要 @Builder 我们已经掌握 @Component 封装可复用UI,适用于独立功能、带业务逻辑、带生命周期的页面/组件。 但开发中存在大量纯UI结构、无状态、无业务、无生命周期的复用片段例如 列表内固定骨架图,页面内重复UI区块。 这类片段不需要独立实例、状态、生命周期,使用 @Component 会造成额外实例开销,粒度过细。因此 ArkTS 提供最轻量UI复用方案:@Builder 自定义构建函数,专注细粒度UI复用,几乎无额外性能消耗。 1.3 @Builder 与 @Component 核心区别 特性 @Builder @Component 本质 UI构建函数 独立自定义组件 实例 无实例 有独立组件实例 状态支持 不能声明@State 支持@State、@Prop等状态 生命周期 无生命周期 拥有完整生命周期 调用方式 直接调用 作为组件使用 适用场景 纯UI结构复用 功能组件/页面/业务模块 刷新范围 随所属组件整体刷新,无独立刷新 可独立响应式刷新 调用位置 只能在build()内部使用 任意生命周期与场景均可使用 1.4 @Builder 两种定义形式 组件内 @Builder:仅当前组件内使用,可直接访问组件成员变量与状态,跟随组件刷新而刷新; 全局 @Builder:整个工程通用,不依赖任何组件实例,不能访问this,所有数据必须通过参数传递。 二、项目工程 本节工程BuilderDemo基于 API 20+,所有案例页面完整对应如下: BuilderDemo/ ├── AppScope/ ├── entry/ │ ├── src/main/ets │ │ ├── entryability/EntryAbility.ets │ │ ├── pages/ │ │ │ ├── Index.ets # 入口:页面跳转 │ │ │ ├── InnerBuilderPage.ets # 组件内Builder │ │ │ ├── GlobalBuilderPage.ets # 全局Builder │ │ │ ├── BuilderParamPage.ets # @BuilderParam UI插槽 │ │ │ ├── AdvancedBuilderPage.ets # 高级:嵌套、this指向、Binding │ │ │ └── LocalBuilderPage.ets # @LocalBuilder 专属案例 三、Index.ets 入口页面 import router from '@ohos.router'; // 定义页面配置接口 interface PageConfig { title: string; url: string; } @Entry @Component struct Index { pageList: PageConfig[] = [ { title: "1. 组件内 Builder", url: "pages/InnerBuilderPage" }, { title: "2. 全局 Builder", url: "pages/GlobalBuilderPage" }, { title: "3. BuilderParam 插槽", url: "pages/BuilderParamPage" }, { title: "4. this指向问题", url: "pages/AdvancedBuilderPage" }, { title: "5. @LocalBuilder", url: "pages/LocalBuilderPage" } ]; // 自定义Builder按钮封装 @Builder builderButton(title: string, onClick: () => void) { Button(title) .width(280) .onClick(onClick); } build() { Column({ space: 20 }) { Text("@Builder & @LocalBuilder 实战") .fontSize(26) .fontWeight(FontWeight.Bold); // ForEach 循环渲染按钮 ForEach( this.pageList, (item: PageConfig) => { this.builderButton(item.title, () => { router.pushUrl({ url: item.url }); }); } ); } .width('100%') .height('100%') .justifyContent(FlexAlign.Center); } } 四、组件内 @Builder 示例演示 4.1 规则说明 组件内 @Builder 直接访问 this 状态变量时,会跟随组件整体刷新,不受传参规则限制,自动刷新; @Builder 默认按值传递参数,传递基础类型、对象变量、多参数时,外部状态更新不会触发 @Builder 刷新; @Builder 实现响应式刷新的标准方式:使用对象字面量形式传入参数,可建立响应式关联,状态更新自动刷新; 多层 @Builder 嵌套时,每一层都必须使用对象字面量传参,才能保证全链路刷新; 禁止在 @Builder 内部修改传入的参数对象,会导致运行异常; 从 API version 20 开始,可通过 UIUtils.makeBinding()、Binding / MutableBinding 实现 @Builder 内状态变量刷新; 使用 UIUtils.makeBinding() 包装读状态回调,支持 @Builder 内 UI 自动刷新; 额外传入写状态回调,可将 @Builder 内部的参数修改同步到外部组件。 4.2 说明 @Builder被用来展示组件,不会参与动态UI刷新。组件中值的变化是通过使用装饰器的特性,监听到值的改变触发的UI刷新,而不是通过@Builder的能力触发的。 4.3 示例代码 示例 1:无参 @Builder + 直接访问 this → 自动刷新 class UserInfo { name: string = '' age: number = 0 } @Entry @Component struct InnerBuilderPage { @State message: string = '组件内 @Builder 实战' @State count: number = 0 @State user: UserInfo = {name:"Tom", age:25} @Builder buildTitle() { Text(this.message) .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor('#333333') } build() { Column({ space: 15 }) { this.buildTitle() Button('更新标题') .onClick(() => { this.message = "组件内 @Builder 实战-更新" }) } .width('100%') } } 示例 2:按值传递 → 不刷新 @Builder buildButton(label: string) { Text("基础类型传参 → 不刷新:" + label) } // 调用 this.buildButton(`count:${this.count}`) 示例 3:直接传递对象变量(按值传递) → 不刷新 @Builder buildUserCardByObj(params: UserInfo) { Text(`姓名:${params.name} 年龄:${params.age}`) } // 调用 this.buildUserCardByObj(this.user) 示例 4:多参数传递(按值传递) → 不刷新 @Builder buildUserCardByValue(name:string, age:number) { Text(`姓名:${name} 年龄:${age}`) } // 调用 this.buildUserCardByValue(this.user.name, this.user.age) 示例 5:对象字面量 → 按引用传递 → 刷新 @Builder buildUserCardByRef(params: UserInfo) { Text(`姓名:${params.name} 年龄:${params.age}`) } // 调用 this.buildUserCardByRef({ name: this.user.name, age: this.user.age }) 示例 6:嵌套 @Builder + 子组件刷新对比 @Component struct ChildComponent1 { @Prop info: UserInfo = new UserInfo(); build() { Row() { Text(`ChildComponent1不刷新===${this.info.name}===${this.info.age}`) .fontSize(20) .fontWeight(FontWeight.Bold) } } } @Component struct ChildComponent2 { @Prop childName: string = ''; @Prop childAge: number = 0; build() { Row() { Text(`ChildComponent2=刷新==${this.childName}===${this.childAge}`) .fontSize(20) .fontWeight(FontWeight.Bold) } } } @Builder parentBuilder(params: UserInfo) { Text(`parentBuilder刷新===${params.name}===${params.age}`) .fontSize(20) .fontWeight(FontWeight.Bold) ChildComponent1({ info: params }) // 不刷新 ChildComponent2({ childName: params.name, childAge: params.age }) // 刷新 } 示例 7:错误示范:Builder 内部修改参数 @Builder errorModifyParam(params: UserInfo) { Button("修改") .onClick(() => { params.name = "错误修改" // 运行异常 }) } 示例 8:API20 新特性 → Binding 绑定传参(可修改+可刷新) @Builder ModifyParam(params: Binding<UserInfo>) { Text(`姓名:${params.value.name}`) Button("内部修改").onClick(() => { params.value.name = "新名字" }) } // 调用 this.ModifyParam(UIUtils.makeBinding( () => this.user, (user) => { this.user = user } )) 运行结果 五、全局 @Builder 实战 5.1 核心功能 定义全局通用UI片段,多页面共享; 无组件依赖,不绑定this,所有数据必须通过参数传递。 5.2 规则 必须加 function 关键字 跨页面使用需要加 export 5.3 示例代码 interface GlobalInfo { globalContent:string callback?: () => void } @Builder export function globalButton(globalInfo: GlobalInfo) { Button(globalInfo.globalContent) .width(180) .height(46) .backgroundColor('#34C85E') .margin(5) .onClick(globalInfo?.callback) } @Builder export function globalCard(title: string, content: string) { Column() { Text(title).fontSize(20).fontWeight(FontWeight.Bold) Text(content).fontSize(16).margin({ top: 10 }) } .width('100%') .padding(20) .backgroundColor('#fff') .borderRadius(12) .margin(10) } @Entry @Component struct GlobalBuilderPage { @State content: string = '全局复用,跨页面统一样式' @State globalInfo: GlobalInfo = {globalContent:'全局按钮1'} build() { Column({ space: 20 }) { globalCard('全局 @Builder 实战', this.content) // 只能通过引用传递 一一传递对应参数 否则无法刷新。 globalButton({ globalContent:this.globalInfo.globalContent, callback:()=>{ this.globalInfo.globalContent = '全局按钮点击更新' } }) } .width('100%') .padding(20) } } 5.4 运行结果 六、@BuilderParam UI插槽 6.1 核心功能 @BuilderParam 用于接收外部传入的 @Builder 函数,实现UI插槽(slot) 能力,让组件结构固定、内容可灵活定制。 6.2 说明 @BuilderParam 是组件属性装饰器,专门用于接收 @Builder 函数类型的参数。 使用场景:父组件向子组件传递一段UI结构,子组件只负责布局,不关心具体内容。 6.3 示例代码 @Component struct CardComponent { @BuilderParam header?: () => void @BuilderParam content?: () => void build() { Column() { if (this.header){ this.header() } Divider().margin(10).color(Color.Red).strokeWidth(2) if (this.content){ this.content() } } .width('100%') .backgroundColor('#fff') .padding(20) .borderRadius(12) } } @Entry @Component struct BuilderParamPage { @Builder customHeader() { Text('自定义头部').fontSize(22).fontWeight(FontWeight.Bold) } @Builder customContent() { Text('这是插槽内容,灵活传入任意UI') Button('插槽内按钮').width(150).margin(10) } build() { Column({ space: 20 }) { CardComponent({ header: this.customHeader, content:this.customContent }) } .padding(20) } } 6.4 运行结果 七、关于this 指向(重点) 我们在使用 @Builder + @BuilderParam 跨组件传递时,会出现一个非常关键的问题:this 指向会变导致UI刷新不符合预期。 7.1 this 指向核心规则 @Builder 直接作为引用传递时,this 由「调用它的组件」决定。 箭头函数没有自己的 this,会继承外层作用域的 this。 在 @BuilderParam 场景: 直接传递:customBuilderParam: this.componentBuilder → 函数在 Child 里执行,this 指向 Child。 箭头函数包裹:customBuilderParam: () => { this.componentBuilder() } → 继承父组件 this,this 指向 Parent。 7.2 示例代码 /** * 问题:@Builder + @BuilderParam this 指向问题 * 页面运行结果:Parent Child Parent * 核心规则: * 1. 直接传递函数引用 → this 由【调用者】决定 * 2. 箭头函数包裹 → this 继承【定义时的组件】 */ @Component struct ChildComponent { label: string = "Child"; // 默认占位Builder,无实际UI @Builder customBuilder() {} @Builder customChangeThisBuilder() {} // 接收外部传入的Builder函数 @BuilderParam customBuilderParam: () => void = this.customBuilder; @BuilderParam customChangeThisBuilderParam: () => void = this.customChangeThisBuilder; build() { Row({ space: 15 }) { // 执行外部传入的构建函数 this.customBuilderParam() this.customChangeThisBuilderParam() } } } @Entry @Component struct AdvancedBuilderPage { label: string = "Parent"; // 普通 @Builder:this 由【运行时调用者】决定,不固定! @Builder componentBuilder() { Text(`${this.label}`) .fontSize(26) .fontWeight(FontWeight.Bold) } build() { Column({ space: 20 }) { Text("运行结果:").fontSize(22) Row({ space: 15 }) { // 1️⃣ 父组件自己调用 → this 指向Parent → 显示 Parent this.componentBuilder() ChildComponent({ // 2️⃣ 直接传递函数引用:函数在子组件内执行 → this 指向Child → 显示 Child customBuilderParam: this.componentBuilder, // 3️⃣ 箭头函数包裹:继承父组件this → this 固定指向Parent → 显示 Parent customChangeThisBuilderParam: () => { this.componentBuilder() } }) } } .width('100%') .padding(30) } } 7.3 运行结果 最终页面显示:Parent Child Parent 7.4 一句话总结 this 是函数执行时的上下文对象,它的指向不是在函数定义时确定的,而是在函数调用时才最终确定。 普通函数:谁调用,this 就指向谁(默认规则,也是最容易错乱的场景); 箭头函数:没有自己的 this,继承外层作用域的 this(永远不会错乱,优先推荐); 八、@LocalBuilder 装饰器 @Builder 在跨组件传递时,this 会被改变,这会导致严重问题UI 刷新不符合预期,为了彻底解决这个问题,ArkTS提供了 @LocalBuilder。 @LocalBuilder可以固定this指向当前 8.1 设计目的 @LocalBuilder 拥有和局部 @Builder 完全一样的UI构建能力,但可以: 固定 this 指向 保证组件父子关系不变 保证状态管理父子关系不变 从根源解决 @Builder 的上下文错乱问题。 8.2 核心特性 只能在组件内部定义,不支持全局; this 永远指向声明它的组件,不会被任何方式改变; 跨组件传递时,组件归属固定,不会被修改; 调用方式与 @Builder 完全一致:this.xxx()。 8.3 @LocalBuilder与@Builder对比示例 @Component struct Child { label: string = 'Child'; @BuilderParam customBuilderParam?: () => void; build() { if (this.customBuilderParam){ this.customBuilderParam() }else { Text('@BuilderParam undefined' ) } } } @Entry @Component struct LocalBuilderPage { label: string = 'Parent'; // @Builder:this 指向调用方 → Child @Builder componentBuilder() { Text(this.label) } // @LocalBuilder:this 永远指向 Parent @LocalBuilder componentLocalBuilder() { Text(this.label) } build() { Column({ space: 20 }) { // 显示 Child Child({ customBuilderParam: this.componentBuilder }) // 显示 Parent Child({ customBuilderParam: this.componentLocalBuilder }) } } } 8.4 限制条件 只能在组件内声明,不允许全局; 不能与其他任何装饰器一起使用; 不能修饰静态方法; 函数内部不允许修改参数; 作为参数传递时推荐:直接传函数:this.func 或箭头函数:() => { this.func() } this指向都不会变. 禁止直接传执行结果,会导致布局错乱。 8.5 参数传递规则(与 @Builder 一致) 按值传递:基础类型、多参数、直接对象 → 不刷新 按引用传递:单个对象字面量 → 可刷新 按回调传递:API20 使用 UIUtils.makeBinding → 可刷新可修改 注意:子组件调用父组件的 @LocalBuilder 并传参时,子组件状态变化不会触发 @LocalBuilder 刷新;推荐:@LocalBuilder 内直接访问父组件自身状态。 九、代码仓库 工程名称:BuilderDemo 仓库地址:https://gitee.com/HarmonyOS-UI-Basics/harmony-os-ui-basics.git 十、下节预告 下一节我们将深入学习鸿蒙UI样式复用三大核心方案: 掌握 @Styles 通用样式复用,实现多组件基础样式统一; 掌握 @Extend 组件专属样式扩展,支持参数与私有属性; 掌握 stateStyles 多态状态样式,实现按压/禁用/选中自动切换; 清晰区分三者适用场景,写出简洁、可维护、高性能样式代码;