HarmonyOS:@Reusable装饰器:组件复用
@Reusable装饰器装饰任意自定义组件时,表示该自定义组件可以复用。
说明
从API version 10开始,对@Reusable进行支持,支持在ArkTS中使用。
一、概述
@Reusable适用自定义组件,与@Component结合使用,标记为@Reusable的自定义组件从组件树上被移除时,组件和其对应的JSView对象都会被放入复用缓存中,后续创建新自定义组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。
二、限制条件
@Reusable装饰器仅用于自定义组件。
// 编译报错,仅用于自定义组件
@Reusable
@Builder
function buildCreativeLoadingDialog(closedClick: () => void) {
Crash()
}
ComponentContent不支持传入@Reusable装饰器装饰的自定义组件。
示例效果图
示例代码
import { ComponentContent } from "@kit.ArkUI"
// @Reusable装饰器仅用于自定义组件。
// 编译报错,仅用于自定义组件
// @Reusable // 注释掉编译不报错
@Builder
function buildCreativeLoadingDialog(closedClick: () => void) {
Crash()
}
// 如果注释掉就可以正常弹出弹窗,如果加上@Reusable就直接crash
@Reusable
@Component
export struct Crash {
build() {
Column() {
Text("Crash")
.fontSize(12)
.lineHeight(18)
.fontColor(Color.Blue)
.margin({
left: 6
})
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
@Builder
function buildCreativeLoadingDialog2(closedClick: () => void) {
Crash2()
}
@Component
export struct Crash2 {
build() {
Column() {
Text("Crash2 弹框提示, 没有使用@Reusable装饰器")
.fontSize(20)
.lineHeight(18)
.fontColor(Color.Blue)
.margin({
left: 6,
top: 100
})
.padding(26)
.backgroundColor(Color.White)
.borderRadius(8)
.width(300)
.height(200)
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
}
}
@Entry
@Component
struct TestReusable {
@State message: string = '@Reusable装饰器:组件复用';
private uicontext = this.getUIContext()
build() {
RelativeContainer() {
Text(this.message)
.id('Index')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => {
// ComponentContent底层是buildNode,buildNode不支持传入@Reusable注解的自定义组件
let contentNode = new ComponentContent(this.uicontext, wrapBuilder(buildCreativeLoadingDialog), () => {
});
this.uicontext.getPromptAction().openCustomDialog(contentNode);
// 传入不使用@Reusable注解的自定义组件
// let contentNode2 = new ComponentContent(this.uicontext, wrapBuilder(buildCreativeLoadingDialog2), () => {
// });
// this.uicontext.getPromptAction().openCustomDialog(contentNode2);
})
}
.height('100%')
.width('100%')
}
}
传入不使用@Reusable注解的自定义组件效果图
@Reusable装饰器不支持嵌套使用。
效果图
ReusableMessage.ets代码
export class ReusableMessage {
value: string | undefined
constructor(value: string) {
this.value = value
}
}
TestReusable2.ets代码
import { ReusableMessage } from "./ReusableMessage"
@Entry
@Component
struct TestReusable2 {
@State switch: boolean = true;
@State childswitch: boolean = false;
build() {
Column() {
Button('click')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.switch = !this.switch;
})
if (this.switch) {
// 父自定义组件已经添加@Reusable(出于描述方便,以下子组件代指自定义组件,父组件代指父自定义组件)
Parent({ message: new ReusableMessage('Parent') })
.reuseId('Parent')
}
}
.height("100%")
.width('100%')
}
}
@Reusable
@Component
struct Parent {
@State message: ReusableMessage = new ReusableMessage('AboutToReuse');
@State switchchild: boolean = true;
aboutToRecycle(): void {
console.info("aboutToRecycle===Parent ====Child==");
}
aboutToReuse(params: Record<string, ESObject>) {
this.message = params.message as ReusableMessage;
console.info("aboutToReuse==Parent====Child==" + this.message.value);
}
build() {
Column() {
Button('click child')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.switchchild = !this.switchchild;
})
// 子自定义组件
if (this.switchchild) {
// 父自定义组件已经添加@Reusable
HasReusableChild({ message: new ReusableMessage('From ChildReuse') });
}
Text(this.message.value)
.fontSize(30)
}
.borderWidth(1)
.height(100)
}
}
// 可复用的自定义组件的子树中存在可复用的自定义组件,如果子组件标记@Reuable,会导致同一颗子树下复用率变低,因此不建议子组件加上@Reusable
@Reusable
@Component
export struct HasReusableChild {
@State message: ReusableMessage = new ReusableMessage('AboutToReuse');
// 子组件有@Reusable,单独刷新子组件,执行了子组件的aboutToReuse,父组件是不复用的
aboutToAppear(): void {
console.info("aboutToAppear=== HasReusableChild ====Child==");
}
aboutToDisappear(): void {
console.info("aboutToDisappear=== HasReusableChild ====Child==");
}
aboutToRecycle(): void {
console.info("aboutToRecycle=== HasReusableChild ====Child==");
}
// 正常复用,父组件刷新,引发子组件aboutToReuse方法的执行
aboutToReuse(params: Record<string, ESObject>) {
this.message = params.message as ReusableMessage;
console.info("aboutToReuse== HasReusableChild ====Child==" + this.message.value);
}
build() {
Column() {
Text(this.message.value)
.fontSize(12)
.lineHeight(18)
.fontColor(Color.Blue)
.margin({
left: 6
})
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
三、使用场景
- 列表滚动:当应用需要展示大量数据的列表,并且用户进行滚动操作时,频繁创建和销毁列表项的视图可能导致卡顿和性能问题。在这种情况下,使用列表组件的组件复用机制可以重用已经创建的列表项视图,提高滚动的流畅度。
- 动态布局更新:如果应用中的界面需要频繁地进行布局更新,例如根据用户的操作或数据变化动态改变视图结构和样式,重复创建和销毁视图可能导致频繁的布局计算,影响帧率。在这种情况下,使用组件复用可以避免不必要的视图创建和布局计算,提高性能。
- 频繁创建和销毁数据项的视图场景下。使用组件复用可以重用已创建的视图,只更新数据的内容,减少视图的创建和销毁,能有效提高性能。
四、使用场景举例
4.1 动态布局更新
- 示例代码将Child自定义组件标记为复用组件,通过Button点击更新Child,触发Child复用;
- @Reusable:自定义组件被@Reusable装饰器修饰,即表示其具备组件复用的能力;
- aboutToReuse:当一个可复用的自定义组件从复用缓存中重新加入到节点树时,触发aboutToReuse生命周期回调,并将组件的构造参数传递给aboutToReuse。
TestReusable3.ets代码
import { ReusableMessage } from "./ReusableMessage"
@Entry
@Component
struct TestReusable3 {
@State switch: boolean = true;
build() {
Column() {
Button('Hello')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.switch = !this.switch;
})
if (this.switch) {
Child({ message: new ReusableMessage('Child') })
// 如果只有一个复用的组件,可以不用设置reuseId
.reuseId('Child')
}
}
.height("100%")
.width('100%')
}
}
@Reusable
@Component
struct Child {
@State message: ReusableMessage = new ReusableMessage('AboutToReuse');
aboutToReuse(params: Record<string, ESObject>) {
console.info("Recycle ====Child==");
this.message = params.message as ReusableMessage;
}
build() {
Column() {
Text(this.message.value)
.fontSize(30)
}
.borderWidth(1)
.height(100)
}
}
效果图
4.2 列表滚动配合LazyForEach使用
- 示例代码将CardView自定义组件标记为复用组件,List上下滑动,触发CardView复用;
- @Reusable:自定义组件被@Reusable装饰器修饰,即表示其具备组件复用的能力;
- 变量item的被@State修饰,才能更新,非@State修饰变量存在无法更新问题。
TestReusable4.ets代码
class MyDataSource implements IDataSource {
private dataArray: string[] =
["示例代码将CardView自定义组件标记为复用组件,List上下滑动,触发CardView复用;",
"自定义组件被@Reusable装饰器修饰,即表示其具备组件复用的能力;",
"变量item的被@State修饰,才能更新,非@State修饰变量存在无法更新问题。"];
private listener: DataChangeListener | undefined;
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): string {
return this.dataArray[index];
}
public pushData(data: string): void {
this.dataArray.push(data);
}
public reloadListener(): void {
this.listener?.onDataReloaded();
}
public registerDataChangeListener(listener: DataChangeListener): void {
this.listener = listener;
}
public unregisterDataChangeListener(listener: DataChangeListener): void {
this.listener = undefined;
}
}
@Entry
@Component
struct TestReusable4 {
private data: MyDataSource = new MyDataSource();
build() {
Column() {
List() {
LazyForEach(this.data, (item: string) => {
ListItem() {
CardView({ item: item })
}
}, (item: string) => item)
}
}
}
}
// 复用组件
@Reusable
@Component
export struct CardView {
@State item: string = '';
aboutToReuse(params: Record<string, Object>): void {
this.item = params.item as string;
}
build() {
Column({ space: 10 }) {
Text(this.item)
.fontSize(20)
}
.borderWidth(1)
.height(100)
}
}
效果图
4.3 if使用场景
- 示例代码将OneMoment自定义组件标记为复用组件,List上下滑动,触发OneMoment复用;
- 可以使用reuseId为复用组件分配复用组,相同reuseId的组件会在同一个复用组中复用,如果只有一个复用的组件,可以不用设置reuseId;
- 通过reuseId来标识需要复用的组件,省去重复执行if的删除重创逻辑,提高组件复用的效率和性能。
@Entry
@Component
struct withoutReuseId {
aboutToAppear(): void {
getFriendMomentFromRawfile();
}
build() {
Column() {
TopBar()
List({ space: ListConstants.LIST_SPACE }) {
LazyForEach(momentData, (moment: FriendMoment) => {
ListItem() {
OneMoment({moment: moment})
// 使用reuseId进行组件复用的控制
.reuseId((moment.image !== '') ? 'withImage' : 'noImage')
}
}, (moment: FriendMoment) => moment.id)
}
.cachedCount(Constants.CACHED_COUNT)
}
}
}
@Reusable
@Component
export struct OneMoment {
@Prop moment: FriendMoment;
build() {
Column() {
...
Text(this.moment.text)
if (this.moment.image !== '') {
Flex({ wrap: FlexWrap.Wrap }) {
Image($r(this.moment.image))
Image($r(this.moment.image))
Image($r(this.moment.image))
Image($r(this.moment.image))
}
}
...
}
}
}
4.4 Foreach使用场景
- 示例点击update,数据刷新成功,但是滑动列表,组件复用无法使用,Foreach的折叠展开属性的原因;
- 点击clear,再次update,复用成功;符合一帧内重复创建多个已被销毁的自定义组件;
// xxx.ets
class MyDataSource implements IDataSource {
private dataArray: string[] = [];
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): string {
return this.dataArray[index];
}
public pushData(data: string): void {
this.dataArray.push(data);
}
public registerDataChangeListener(listener: DataChangeListener): void {
}
public unregisterDataChangeListener(listener: DataChangeListener): void {
}
}
@Entry
@Component
struct Index {
private data: MyDataSource = new MyDataSource();
private data02: MyDataSource = new MyDataSource();
@State isShow: boolean = true;
@State dataSource: ListItemObject[] = [];
aboutToAppear() {
for (let i = 0; i < 100; i++) {
this.data.pushData(i.toString())
}
for (let i = 30; i < 80; i++) {
this.data02.pushData(i.toString())
}
}
build() {
Column() {
Row() {
Button('clear').onClick(() => {
for (let i = 1; i < 50; i++) {
let obj = new ListItemObject();
obj.id = i;
obj.uuid = Math.random().toString();
obj.isExpand = false;
this.dataSource.pop();
}
}).height(40)
Button('update').onClick(() => {
for (let i = 1; i < 50; i++) {
let obj = new ListItemObject();
obj.id = i;
obj.uuid = Math.random().toString();
obj.isExpand = false
this.dataSource.push(obj);
}
}).height(40)
}
List({ space: 10 }) {
ForEach(this.dataSource, (item: ListItemObject) => {
ListItem() {
ListItemView({
obj: item
})
}
}, (item: ListItemObject) => {
return item.uuid.toString()
})
}.cachedCount(0)
.width('100%')
.height('100%')
}
}
}
@Reusable
@Component
struct ListItemView {
@ObjectLink obj: ListItemObject;
@State item: string = '';
aboutToAppear(): void {
// 点击 update,首次进入,上下滑动,由于Foreach折叠展开属性,无法复用
console.log("=====abouTo===Appear=====ListItemView==创建了==" + this.item)
}
aboutToReuse(params: ESObject) {
this.item = params.item;
// 点击 clear,再次update,复用成功
// 符合一帧内重复创建多个已被销毁的自定义组件
console.log("=====aboutTo===Reuse====ListItemView==复用了==" + this.item)
}
build() {
Column({ space: 10 }) {
Text(`${this.obj.id}.标题`)
.fontSize(16)
.fontColor('#000000')
.padding({
top: 20,
bottom: 20,
})
if (this.obj.isExpand) {
Text('')
.fontSize(14)
.fontColor('#999999')
}
}
.width('100%')
.borderRadius(10)
.backgroundColor(Color.White)
.padding(15)
.onClick(() => {
this.obj.isExpand = !this.obj.isExpand;
})
}
}
@Observed
class ListItemObject {
uuid: string = "";
id: number = 0;
isExpand: boolean = false;
}
4.5 Grid使用场景
- 示例中使用@Reusable装饰器修饰GridItem中的自定义组件ReusableChildComponent,即表示其具备组件复用的能力;
- 使用aboutToReuse是为了让Grid在滑动时从复用缓存中加入到组件树之前触发,用于更新组件的状态变量以展示正确的内容;
- 需要注意的是无需在aboutToReuse中对@Link、@StorageLink、@ObjectLink、@Consume等自动更新值的状态变量进行更新,可能触发不必要的组件刷新。
// MyDataSource类实现IDataSource接口
class MyDataSource implements IDataSource {
private dataArray: number[] = [];
public pushData(data: number): void {
this.dataArray.push(data);
}
// 数据源的数据总量
public totalCount(): number {
return this.dataArray.length;
}
// 返回指定索引位置的数据
public getData(index: number): number {
return this.dataArray[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
}
unregisterDataChangeListener(listener: DataChangeListener): void {
}
}
@Entry
@Component
struct MyComponent {
// 数据源
private data: MyDataSource = new MyDataSource();
aboutToAppear() {
for (let i = 1; i < 1000; i++) {
this.data.pushData(i);
}
}
build() {
Column({ space: 5 }) {
Grid() {
LazyForEach(this.data, (item: number) => {
GridItem() {
// 使用可复用自定义组件
ReusableChildComponent({ item: item })
}
}, (item: string) => item)
}
.cachedCount(2) // 设置GridItem的缓存数量
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.margin(10)
.height(500)
.backgroundColor(0xFAEEE0)
}
}
}
// 自定义组件被@Reusable装饰器修饰,即标志其具备组件复用的能力
@Reusable
@Component
struct ReusableChildComponent {
@State item: number = 0;
// aboutToReuse从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容
// aboutToReuse参数类型已不支持any,这里使用Record指定明确的数据类型。Record用于构造一个对象类型,其属性键为Keys,属性值为Type
aboutToReuse(params: Record<string, number>) {
this.item = params.item;
}
build() {
Column() {
Image($r('app.media.icon'))
.objectFit(ImageFit.Fill)
.layoutWeight(1)
Text(`图片${this.item}`)
.fontSize(16)
.textAlign(TextAlign.Center)
}
.width('100%')
.height(120)
.backgroundColor(0xF9CF93)
}
}
4.6 WaterFlow使用场景
- WaterFlow滑动场景存在FlowItem及其子组件的频繁创建和销毁,可以将FlowItem中的组件封装成自定义组件,并使用@Reusable装饰器修饰,使其具备组件复用能力;
build() {
Column({ space: 2 }) {
WaterFlow() {
LazyForEach(this.datasource, (item: number) => {
FlowItem() {
// 使用可复用自定义组件
ReusableFlowItem({ item: item })
}
.onAppear(() => {
// 即将触底时提前增加数据
if (item + 20 == this.datasource.totalCount()) {
for (let i = 0; i < 100; i++) {
this.datasource.AddLastItem()
}
}
})
.width('100%')
.height(this.itemHeightArray[item % 100])
.backgroundColor(this.colors[item % 5])
}, (item: string) => item)
}
.columnsTemplate("1fr 1fr")
.columnsGap(10)
.rowsGap(5)
.backgroundColor(0xFAEEE0)
.width('100%')
.height('80%')
}
}
@Reusable
@Component
struct ReusableFlowItem {
@State item: number = 0;
// 从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容
aboutToReuse(params) {
this.item = params.item;
}
build() {
Column() {
Text("N" + this.item).fontSize(12).height('16')
Image('res/waterFlowTest (' + this.item % 5 + ').jpg')
.objectFit(ImageFit.Fill)
.width('100%')
.layoutWeight(1)
}
}
}
4.7 多种条目类型使用场景
标准型
- 复用组件之间布局完全相同;
- 示例同列表滚动中描述;
有限变化型
- 复用组件之间有不同,但是类型有限;
- 示例为复用组件显式设置两个reuseId与使用两个自定义组件进行复用;
class MyDataSource implements IDataSource {
...
}
@Entry
@Component
struct Index {
private data: MyDataSource = new MyDataSource();
aboutToAppear() {
for (let i = 0; i < 1000; i++) {
this.data.pushData(i);
}
}
build() {
Column() {
List({ space: 10 }) {
LazyForEach(this.data, (item: number) => {
ListItem() {
ReusableComponent({ item: item })
.reuseId(item % 2 === 0 ? 'ReusableComponentOne' : 'ReusableComponentTwo')
}
.backgroundColor(Color.Orange)
.width('100%')
}, (item: number) => item.toString())
}
.cachedCount(2)
}
}
}
@Reusable
@Component
struct ReusableComponent {
@State item: number = 0;
aboutToReuse(params: ESObject) {
this.item = params.item;
}
build() {
Column() {
if (this.item % 2 === 0) {
Text(`Item ${this.item} ReusableComponentOne`)
.fontSize(20)
.margin({ left: 10 })
} else {
Text(`Item ${this.item} ReusableComponentTwo`)
.fontSize(20)
.margin({ left: 10 })
}
}.margin({ left: 10, right: 10 })
}
}
组合型
- 复用组件之间有不同,情况非常多,但是拥有共同的子组件;
示例按照组合型的组件复用方式,将上述示例中的三种复用组件转变为Builder函数后,内部共同的子组件就处于同一个父组件MyComponent下;- 对这些子组件使用组件复用时,它们的缓存池也会在父组件上共享,节省组件创建时的消耗。
class MyDataSource implements IDataSource {
...
}
@Entry
@Component
struct MyComponent {
private data: MyDataSource = new MyDataSource();
aboutToAppear() {
for (let i = 0; i < 1000; i++) {
this.data.pushData(i.toString())
}
}
@Builder
itemBuilderOne(item: string) {
Column() {
ChildComponentA({ item: item })
ChildComponentB({ item: item })
ChildComponentC({ item: item })
}
}
@Builder
itemBuilderTwo(item: string) {
Column() {
ChildComponentA({ item: item })
ChildComponentC({ item: item })
ChildComponentD({ item: item })
}
}
@Builder
itemBuilderThree(item: string) {
Column() {
ChildComponentA({ item: item })
ChildComponentB({ item: item })
ChildComponentD({ item: item })
}
}
build() {
List({ space: 40 }) {
LazyForEach(this.data, (item: string, index: number) => {
ListItem() {
if (index % 3 === 0) {
this.itemBuilderOne(item)
} else if (index % 5 === 0) {
this.itemBuilderTwo(item)
} else {
this.itemBuilderThree(item)
}
}
.backgroundColor('#cccccc')
.width('100%')
.onAppear(() => {
console.log(`ListItem ${index} onAppear`);
})
}, (item: number) => item.toString())
}
.width('100%')
.height('100%')
.cachedCount(0)
}
}
@Reusable
@Component
struct ChildComponentA {
@State item: string = '';
aboutToReuse(params: ESObject) {
console.log(`ChildComponentA ${params.item} Reuse ${this.item}`);
this.item = params.item;
}
aboutToRecycle(): void {
console.log(`ChildComponentA ${this.item} Recycle`);
}
build() {
Column() {
Text(`Item ${this.item} Child Component A`)
.fontSize(20)
.margin({ left: 10 })
.fontColor(Color.Blue)
Grid() {
ForEach((new Array(20)).fill(''), (item: string,index: number) => {
GridItem() {
Image($r('app.media.startIcon'))
.height(20)
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr')
.columnsGap(10)
.width('90%')
.height(160)
}
.margin({ left: 10, right: 10 })
.backgroundColor(0xFAEEE0)
}
}
@Reusable
@Component
struct ChildComponentB {
@State item: string = '';
aboutToReuse(params: ESObject) {
this.item = params.item;
}
build() {
Row() {
Text(`Item ${this.item} Child Component B`)
.fontSize(20)
.margin({ left: 10 })
.fontColor(Color.Red)
}.margin({ left: 10, right: 10 })
}
}
@Reusable
@Component
struct ChildComponentC {
@State item: string = '';
aboutToReuse(params: ESObject) {
this.item = params.item;
}
build() {
Row() {
Text(`Item ${this.item} Child Component C`)
.fontSize(20)
.margin({ left: 10 })
.fontColor(Color.Green)
}.margin({ left: 10, right: 10 })
}
}
@Reusable
@Component
struct ChildComponentD {
@State item: string = '';
aboutToReuse(params: ESObject) {
this.item = params.item;
}
build() {
Row() {
Text(`Item ${this.item} Child Component D`)
.fontSize(20)
.margin({ left: 10 })
.fontColor(Color.Orange)
}.margin({ left: 10, right: 10 })
}
}