鴻蒙應用開發從入門到實戰(十八):組件編程思想之代碼復用
大家好,我是潘Sir,持續分享IT技術,幫你少走彎路。《鴻蒙應用開發從入門到項目實戰》系列文章持續更新中,陸續更新AI+編程、企業級項目實戰等原創內容、歡迎關注!
ArkUI提供了豐富的系統組件,用于制作鴻蒙原生應用APP的UI,在制作UI時會經常遇到代碼或樣式重復問題,本文通過ArkUI提供的適配器實現代碼復用。
一、樣式復用
1.1 概述
當多個組件具有相同的樣式時,若每個組件都單獨設置,將會有大量的重復代碼。為避免重復代碼,開發者可使用@Styles或者@Extend裝飾器將多條樣式設置提煉成一個方法,然后直接在各組件聲明的位置進行調用,這樣就能完成樣式的復用。

1.2 @Styles方法
@Styles方法可定義在組件內或者全局,具體語法如下
- 組件內
@Entry
@Component
struct StylesPage {
build() {
Column() {
Row({ space: 50 }) {
Button('確認')
.type(ButtonType.Normal)
.backgroundColor(Color.Green)
.compButtonStyle() //復用樣式
.onClick(() => console.log('確認'))
Button('取消')
.type(ButtonType.Normal)
.backgroundColor(Color.Gray)
.compButtonStyle() //復用樣式
.onClick(() => console.log('取消'))
}
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
//組件內樣式定義
@Styles compButtonStyle() {
.width(100)
.height(40)
.borderRadius(10)
}
}
- 全局
@Entry
@Component
struct StylesPage {
build() {
Column() {
Row({ space: 50 }) {
Button('確認')
.type(ButtonType.Normal)
.backgroundColor(Color.Green)
.globalButtonStyle() //復用樣式
.onClick(() => console.log('確認'))
Button('取消')
.type(ButtonType.Normal)
.backgroundColor(Color.Gray)
.globalButtonStyle() //復用樣式
.onClick(() => console.log('取消'))
}
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
//全局樣式定義
@Styles function globalButtonStyle() {
.width(100)
.height(40)
.borderRadius(10)
}
注意
- 組件內的
@Styles方法只能在當前組件中使用,全局的@Styles方法目前只允許在當前的.ets文件中使用 - 組件內定義
@Styles方法時不需要使用function關鍵字,全局的@Styles方法需要使用function關鍵字 @Styles方法中只能包含通用屬性方法和通用事件方法@Styles方法不支持參數
示例代碼
pages/component目錄下新建resue目錄,新建StylesPage.ets文件
@Entry
@Component
struct StylesPage {
build() {
Column() {
Row({ space: 50 }) {
Button('確認')
.type(ButtonType.Normal)
.backgroundColor(Color.Green)
.compButtonStyle() //復用樣式
// .globalButtonStyle() //復用樣式
.onClick(() => console.log('確認'))
Button('取消')
.type(ButtonType.Normal)
.backgroundColor(Color.Gray)
.compButtonStyle() //復用樣式
// .globalButtonStyle() //復用樣式
.onClick(() => console.log('取消'))
}
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
//組件內樣式定義
@Styles compButtonStyle() {
.width(100)
.height(40)
.borderRadius(10)
}
}
//全局樣式定義
@Styles function globalButtonStyle() {
.width(100)
.height(40)
.borderRadius(10)
}
1.3 @Extend方法
@Extend裝飾的方法同樣可用于組件樣式的復用,與@Styles不同的是,@Extend方法只能定義在全局。并且@Extend方法只能用于指定類型的組件,例如以下方法只能用于Button組件(可以理解為是Button組件的擴展樣式)
@Extend(Button) function buttonStyle(){
...
}
由于@Extend方法只能用于指定類型的組件,因此方法中可包含指定組件的專有屬性方法和專有事件方法。另外,@Extend方法還支持參數,具體語法如下
@Entry
@Component
struct ExtendPage {
build() {
Column() {
Row({ space: 50 }) {
Button('確認')
.buttonExtendStyle(Color.Green, () => console.log('確認')) //復用樣式
Button('取消')
.buttonExtendStyle(Color.Gray, () => console.log('取消')) //復用樣式
}
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
//樣式定義
@Extend(Button) function buttonExtendStyle(color: Color, callback: () => void) {
.width(100)
.height(40)
.borderRadius(10)
.type(ButtonType.Normal)
.backgroundColor(color)
.onClick(callback)
}
總結
@Extend方法只能定義在全局,使用范圍目前只限于當前的.ets文件@Extend方法用于特定類型的組件,因此可包含該組件的專有屬性方法和專有事件方法@Extend方法支持參數
示例代碼
pages/component/resue目錄下新建ExtendPage.ets文件
@Entry
@Component
struct ExtendPage {
build() {
Column() {
Row({ space: 50 }) {
Button('確認')
.buttonExtendStyle(Color.Green, () => console.log('確認')) //復用樣式
Button('取消')
.buttonExtendStyle(Color.Gray, () => console.log('取消')) //復用樣式
}
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
//樣式定義
@Extend(Button) function buttonExtendStyle(color: Color, callback: () => void) {
.width(100)
.height(40)
.borderRadius(10)
.type(ButtonType.Normal)
.backgroundColor(color)
.onClick(callback)
}
二、UI結構復用
2.1 概述
當頁面有多個相同的UI結構時,若每個都單獨聲明,同樣會有大量重復的代碼。為避免重復代碼,可以將相同的UI結構提煉為一個自定義組件,完成UI結構的復用。
除此之外,ArkTS還提供了一種更輕量的UI結構復用機制@Builder方法,開發者可以將重復使用的UI元素抽象成一個@Builder方法,該方法可在build()方法中調用多次,以完成UI結構的復用。

2.2 語法說明
@Builder方法同樣可以定義在組件內或者全局,具體語法如下
- 組件內
@Entry
@Component
struct BuilderPage {
build() {
Column() {
Row({ space: 50 }) {
//復用UI結構
this.compButtonBuilder($r('app.media.icon_edit'), '編輯', () => console.log('編輯'))
this.compButtonBuilder($r('app.media.icon_send'), '發送', () => console.log('發送'))
}
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
//定義UI結構
@Builder compButtonBuilder(icon: Resource, text: string, callback: () => void) {
Button() {
Row({ space: 10 }) {
Image(icon)
.width(25)
.height(25)
Text(text)
.fontColor(Color.White)
.fontSize(25)
}
}.width(120)
.height(50)
.onClick(callback)
}
}
- 全局
@Entry
@Component
struct BuilderPage {
build() {
Column() {
Row({ space: 50 }) {
//復用UI結構
globalButtonBuilder($r('app.media.icon_edit'), '編輯', () => console.log('編輯'))
globalButtonBuilder($r('app.media.icon_send'), '發送', () => console.log('發送'))
}
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
//定義UI結構
@Builder function globalButtonBuilder(icon: Resource, text: string, callback: () => void) {
Button() {
Row({ space: 10 }) {
Image(icon)
.width(25)
.height(25)
Text(text)
.fontColor(Color.White)
.fontSize(25)
}
}.width(120)
.height(50)
.onClick(callback)
}
注意
- 組件內的
@Builder方法可通過this訪問當前組件的屬性和方法,而全局的@Builder方法則不能 - 組件內的
@Builder方法只能用于當前組件,全局的@Builder方法導出(export)后,可用于整個應用。
示例代碼
拷貝icon_edit.png和icon_send.png文件到resources/base/media目錄
pages/component/resue目錄下新建BuilderPage.ets文件
@Entry
@Component
struct BuilderPage {
build() {
Column() {
Row({ space: 50 }) {
this.compButtonBuilder($r('app.media.icon_edit'), '編輯', () => console.log('編輯'))
this.compButtonBuilder($r('app.media.icon_send'), '發送', () => console.log('發送'))
// globalButtonBuilder($r('app.media.icon_edit'), '編輯', () => console.log('編輯'))
// globalButtonBuilder($r('app.media.icon_send'), '發送', () => console.log('發送'))
}
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
@Builder compButtonBuilder(icon: Resource, text: string, callback: () => void) {
Button() {
Row({ space: 10 }) {
Image(icon)
.width(25)
.height(25)
Text(text)
.fontColor(Color.White)
.fontSize(25)
}
}.width(120)
.height(50)
.onClick(callback)
}
}
@Builder function globalButtonBuilder(icon: Resource, text: string, callback: () => void) {
Button() {
Row({ space: 10 }) {
Image(icon)
.width(25)
.height(25)
Text(text)
.fontColor(Color.White)
.fontSize(25)
}
}.width(120)
.height(50)
.onClick(callback)
}
2.3 @Builder方法參數傳遞規則
@Builder方法具有兩種參數傳遞機制——按值傳遞和按引用傳遞。當只有一個參數且參數為對象字面量時為按引用傳遞,其余情況均為按值傳遞。
按引用傳遞時,若傳遞的參數為狀態變量,則狀態變量的變化將會觸發@Builder方法內部UI的刷新;按值傳遞時則不會。
示例代碼
pages/component/resue目錄下新建BuilderParameterPage.ets文件
@Entry
@Component
struct BuilderParameterPage {
@State count: number = 0;
build() {
Column({ space: 50 }) {
//按值傳遞
valueTextBuilder(this.count)
//按引用傳遞
referenceTextBuilder({ count: this.count })
Row({ space: 50 }) {
Button('-1').onClick(() => {
this.count--;
})
Button('+1').onClick(() => {
this.count++;
})
}
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
@Builder function valueTextBuilder(count: number) {
Text(`按值傳遞: ${count}`)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
@Builder function referenceTextBuilder(obj: { count: number }) {
Text(`按引用傳遞: ${obj.count}`)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
2.4 @Builder方法和自定義組件的區別
@Builder方法和自定義組件雖然都可以實現UI復用的效果,但是兩者還是有著本質的區別的,其中最為顯著的一個區別就是自定義組件可以定義自己的狀態變量,而@Builder方法則不能。
以下案例中,每個待辦事項的UI結構都相同,因此可考慮將其提煉為一個自定義組件或者@Builder方法,但是由于每個待辦事項均有已完成和未完成兩種狀態,因此需要為每個待辦事項都定義一個狀態變量,所以此時就只能使用自定義組件而不能使用@Builder方法。

總結
若復用的UI結構沒有狀態,推薦使用@Builder方法,否則使用自定義組件。
示例代碼
pages/component/resue目錄下新建DifferencePage.ets文件
@Entry
@Component
struct DifferencePage {
build() {
Column({ space: 10 }) {
Text('待辦事項')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.width('100%')
TodoItem({ text: '讀書' })
TodoItem({ text: '運動' })
TodoItem({ text: '早睡' })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
.padding(10)
.backgroundColor('#f2f2f2')
}
}
@Component
struct TodoItem {
text: string;
@State isDone: boolean = false;
build() {
Row() {
Text(this.text)
.fontSize(30)
.fontWeight(FontWeight.Medium)
//文本裝飾線,根據isDone的值選擇不同的類型
.decoration({ type: this.isDone ? TextDecorationType.LineThrough : TextDecorationType.None })
//用于填充Column/Row容器的剩余空間
Blank()
Toggle({ type: ToggleType.Checkbox })
.onChange((value) => {
this.isDone = value;
})
}
.width('100%')
.height(60)
.backgroundColor(Color.White)
.padding(10)
.borderRadius(10)
}
}
2.5 @BuilderParam
@BuilderParam用于裝飾自定義組件(struct)中的屬性,其裝飾的屬性可作為一個UI結構的占位符,待創建該組件時,可通過參數為其傳入具體的內容。(其作用類似于Vue框架中的slot)。
- 組件定義
@Component
struct Container {
//@BuilderParam屬性
@BuilderParam content: () => void
build() {
Column() {
Text('其他內容') //其他內容
this.content(); //占位符
Button('其他內容') //其他內容
}
}
}
- UI結構定義
@Builder function contentBuilder1() {
...
}
@Builder function contentBuilder2() {
...
}
@Builder function contentBuilder3() {
...
}
- 組件創建
Container({ content: contentBuilder1 })
Container({ content: contentBuilder2 })
Container({ content: contentBuilder3 })
下面通過一個案例展示@BuilderParam的具體用法,例如,現需要實現一個通用的卡片組件,如下圖所示

卡片中顯示的內容不固定,例如

具體實現步驟如下:
(1)卡片組件定義
@Component
struct Card {
@BuilderParam content: () => void; //@BuilderParam屬性
build() {
Column() {
this.content(); //占位符
}.width('90%')
.padding(10)
.borderRadius(10)
.shadow({ radius: 20 })
}
}
效果

(2)卡片內容定義
@Builder function imageBuilder() {
Column({ space: 10 }) {
Image($r('app.media.img_harmony'))
.width(300)
.height(150)
Text('鴻蒙操作系統')
}
}
效果圖

(3)創建卡片組件
Card({ content: imageBuilder })

另外,如果一個組件中只定義了一個@BuilderParam屬性,那么創建該組件時,也可直接通過"子組件"的方式傳入具體的UI結構,例如
創建卡片組件
Card() {
Column({ space: 10 }) {
Text('鴻蒙操作系統')
.fontSize(25)
.fontWeight(FontWeight.Bold)
Text('鴻蒙操作系統是...')
}
}
效果圖

示例代碼
pages/component/resue目錄下新建BuilderParamPage2.ets文件
@Entry
@Component
struct BuilderParamPage2 {
build() {
Column({ space: 50 }) {
//創建卡片組件(傳參)
Card({ content: imageBuilder })
//創建卡片組件("子組件")
Card() {
Column({ space: 10 }) {
Text('鴻蒙操作系統')
.fontSize(25)
.fontWeight(FontWeight.Bold)
Text('鴻蒙操作系統是一款由華為公司開發的多設備統一操作系統,致力于實現無縫連接和協同工作。其采用分布式架構,支持多終端智能互聯,提供高效、安全、流暢的用戶體驗。')
}
}
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
//卡片內容
@Builder function imageBuilder() {
Column({ space: 10 }) {
Image($r('app.media.img_harmony'))
.width(300)
.height(150)
Text('鴻蒙操作系統')
}
}
//卡片組件
@Component
struct Card {
@BuilderParam content: () => void; //@BuilderParam屬性
build() {
Column() {
this.content(); //占位符
}.width('90%')
.padding(10)
.borderRadius(10)
.shadow({ radius: 20 })
}
}
《鴻蒙應用開發從入門到項目實戰》系列文章持續更新中,陸續更新AI+編程、企業級項目實戰等原創內容,防止迷路,歡迎關注!
作者:黑馬騰云
微信公眾賬號:自學幫
博客園:黑馬騰云博客
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注微信公眾號)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
浙公網安備 33010602011771號