@Reusable組件復用概述:
ArkUI布局中,將自定義組件從組件樹上移除后放入緩存池,后續在創建相同類型的組件節點時,直接復用緩存池中的組件對象。ArkUI中使用@Reusable裝飾器以實現自定義組件的復用。
常見的組件復用場景是當有大量數據使用相同的組件模版在界面中展示時,比如:List(列表)、Grid(網格)、Swiper(輪播)等組件中使用
優點:
1、避免頻繁創建和銷毀對象的過程,減少內存回收的頻率
2、復用緩存中的組件并直接綁定數據進行顯示,與創建新視圖相比,降低了計算開銷,提升了顯示效率
下面使用List列表加載大量數據的簡單案例:
1、用export class聲明在Item上呈現數據的ItemData類,并用@Observed修飾該類。
@Observed類裝飾器用于在涉及嵌套對象或數組的場景中進行雙向數據同步
@Observed
export class ItemData {id: string = '';title: string | Resource = '';content: string = '';from: string | Resource = '';isSelect:boolean = falseconstructor(id: string, type: number) {this.id = id;this.type = type;}
}
2、創建類ItemDataSource 并實現IDataSource接口,重寫IDataSource中的抽象函數:
IDataSource用于向ForEach或LazyForEach組件提供數據。
totalCount(): number-返回列表項展示總數
getData(index: number): any - 返回Item上展示的數據對象
registerDataChangeListener(listener: DataChangeListener): void - 注冊Data數據改變的監聽對象。
unregisterDataChangeListener(listener: DataChangeListener): void - 注銷Data數據改變的監聽對象
import { ItemData } from "./ItemData";
export class ItemDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: ItemData[] = [];public totalCount(): number {return this.originDataArray.length;}public getData(index: number): ItemData {return this.originDataArray[index];}registerDataChangeListener(listener: DataChangeListener): void {console.log("listener----------",listener)if (this.listeners.indexOf(listener) < 0) {this.listeners.push(listener);}}unregisterDataChangeListener(listener: DataChangeListener): void {const pos = this.listeners.indexOf(listener);if (pos >= 0) {this.listeners.splice(pos, 1);}}
基于適配器(Adapter)設計模式,將數據源和視圖組件相互獨立,數據源的改變和組件的改變不相互影響。我們在ItemDataSource類中添加一下一些常見的更新數據的方法。
//更新所有列表數據
notifyDataReload(): void {this.listeners.forEach(listener => {listener.onDataReloaded();});
}
//指定位置插入元素后更新
notifyDataAdd(index: number): void {this.listeners.forEach(listener => {listener.onDataAdd(index);});
}
//item數據源改變后更新
notifyDataChange(index: number): void {this.listeners.forEach(listener => {listener.onDataChange(index);});
}
// 獲取數據后,更新列表
public pushArray(newData: ItemData[]): void {this.originDataArray.push(...newData);this.notifyDataReload();
}
... 增刪改查的方法雷同,不再贅述。
3、定義ListItemView組件,并且用@Reusable裝飾器修飾以實現組件的復用。
父子組件的傳參可以用@Require (@Prop、@Link ......)等裝飾器修飾
@Require裝飾器修飾的變量,在構造該自定義組件時,必須在構造時傳參。
@Prop裝飾的變量可以和父組件建立單向同步關系。
@Link裝飾的變量與其父組件中對應的數據源建立雙向數據綁定。
*子組件的點擊事件回調,可以聲明函數變量,
@Require onItemClick:(itemData:ItemData,index:number) => void = (itemData:ItemData,index:number)=>{},
import { ItemData } from "../../../model/ItemData";
@Reusable
@Component
export struct StudyListItemView {@Require @Prop mItemData:ItemData@Require @Prop index:number@Require onItemClick:(itemData:ItemData,index:number) => void = (itemData:ItemData,index:number)=>{}// update data in aboutToReuse methodaboutToReuse(params: Record<string, Object>): void {this.mItemData = params.mItemData as ItemData}isSelectIcon(isSelect:boolean):Resource{return isSelect? $r('app.media.ic_tab_lab_select'):$r('app.media.ic_tab_lab')}build() {Column() {Text(this.mItemData.title).fontSize(16).fontWeight(FontWeight.Medium).fontColor(Color.Black).lineHeight(22).textOverflow({ overflow: TextOverflow.Ellipsis }).width('100%')Row() {Text(this.mItemData.from).fontSize(12).fontWeight(FontWeight.Regular).fontColor(0x0A59F7)Text(this.mItemData.tail).fontSize(12).opacity(0.4).fontWeight(FontWeight.Regular).margin({ left: 6 }).layoutWeight(1)Image(this.isSelectIcon(this.mItemData.isSelect)).width(32).height(32)}.onClick((event)=>{this.onItemClick(this.mItemData,this.index)}).margin({ top: 12 })}.padding({top: 16,bottom: 12,left: 16,right: 16}).margin({ top: 12, left: 16, right: 16 }).borderRadius(12).backgroundColor(Color.White)}
}
4、使用List列表組件,渲染ListItemView組件
//創建ItemDataSource對象
private dataSource: ItemDataSource = new ItemDataSource();
aboutToAppear(): void {this.requestUpdate()
}
// 在鉤子函數中,模擬請求網絡數據
requestUpdate = () => {this.dataSource.pushArray(genMockItemData(1000));
}
// Item點擊是更改Item上的數據,通過數據驅動,改變ArkUI界面展示
onItemClick = (info:ItemData,index:number) =>{let tempInfo:ItemData = this.dataSource.getData(index)tempInfo.title = "--我是更改后的數據--"tempInfo.from = "update"tempInfo.tail = "13223211234"tempInfo.isSelect = !tempInfo.isSelectthis.dataSource.notifyDataChange(index)
}
//ArkUI 頁面繪制如下
build() {NavDestination(){RelativeContainer(){Column(){List(){LazyForEach(this.dataSource,(info:ItemData,index:number)=>{StudyListItemView({mItemData: info,index:index,onItemClick: this.onItemClick})},(item:StudyInfo)=>item.id)}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]).cachedCount(1)}.height('100%')}.width('100%').height('100%')}.title("組件復用").onReady((context)=>{this.pathStack = context.pathStack}).backgroundColor(0xF1F3F5)
}
5、使用ArkUI的Refresh組件,實現列表組件的下拉刷新與上拉加載
@State isRefreshing: boolean = false; //是否正在刷新中
@State isLoading: boolean = false; //是否正在加載中
@State page: number = 1; //當前頁面
@State hasMoreData: boolean = true; // 無更多數據時展示視圖
Refresh(this.isRefreshing) {// List組件...// 加載更多提示項if (this.isLoading || this.hasMoreData) {ListItem() {Row() {if (this.isLoading) {LoadingProgress().color('#007DFF').width(24).height(24)}Text(this.isLoading ? '加載中...' : '上拉加載更多').fontSize(14).fontColor('#999999').margin({ left: 10 })}.width('100%').height(50).justifyContent(FlexAlign.Center)}} else {// 沒有更多數據提示ListItem() {Text('已經到底啦').fontSize(14).fontColor('#999999').width('100%').height(50).textAlign(TextAlign.Center)}}
}
.onRefresh(() => {this.onRefresh();
})
模擬下拉加載與上拉刷新視圖
// 下拉刷新處理函數
onRefresh = () => {this.isRefreshing = true;this.page = 1;setTimeout(()=>{this.requestUpdate();},1000)
}// 上拉加載處理函數
onReachBottom() {if (!this.isLoading && this.hasMoreData) {this.isLoading = true;this.page++;setTimeout(()=>{this.requestUpdate();},1000)}
}
模擬分頁請求網絡數據
requestUpdate() {this.isRefreshing = false;this.isLoading = false;let tempData = getMockItemDataByPage(this.page, this.page == 4 ? 10 : 8)if (tempData.length < 10){this.hasMoreData = true}else{this.hasMoreData = false}if (this.page == 1) {this.dataSource.resetArray(tempData);}else {this.dataSource.pushArray(tempData);}
}
至此List列表分頁加載網絡數據并完成組件復用。@Component組件完整代碼如下:
@Component
export struct StudyList{private pathStack:NavPathStack = new NavPathStack()private dataSource: ItemDataSource = new ItemDataSource();@State isRefreshing: boolean = false;@State isLoading: boolean = false;@State page: number = 1;@State hasMoreData: boolean = true;aboutToAppear(): void {this.requestUpdate()}// 下拉刷新處理函數onRefresh = () => {this.isRefreshing = true;this.page = 1;setTimeout(()=>{this.requestUpdate();},1000)}// 上拉加載處理函數onReachBottom() {if (!this.isLoading && this.hasMoreData) {this.isLoading = true;this.page++;setTimeout(()=>{this.requestUpdate();},1000)}}requestUpdate() {this.isRefreshing = false;this.isLoading = false;let tempData = getMockItemDataByPage(this.page, this.page == 4 ? 10 : 8)if (tempData.length < 10){this.hasMoreData = true}else{this.hasMoreData = false}if (this.page == 1) {this.dataSource.resetArray(tempData);}else {this.dataSource.pushArray(tempData);}}onItemClick = (info:ItemData,index:number) =>{let tempInfo:ItemData = this.dataSource.getData(index)tempInfo.title = "--我是更改后的數據--"tempInfo.from = "update"tempInfo.tail = "1分鐘之前"tempInfo.isSelect = !tempInfo.isSelectthis.dataSource.notifyDataChange(index)}build() {NavDestination(){RelativeContainer(){Column(){Refresh({ refreshing: this.isRefreshing }){List(){LazyForEach(this.dataSource,(info:ItemData,index:number)=>{StudyListItemView({mItemData: info,index:index,onItemClick: this.onItemClick})},(item:StudyInfo)=>item.id)// 加載更多提示項if (this.isLoading || this.hasMoreData) {ListItem() {Row() {if (this.isLoading) {LoadingProgress().color('#007DFF').width(24).height(24)}Text(this.isLoading ? '加載中...' : '上拉加載更多').fontSize(14).fontColor('#999999').margin({ left: 10 })}.width('100%').height(50).justifyContent(FlexAlign.Center)}} else {// 沒有更多數據提示ListItem() {Text('已經到底啦').fontSize(14).fontColor('#999999').width('100%').height(50).textAlign(TextAlign.Center)}}}.width('100%').height('100%').onReachEnd(() => {this.onReachBottom();}).expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]).cachedCount(1)}.onRefreshing(()=>{this.onRefresh()})}.height('100%')}.width('100%').height('100%')}.title("組件復用").onReady((context)=>{this.pathStack = context.pathStack}).backgroundColor(0xF1F3F5)}
}