@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)}
}