Intelligent Assistant
Chat with our virtual assistant to get answers promptly.
Custom components decorated by @Reusable support component reuse. When a custom component is removed from the component tree, it is stored in a cache pool. When a component node of the same type is subsequently created, the component object in the cache pool is preferentially reused, thereby avoiding repeated creation and destruction and improving performance.
The @Reusable decorator is supported since API version 10 and can be used in ArkTS.
For details about the principles, optimization methods, and use scenarios of component reuse, see Component Reuse.
Using @Reusable to decorate the custom components indicates that these components are reusable.
When developing complex UIs, rendering efficiency is a key consideration. For example, during rapid scrolling through long lists, the frequent creation and destruction of numerous list items can cause interface stuttering. Component reuse is an important method for optimizing UI performance. By reusing previously created component objects that have been detached from the tree, the frequency of component creation and destruction is reduced, thereby lowering computational overhead and improving UI rendering efficiency.
The @Reusable decorator is only applicable to custom components @Component and cannot be used together with @Builder.
@Reusable is not compatible with @ComponentV2. For @ComponentV2 reuse, use the @ReusableV2decorator instead.
import { ComponentContent } from '@kit.ArkUI';
// @Builder cannot be used together with @Reusable.
// @Reusable
@Builder
function buildCreativeLoadingDialog(closedClick: () => void) {
Crash();
}
@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)
}
}
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
private uiContext = this.getUIContext();
build() {
RelativeContainer() {
Text(this.message)
.id('Index')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => {
let contentNode = new ComponentContent(this.uiContext, wrapBuilder(buildCreativeLoadingDialog), () => {
});
this.uiContext.getPromptAction().openCustomDialog(contentNode);
})
}
.height('100%')
.width('100%')
}
}When a custom component decorated with @Reusable is reused, the aboutToReuse API is invoked recursively for the component and all its child components. Avoid modifying state variables of the parent component in its child component's aboutToReuse API. Such modification will not take effect. To update the parent component's state variables, use setTimeout to delay execution, moving the task outside the scope of component reuse.
Incorrect Usage
Modifying a parent component's state variable in its child component's aboutToReuse API:
class IncorrectBasicDataSource implements IDataSource {
private listener: DataChangeListener | undefined = undefined;
public dataArray: number[] = [];
totalCount(): number {
return this.dataArray.length;
}
getData(index: number): number {
return this.dataArray[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listener = listener;
}
unregisterDataChangeListener(listener: DataChangeListener): void {
this.listener = undefined;
}
}
@Entry
@Component
struct IncorrectIndex {
private data: IncorrectBasicDataSource = new IncorrectBasicDataSource();
aboutToAppear(): void {
for (let index = 1; index < 20; index++) {
this.data.dataArray.push(index);
}
}
build() {
List() {
LazyForEach(this.data, (item: number, index: number) => {
ListItem() {
IncorrectReuseComponent({ num: item });
}
}, (item: number, index: number) => index.toString())
}.cachedCount(0)
}
}
@Reusable
@Component
struct IncorrectReuseComponent {
@State num: number = 0;
aboutToReuse(params: ESObject): void {
this.num = params.num;
}
build() {
Column() {
Text('ReuseComponent num:' + this.num.toString())
IncorrectReuseComponentChild({ num: this.num })
Button('plus')
.onClick(() => {
// Increase by 10 on each tap.
this.num += 10;
})
}
.height(200)
}
}
@Component
struct IncorrectReuseComponentChild {
@Link num: number;
aboutToReuse(params: ESObject): void {
this.num = -1 * params.num;
}
build() {
Text('ReuseComponentChild num:' + this.num.toString())
}
}Correct Usage
To modify a parent component's state variable in a child component's aboutToReuse API, use setTimeout to move the modification outside the scope of component reuse:
class BasicDataSource implements IDataSource {
private listener: DataChangeListener | undefined = undefined;
public dataArray: number[] = [];
totalCount(): number {
return this.dataArray.length;
}
getData(index: number): number {
return this.dataArray[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listener = listener;
}
unregisterDataChangeListener(listener: DataChangeListener): void {
this.listener = undefined;
}
}
@Entry
@Component
struct Index {
private data: BasicDataSource = new BasicDataSource();
aboutToAppear(): void {
for (let index = 1; index <= 20; index++) { // Loop 20 times.
this.data.dataArray.push(index);
}
}
build() {
List() {
LazyForEach(this.data, (item: number, index: number) => {
ListItem() {
ReuseComponent({ num: item })
}
}, (item: number, index: number) => index.toString())
}.cachedCount(0)
}
}
@Reusable
@Component
struct ReuseComponent {
@State num: number = 0;
aboutToReuse(params: ESObject): void {
this.num = params.num;
}
build() {
Column() {
Text('ReuseComponent num:' + this.num.toString())
ReuseComponentChild({ num: this.num })
Button('plus')
.onClick(() => {
this.num += 10; // Increment the number by 10 on each click.
})
}
.height(200)
}
}
@Component
struct ReuseComponentChild {
@Link num: number;
aboutToReuse(params: ESObject): void {
setTimeout(() => {
this.num = -1 * params.num;
}, 1)
}
build() {
Text('ReuseComponentChild num:' + this.num.toString());
}
}Custom components decorated with @Reusable should maintain the same component structure before and after reuse. Otherwise, child components may be created or destroyed during the reuse process, reducing reuse efficiency and performance, and potentially causing abnormal application behavior.
For child components created during reuse, the framework will call the aboutToReuse method followed by the aboutToAppear method after their creation. When aboutToReuse is called, since aboutToAppear has not yet been executed and internal child components have not been created, operations that depend on the execution result of aboutToAppear or the states of internal child components in aboutToReuse will cause unexpected behavior. After calling aboutToReuse, the framework will then call aboutToAppear to initialize the component.
For scenarios where component structures differ, you must differentiate them by setting different reuseId values. For details, see Scenarios Involving Multiple Item Types.
Incorrect Usage
Component structures differ but are not distinguished via reuseId.
In the following example, first click the show/hide branch A button to recycle the component, then click the show/hide branch B button to reuse the component. The child component ReusableChildB is created during reuse, and both the aboutToReuse and aboutToAppear methods are called sequentially.
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';
@Entry
@Component
struct Index {
@State showBranchA: boolean = true;
@State showBranchB: boolean = false;
build() {
Column({ space: 5 }) {
Button('show/hide branch A')
.onClick(() => {
this.showBranchA = !this.showBranchA;
})
if (this.showBranchA) {
// The component structures differ and need to be distinguished by reuseId.
ReusableComponent({ flag: true })
}
Button('show/hide branch B')
.onClick(() => {
this.showBranchB = !this.showBranchB;
})
if (this.showBranchB) {
// The component structures differ and need to be distinguished by reuseId.
ReusableComponent({ flag: false })
}
}
}
}
@Reusable
@Component
struct ReusableComponent {
@Require @Prop flag: boolean = true;
aboutToAppear() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToAppear');
}
aboutToReuse(params: ESObject) {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToReuse');
this.flag = params.flag;
}
build() {
Column({ space: 5 }) {
Text('ReusableComponent')
if (this.flag) {
ReusableChildA()
} else {
ReusableChildB()
}
}.border({ width: 1 })
}
}
@Component
struct ReusableChildA {
aboutToAppear() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToAppear');
}
aboutToReuse() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToReuse');
}
build() {
Text('ReusableChildA')
.border({ width: 1 })
}
}
@Component
struct ReusableChildB {
aboutToAppear() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToAppear');
}
aboutToReuse() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToReuse');
}
build() {
Text('ReusableChildB')
.border({ width: 1 })
}
}Correct Usage
Component structures differ and are distinguished via reuseId.
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';
@Entry
@Component
struct Index {
@State showBranchA: boolean = true;
@State showBranchB: boolean = false;
build() {
Column({ space: 5 }) {
Button('show/hide branch A')
.onClick(() => {
this.showBranchA = !this.showBranchA;
})
if (this.showBranchA) {
ReusableComponent({ flag: true })
.reuseId('ReuseA') // Use reuseId to distinguish between reusable components with different structures.
}
Button('show/hide branch B')
.onClick(() => {
this.showBranchB = !this.showBranchB;
})
if (this.showBranchB) {
ReusableComponent({ flag: false })
.reuseId('ReuseB') // Use reuseId to distinguish between reusable components with different structures.
}
}
}
}
@Reusable
@Component
struct ReusableComponent {
@Require @Prop flag: boolean = true;
aboutToAppear() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToAppear');
}
aboutToReuse(params: ESObject) {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToReuse');
this.flag = params.flag;
}
build() {
Column({ space: 5 }) {
Text('ReusableComponent')
if (this.flag) {
ReusableChildA()
} else {
ReusableChildB()
}
}.border({ width: 1 })
}
}
@Component
struct ReusableChildA {
aboutToAppear() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToAppear');
}
aboutToReuse() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToReuse');
}
build() {
Text('ReusableChildA')
.border({ width: 1 })
}
}
@Component
struct ReusableChildB {
aboutToAppear() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToAppear');
}
aboutToReuse() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToReuse');
}
build() {
Text('ReusableChildB')
.border({ width: 1 })
}
}ComponentContent does not support passing @Reusable decorated custom components.
import { ComponentContent } from '@kit.ArkUI';
@Builder
function buildCreativeLoadingDialog(closedClick: () => void) {
Crash();
}
// The dialog box pops up correctly if @Reusable is commented out; it crashes when @Reusable is added.
@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)
}
}
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
private uiContext = this.getUIContext();
build() {
RelativeContainer() {
Text(this.message)
.id('Index')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => {
// ComponentContent is based on BuilderNode, which does not support @Reusable decorated custom components.
let contentNode = new ComponentContent(this.uiContext, wrapBuilder(buildCreativeLoadingDialog), () => {
});
this.uiContext.getPromptAction().openCustomDialog(contentNode);
})
}
.height('100%')
.width('100%')
}
}Nesting @Reusable decorators is not recommended, as it increases memory usage, reduces reuse efficiency, and complicates maintenance. Nested usage creates additional cache pools with identical tree structures, leading to low reuse efficiency. In addition, it complicates lifecycle management and makes resource and variable sharing difficult.
Repeatedly creating and removing views can trigger frequent layout calculations, which may affect frame rates. Component reuse avoids unnecessary view creation and layout recalculations, improving performance.
In the following example, the Child custom component is marked as reusable. Clicking the button updates Child, triggering reuse.
// xxx.ets
export class Message {
public value: string | undefined;
constructor(value: string) {
this.value = value;
}
}
@Entry
@Component
struct Index {
@State switch: boolean = true;
build() {
Column() {
Button('Hello')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.switch = !this.switch;
})
if (this.switch) {
// If only one reusable component is used, reuseId is optional.
Child({ message: new Message('Child') })
.reuseId('Child');
}
}
.height('100%')
.width('100%')
}
}
@Reusable
@Component
struct Child {
@State message: Message = new Message('AboutToReuse');
aboutToReuse(params: Record<string, ESObject>) {
this.message = params.message as Message;
}
build() {
Column() {
Text(this.message.value)
.fontSize(30)
}
.borderWidth(1)
.height(100)
}
}When a user scrolls a list containing a large amount of data, frequent creation and destruction of list items can cause lag and performance issues. The reuse mechanism of the List component can reuse the existing list items to improve the scrolling smoothness.
In the following example, the CardView custom component is marked as reusable. Scrolling the list up or down triggers reuse of CardView.
class MyDataSource implements IDataSource {
private dataArray: string[] = [];
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 ReuseDemo {
private data: MyDataSource = new MyDataSource();
aboutToAppear() {
for (let i = 1; i <= 1000; i++) { // Loop 1000 times.
this.data.pushData(i + '');
}
}
build() {
Column() {
List() {
LazyForEach(this.data, (item: string) => {
ListItem() {
CardView({ item: item });
}
}, (item: string) => item)
}
}
}
}
// Reusable component
@Reusable
@Component
export struct CardView {
// Only the item variable decorated with@State will be updated.
@State item: string = '';
aboutToReuse(params: Record<string, Object>): void {
this.item = params.item as string;
}
build() {
Column() {
Text(this.item)
.fontSize(30)
}
.borderWidth(1)
.height(100)
}
}In the following example, the OneMoment custom component is marked as reusable. Scrolling the list up or down triggers reuse of OneMoment. reuseId can be used to assign a reuse group for reusable components. Components with the same reuseId are reused within the same reuse group. A single reusable component does not require reuseId. Using reuseId to identify reusable components avoids repeated deletion and re-creation logic in if statements, improving reuse efficiency and performance.
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';
@Entry
@Component
struct Index {
private dataSource = new MyDataSource<FriendMoment>();
aboutToAppear(): void {
for (let i = 0; i < 20; i++) { // Loop 20 times.
let title = i + 1 + 'test_if';
// Ensure that the app.media.app_icon file is added to src/main/resources/base/media. Missing this file will trigger a blank image.
this.dataSource.pushData(new FriendMoment(i.toString(), title, 'app.media.app_icon'));
}
for (let i = 0; i < 50; i++) { // Loop 50 times.
let title = i + 1 + 'test_if';
this.dataSource.pushData(new FriendMoment(i.toString(), title, ''));
}
}
build() {
Column() {
// TopBar()
List({ space: 3 }) {
LazyForEach(this.dataSource, (moment: FriendMoment) => {
ListItem() {
// Use reuseId to control component reuse.
OneMoment({ moment: moment })
.reuseId((moment.image !== '') ? 'withImage' : 'noImage');
}
}, (moment: FriendMoment) => moment.id)
}
.cachedCount(0)
}
}
}
class FriendMoment {
public id: string = '';
public text: string = '';
public title: string = '';
public image: string = '';
public answers: Array<ResourceStr> = [];
constructor(id: string, title: string, image: string) {
this.text = id;
this.title = title;
this.image = image;
}
}
@Reusable
@Component
export struct OneMoment {
@Prop moment: FriendMoment;
// Only components with the same reuseId trigger reuse.
aboutToReuse(params: ESObject): void {
hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToReuse====OneMoment==reused==' + this.moment.text);
}
build() {
Column() {
Text(this.moment.text)
// Conditional rendering with if
if (this.moment.image !== '') {
Flex({ wrap: FlexWrap.Wrap }) {
Image($r(this.moment.image)).height(50).width(50);
Image($r(this.moment.image)).height(50).width(50);
Image($r(this.moment.image)).height(50).width(50);
Image($r(this.moment.image)).height(50).width(50);
}
}
}
}
}
class BasicDataSource<T> implements IDataSource {
private listeners: DataChangeListener[] = [];
private originDataArray: T[] = [];
public totalCount(): number {
return 0;
}
public getData(index: number): T {
return this.originDataArray[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
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);
}
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
});
}
}
export class MyDataSource<T> extends BasicDataSource<T> {
private dataArray: T[] = [];
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): T {
return this.dataArray[index];
}
public pushData(data: T): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
}When ForEach is used to create reusable custom components, the full-expansion behavior of the ForEach rendering control syntax prevents the reusable components from being reused. In the example: clicking update successfully refreshes the data, but when scrolling the list, ListItemView cannot be reused. Clicking clear and then clicking update again allows ListItemView to be reused successfully, because multiple custom components that have already been destroyed are re-created within a single frame.
// xxx.ets
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';
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++) { // Loop 100 times.
this.data.pushData(i.toString());
}
for (let i = 30; i < 80; i++) { // Loop 50 times.
this.data02.pushData(i.toString());
}
}
build() {
Column() {
Row() {
Button('clear').onClick(() => {
for (let i = 1; i <= 50; i++) { // Loop 50 times.
this.dataSource.pop();
}
}).height(40)
Button('update').onClick(() => {
for (let i = 1; i <= 50; i++) { // Loop 50 times.
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 {
// Click update. When entering for the first time, swipe up and down. Reuse is not possible due to the ForEach collapse/expand attribute.
hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToAppear=====ListItemView==created==' + this.item);
}
aboutToReuse(params: ESObject) {
this.item = params.item;
// Reuse succeeds after clear and update are clicked
// (which recreates destroyed components in one frame).
hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToReuse====ListItemView==reused==' + this.item);
}
build() {
Column({ space: 10 }) {
Text(`${this.obj.id}.Title`)
.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 {
public uuid: string = '';
public id: number = 0;
public isExpand: boolean = false;
}In the following example, the @Reusable decorator is used to decorate the custom component ReusableChildComponent in GridItem, indicating that the component can be reused.
The aboutToReuse API is triggered when the component is obtained from the reuse cache and added to the component tree during grid scrolling. This allows you to update the component's state variables to display correct content.
Note: There is no need to update state variables that automatically synchronize values (such as variables decorated with @Link, @StorageLink, @ObjectLink, or @Consume) in aboutToReuse, as this may trigger unnecessary component re-renders.
// Class MyDataSource implements the IDataSource API.
class MyDataSource implements IDataSource {
private dataArray: number[] = [];
public pushData(data: number): void {
this.dataArray.push(data);
}
// Total number of items in the data source.
public totalCount(): number {
return this.dataArray.length;
}
// Return data at the specified index.
public getData(index: number): number {
return this.dataArray[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
}
unregisterDataChangeListener(listener: DataChangeListener): void {
}
}
@Entry
@Component
struct MyComponent {
// Data source.
private data: MyDataSource = new MyDataSource();
aboutToAppear() {
for (let i = 1; i <= 1000; i++) { // Loop 1000 times.
this.data.pushData(i);
}
}
build() {
Column({ space: 5 }) {
Grid() {
LazyForEach(this.data, (item: number) => {
GridItem() {
// Use the reusable custom component.
ReusableChildComponent({ item: item });
}
}, (item: number) => item.toString())
}
.cachedCount(2) // Set the number of cached GridItem components.
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.margin(10)
.height(500)
.backgroundColor(0xFAEEE0)
}
}
}
@Reusable
@Component
struct ReusableChildComponent {
@State item: number = 0;
// Called before the component is added to the component tree from the reuse cache. The component's state variable can be updated here to display the correct content.
// The aboutToReuse parameter no longer supports the any type. Record is used to specify explicit data types, constructing an object type where property keys are strings and property values are numbers.
aboutToReuse(params: Record<string, number>) {
this.item = params.item;
}
build() {
Column() {
// Ensure that the app.media.app_icon file is added to src/main/resources/base/media. Missing this file will trigger a runtime error.
Image($r('app.media.app_icon'))
.objectFit(ImageFit.Fill)
.layoutWeight(1)
Text(`Image${this.item}`)
.fontSize(16)
.textAlign(TextAlign.Center)
}
.width('100%')
.height(120)
.backgroundColor(0xF9CF93)
}
}For WaterFlow scrolling scenarios where FlowItem and its child components are frequently created and destroyed, you can encapsulate components in FlowItem into a custom component and decorate it with @Reusable to implement component reuse.
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';
class WaterFlowDataSource implements IDataSource {
private dataArray: number[] = [];
private listeners: DataChangeListener[] = [];
constructor() {
for (let i = 0; i < 60; i++) { // Loop 60 times.
this.dataArray.push(i);
}
}
// Obtain data at the specified index.
public getData(index: number): number {
return this.dataArray[index];
}
// Notify listeners of new data addition.
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
});
}
// Obtain the total number of data items.
public totalCount(): number {
return this.dataArray.length;
}
// Register a data change listener.
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
// Unregister the data change listener.
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
// Add an item to the end of the data array.
public addLastItem(): void {
this.dataArray.splice(this.dataArray.length, 0, this.dataArray.length);
this.notifyDataAdd(this.dataArray.length - 1);
}
}
@Reusable
@Component
struct ReusableFlowItem {
@State item: number = 0;
// Called before the component is added to the component tree from the reuse cache. The component's state variable can be updated here to display the correct content.
aboutToReuse(params: ESObject) {
this.item = params.item;
hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToReuse====FlowItem==reused==' + this.item);
}
aboutToRecycle(): void {
hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToRecycle====FlowItem==recycled==' + this.item);
}
build() {
// Ensure that the app.media.app_icon file is added to src/main/resources/base/media. Missing this file will trigger a runtime error.
Column() {
Text('N' + this.item).fontSize(24).height('26').margin(10);
Image($r('app.media.app_icon'))
.objectFit(ImageFit.Cover)
.width(50)
.height(50);
}
}
}
@Entry
@Component
struct Index {
@State minSize: number = 50; // Minimum value: 50.
@State maxSize: number = 80; // Maximum value: 80.
@State colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F];
scroller: Scroller = new Scroller();
dataSource: WaterFlowDataSource = new WaterFlowDataSource();
private itemWidthArray: number[] = [];
private itemHeightArray: number[] = [];
// Calculate random size for flow items.
getSize() {
let ret = Math.floor(Math.random() * this.maxSize);
return (ret > this.minSize ? ret : this.minSize);
}
// Generate size arrays for flow items.
getItemSizeArray() {
for (let i = 0; i < 100; i++) { // Loop 100 times.
this.itemWidthArray.push(this.getSize());
this.itemHeightArray.push(this.getSize());
}
}
aboutToAppear() {
this.getItemSizeArray();
}
build() {
Stack({ alignContent: Alignment.TopStart }) {
Column({ space: 2 }) {
Button('back top')
.height('5%')
.onClick(() => {
// Scroll to top when the component is clicked.
this.scroller.scrollEdge(Edge.Top);
})
WaterFlow({ scroller: this.scroller }) {
LazyForEach(this.dataSource, (item: number) => {
FlowItem() {
ReusableFlowItem({ item: item })
}.onAppear(() => {
if (item + 20 == this.dataSource.totalCount()) { // Threshold: 20.
for (let i = 0; i < 50; i++) { // Loop 50 times.
this.dataSource.addLastItem();
}
}
})
})
}
}
}
}
}For Swiper scrolling scenarios where child components are frequently created and destroyed, you can encapsulate the child components into a custom component and decorate it with @Reusable to implement component reuse.
@Entry
@Component
struct Index {
private dataSource = new MyDataSource<Question>();
aboutToAppear(): void {
for (let i = 0; i < 1000; i++) { // Loop 1000 times.
let title = i + 1 + 'test_swiper';
let answers = ['test1', 'test2', 'test3', 'test4'];
// Ensure that the app.media.app_icon file is added to src/main/resources/base/media. Missing this file will trigger a runtime error.
this.dataSource.pushData(new Question(i.toString(), title, $r('app.media.app_icon'), answers));
}
}
build() {
Column({ space: 5 }) {
Swiper() {
LazyForEach(this.dataSource, (item: Question) => {
QuestionSwiperItem({ itemData: item });
}, (item: Question) => item.id)
}
}
.width('100%')
.margin({ top: 5 })
}
}
class Question {
public id: string = '';
public title: ResourceStr = '';
public image: ResourceStr = '';
public answers: Array<ResourceStr> = [];
constructor(id: string, title: ResourceStr, image: ResourceStr, answers: Array<ResourceStr>) {
this.id = id;
this.title = title;
this.image = image;
this.answers = answers;
}
}
@Reusable
@Component
struct QuestionSwiperItem {
@State itemData: Question | null = null;
aboutToReuse(params: Record<string, Object>): void {
this.itemData = params.itemData as Question;
}
build() {
Column() {
Text(this.itemData?.title)
.fontSize(18)
.fontColor($r('sys.color.ohos_id_color_primary'))
.alignSelf(ItemAlign.Start)
.margin({
top: 10,
bottom: 16
})
Image(this.itemData?.image)
.borderRadius(12)
.objectFit(ImageFit.Contain)
.margin({
bottom: 16
})
.height(80)
.width(80)
Column({ space: 16 }) {
ForEach(this.itemData?.answers, (item: Resource) => {
Text(item)
.fontSize(16)
.fontColor($r('sys.color.ohos_id_color_primary'))
}, (item: ResourceStr) => JSON.stringify(item))
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding({
left: 16,
right: 16
})
}
}
class BasicDataSource<T> implements IDataSource {
private listeners: DataChangeListener[] = [];
private originDataArray: T[] = [];
public totalCount(): number {
return 0;
}
public getData(index: number): T {
return this.originDataArray[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
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);
}
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
});
}
}
export class MyDataSource<T> extends BasicDataSource<T> {
private dataArray: T[] = [];
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): T {
return this.dataArray[index];
}
public pushData(data: T): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
}For list scrolling scenarios where the ListItemGroup component is used, you can encapsulate child components in ListItem that need to be destroyed and re-created into a custom component and decorate it with @Reusable to implement component reuse.
@Entry
@Component
struct ListItemGroupAndReusable {
data: DataSrc2 = new DataSrc2();
@Builder
itemHead(text: string) {
Text(text)
.fontSize(20)
.backgroundColor(0xAABBCC)
.width('100%')
.padding(10)
}
aboutToAppear() {
for (let i = 0; i < 10000; i++) { // Loop 10000 times.
let data1 = new DataSrc1();
for (let j = 0; j < 12; j++) { // Loop 12 times.
data1.data.push(`Test item data: ${i} - ${j}`);
}
this.data.data.push(data1);
}
}
build() {
Stack() {
List() {
LazyForEach(this.data, (item: DataSrc1, index: number) => {
ListItemGroup({ header: this.itemHead(index.toString()) }) {
LazyForEach(item, (ii: string, index: number) => {
ListItem() {
Inner({ str: ii });
}
})
}
.width('100%')
.height('60vp')
})
}
}
.width('100%')
.height('100%')
}
}
@Reusable
@Component
struct Inner {
@State str: string = '';
aboutToReuse(param: ESObject) {
this.str = param.str;
}
build() {
Text(this.str);
}
}
class DataSrc1 implements IDataSource {
public listeners: DataChangeListener[] = [];
public data: string[] = [];
public totalCount(): number {
return this.data.length;
}
public getData(index: number): string {
return this.data[index];
}
// Called by the framework to add a listener to the data source for LazyForEach.
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
// Called by the framework to remove the listener from the data source for the corresponding LazyForEach component.
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
// Notify LazyForEach that all child components need to be reloaded.
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
});
}
// Notify LazyForEach that a child component needs to be added at the specified index.
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
});
}
// Notify LazyForEach that the data item at the specified index has changed and the child component needs to be rebuilt.
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
});
}
// Notify LazyForEach that the child component at the specified index needs to be deleted.
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
});
}
// Notify LazyForEach that data needs to be swapped between the from and to positions.
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to);
});
}
}
class DataSrc2 implements IDataSource {
public listeners: DataChangeListener[] = [];
public data: DataSrc1[] = [];
public totalCount(): number {
return this.data.length;
}
public getData(index: number): DataSrc1 {
return this.data[index];
}
// Called by the framework to add a listener to the data source for LazyForEach.
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
// Called by the framework to remove the listener from the data source for the corresponding LazyForEach component.
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
// Notify LazyForEach that all child components need to be reloaded.
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
});
}
// Notify LazyForEach that a child component needs to be added at the specified index.
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
});
}
// Notify LazyForEach that the data item at the specified index has changed and the child component needs to be rebuilt.
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
});
}
// Notify LazyForEach that the child component at the specified index needs to be deleted.
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
});
}
// Notify LazyForEach that data needs to be swapped between the from and to positions.
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to);
});
}
}Standard
Reusable components have the same layout. For implementation examples, see the description in the list scrolling sections.
Limited Variation
There are differences between reusable components, but the number of types is limited. For example, reuse can be achieved by explicitly setting two reuseId values or using two custom components.
class LimitedMyDataSource implements IDataSource {
private dataArray: number[] = [];
private listener: DataChangeListener | undefined;
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): number {
return this.dataArray[index];
}
public pushData(data: number): 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 LimitedIndex {
private data: LimitedMyDataSource = new LimitedMyDataSource();
aboutToAppear() {
for (let i = 0; i < 1000; i++) { // Loop 1000 times.
this.data.pushData(i);
}
}
build() {
Column() {
List({ space: 10 }) {
LazyForEach(this.data, (item: number) => {
ListItem() {
ReusableComponent({ item: item })
// Set two reuseId values with limited variations.
.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() {
// Render according to type differences inside the component.
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 })
}
}Composite
There are multiple differences between reusable components, but they usually share common child components. In the example, after three reusable components are converted into Builder functions in a combined manner, the internal shared child components will be uniformly placed under the parent component MyComponent. The reuse cache is shared at the parent component level for child component reuse, reducing resource consumption during component creation.
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';
const NUMBER3 = 3;
const NUMBER5 = 5;
class MyDataSource implements IDataSource {
private dataArray: string[] = [];
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 MyComponent {
private data: MyDataSource = new MyDataSource();
aboutToAppear() {
for (let i = 0; i < 1000; i++) { // Loop 1000 times.
this.data.pushData(i.toString());
}
}
// The reusable component implementation of itemBuilderOne is not shown. Below is the Builder version after conversion.
@Builder
itemBuilderOne(item: string) {
Column() {
ChildComponentA({ item: item });
ChildComponentB({ item: item });
ChildComponentC({ item: item });
}
}
// Builder version of itemBuilderTwo after conversion.
@Builder
itemBuilderTwo(item: string) {
Column() {
ChildComponentA({ item: item });
ChildComponentC({ item: item });
ChildComponentD({ item: item });
}
}
// Builder version of itemBuilderThree after conversion.
@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 % NUMBER3 === 0) {
this.itemBuilderOne(item);
} else if (index % NUMBER5 === 0) {
this.itemBuilderTwo(item);
} else {
this.itemBuilderThree(item);
}
}
.backgroundColor('#cccccc')
.width('100%')
.onAppear(() => {
hilog.info(DOMAIN, TAG, BUNDLE + `ListItem ${index} onAppear`);
})
}, (item: string) => item)
}
.width('100%')
.height('100%')
.cachedCount(0)
}
}
@Reusable
@Component
struct ChildComponentA {
@State item: string = '';
aboutToReuse(params: ESObject) {
hilog.info(DOMAIN, TAG, BUNDLE + `ChildComponentA ${params.item} Reuse ${this.item}`);
this.item = params.item;
}
aboutToRecycle(): void {
hilog.info(DOMAIN, TAG, BUNDLE + `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() {
// Ensure that the app.media.startIcon file is added to src/main/resources/base/media. Missing this file will trigger a runtime error.
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 })
}
}