openharmony 鸿蒙 arkts-lazyforeach-repeat-migration-guide

2026-08-25 浏览 (1)

LazyForEach迁移Repeat指南

Repeat是ArkUI在API version 12中新引入的循环渲染组件,相比LazyForEach具有更简洁的API、更丰富的功能以及更强的性能优化能力。本指南帮助开发者将LazyForEach平滑地迁移到Repeat。

基础用法迁移

数据首次渲染

LazyForEach示例

LazyForEach根据数据源循环渲染子组件。

示例1中,在容器组件List中使用LazyForEach,并基于数据源循环渲染出了一系列Text子组件。

示例1 - 迁移前

/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 */

class MyDataSource extends BasicDataSource {
  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);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

@Entry
@Component
struct MyComponent {
  private data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 0; i <= 20; i++) {
      this.data.pushData(`Hello ${i}`);
    }
  }

  build() {
    List({ space: 3 }) {
      LazyForEach(this.data, (item: string) => {
        ListItem() {
          Row() {
            Text(item).fontSize(50)
              .onAppear(() => {
                console.info(`appear: ${item}`);
              })
          }.margin({ left: 10, right: 10 })
        }
      }, (item: string) => item)
    }.cachedCount(5)
  }
}

以上是一个典型的使用LazyForEach循环渲染子组件的场景,下面将介绍如何将此示例迁移至Repeat。

迁移步骤

  1. 使用状态管理V2装饰器。

    Repeat推荐和状态管理V2装饰器配合使用(懒加载模式下只支持和状态管理V2装饰器配合使用)。如果之前使用的是状态管理V1装饰器,需要修改为状态管理V2装饰器。

    // 迁移前 - LazyForEach
    @Component // 状态管理V1
    struct MyComponent {
      build() {
        // ...
        LazyForEach(...)
        // ...
      }
      // ...其他属性、方法
    }
      
    // 迁移后 - Repeat
    @ComponentV2 // 状态管理V2
    struct MyComponent {
      build() {
        // ...
        Repeat(...)
        // ...
      }
      // ...其他属性、方法
    }
    
  2. 迁移数据源。

    LazyForEach使用专用的数据结构IDataSource作为数据源。迁移至Repeat后,不再使用IDataSource作为数据源,而是使用状态管理V2装饰的数组作为数据源。

    // 迁移前 - LazyForEach
    class MyDataSource implements IDataSource {
      private dataArray: string[] = [];
      
      public totalCount(): number {
        return this.dataArray.length;
      }
      
      public getData(index: number): string {
        return this.dataArray[index];
      }
      
      // ...其他方法
    }
    
    // 迁移后 - Repeat
    @Local data: Array<string> = [];
    
  3. 迁移组件生成函数和键值生成函数。

    LazyForEach与Repeat均通过组件生成函数,为每一项数据创建一个子组件;通过键值生成函数,为每一项数据生成一个唯一的键值。
    从LazyForEach迁移至Repeat时,两者的语法存在差异。Repeat需要在.each().template()中设置组件生成函数,在.key()中设置键值生成函数。

    // 迁移前 - LazyForEach
    List() {
      LazyForEach(
        this.data, // 数据源
        (item: string, index: number) => { // 组件生成函数
          ListItem() {
            Text(item)
          }
        },
        (item: string, index: number) => item // 键值生成函数
      )
    }
    
    // 迁移后 - Repeat
    List() {
      Repeat<string>(this.data) // 数据源
        .each((repeatItem: RepeatItem<string>) => { // 组件生成函数
          ListItem() {
            Text(repeatItem.item)
          }
        })
        .key((item: string, index: number) => item) // 键值生成函数
    }
    
  4. 配置懒加载功能。

    Repeat具有懒加载和全量加载两种模式。

    • 全量加载模式渲染所有子节点(对标ForEach)。
    • 懒加载模式动态渲染屏幕区域和预加载区域内的子节点(需要与容器组件配合使用,对标LazyForEach)。

    从LazyForEach迁移至Repeat时,需要调用virtualScroll属性,使能懒加载。

    // 迁移前 - LazyForEach
    LazyForEach(data, (item) => {...}, (item) => item)
    
    // 迁移后 - Repeat
    Repeat(data)
      .virtualScroll() // 使能懒加载
    

迁移后代码

通过以上步骤,可以将示例1从LazyForEach迁移至Repeat,迁移后的完整示例如下所示。

示例1 - 迁移后

@Entry
@ComponentV2
  // 使用状态管理V2
struct MyComponent {
  @Local data: Array<string> = []; // 数据源为状态管理V2装饰的数组

  aboutToAppear() {
    for (let i = 0; i <= 20; i++) {
      this.data.push(`Hello ${i}`);
    }
  }

  build() {
    List({ space: 3 }) {
      Repeat(this.data) // 使用Repeat
        .each((repeatItem: RepeatItem<string>) => { // 组件生成函数
          ListItem() {
            Row() {
              Text(repeatItem.item).fontSize(50)
                .onAppear(() => {
                  console.info(`appear: ${repeatItem.item}`);
                })
            }.margin({ left: 10, right: 10 })
          }
        })
        .key((item: string) => item) // 键值生成函数
        .virtualScroll() // 使能懒加载
    }.cachedCount(5)
  }
}

运行后界面如下图所示。

LazyForEach-Repeat-Migration-Demo-1

数据更新操作

LazyForEach示例

当LazyForEach的数据源发生变化时,开发者需要根据数据源的变化情况调用DataChangeListener对应的接口,通知LazyForEach做相应的更新。主要的数据操作包括:添加数据、删除数据、交换数据、修改单个数据、修改多个数据、精准批量修改数据。

示例2演示了主要的数据操作。

示例2 - 迁移前

/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 */

class MyDataSource extends BasicDataSource {
  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);
    this.notifyDataAdd(this.dataArray.length - 1);
  }

  // 删除数据
  public deleteData(index: number): void {
    this.dataArray.splice(index, 1);
    this.notifyDataDelete(index);
  }

  // 交换数据
  public moveData(from: number, to: number): void {
    let temp: string = this.dataArray[from];
    this.dataArray[from] = this.dataArray[to];
    this.dataArray[to] = temp;
    this.notifyDataMove(from, to);
  }

  // 修改单个数据
  public changeData(index: number, data: string): void {
    this.dataArray.splice(index, 1, data);
    this.notifyDataChange(index);
  }

  // 修改多个数据
  public modifyAllData(): void {
    this.dataArray = this.dataArray.map((item: string) => {
        return 'Changed ' + item;
    });
    this.notifyDataReload();
  }
}

@Entry
@Component
struct MyComponent {
  private data: MyDataSource = new MyDataSource();
  private count: number = 0;

  aboutToAppear() {
    for (let i = 0; i <= 10; i++) {
      this.data.pushData(`Hello ${i}`);
    }
  }

  build() {
    Column({ space: 3 }) {
      // 点击追加子组件
      Button('Add new item')
        .onClick(() => {
          this.data.pushData(`New item ${this.count++}`);
        })
      // 点击删除子组件
      Button('Delete item 0')
        .onClick(() => {
          this.data.deleteData(0);
        })
      // 点击交换子组件
      Button('Swap item 0 and item 1')
        .onClick(() => {
          this.data.moveData(0, 1);
        })
      // 点击修改单个子组件
      Button('Change item 0')
        .onClick(() => {
          this.data.changeData(0, `Changed item ${this.count++}`);
        })
      // 点击修改多个子组件
      Button('Change all items')
        .onClick(() => {
          this.data.modifyAllData();
        })
      List({ space: 3 }) {
        LazyForEach(this.data, (item: string) => {
          ListItem() {
            Row() {
              Text(item).fontSize(25)
            }
          }
        }, (item: string) => item)
      }.cachedCount(5)
    }
  }
}

以上是一个典型的更新数据后LazyForEach重新渲染子组件的场景,下面将介绍如何将此示例迁移至Repeat。

迁移步骤

  1. 迁移准备。

    根据数据首次渲染小节中的步骤,将LazyForEach替换为Repeat。

    1. 使用状态管理V2装饰器。
    2. 迁移数据源。
    3. 迁移组件生成函数与键值生成函数。
    4. 使能懒加载。
  2. 迁移数据源修改方式。

    • 对于LazyForEach,在修改数据源后需要调用对应的接口通知其更新。
    • 对于Repeat,由状态管理V2监听其数据源变化,并触发更新。因此,开发者直接修改数据源即可,无需其他额外操作。
    // 以修改单个数据为例
    // 迁移前 - LazyForEach
    class MyDataSource implements IDataSource {
      private dataArray: string[] = [];
      
      public changeData(index: number, newData: string): void {
        this.dataArray.splice(index, 1, data);
        this.notifyDataChange(index);
      }
      
      // ...其他方法
    }
    
    // 迁移后 - Repeat
    this.data.splice(index, 1, data);
    

    其他数据更新操作,如添加数据、删除数据、交换数据等,与以上方法类似,可通过直接修改数据源数组实现。

迁移后代码

迁移后的完整示例如下。

示例2 - 迁移后

@Entry
@ComponentV2
struct MyComponent {
  @Local data: Array<string> = [];
  private count: number = 0;

  aboutToAppear() {
    for (let i = 0; i <= 10; i++) {
      this.data.push(`Hello ${i}`);
    }
  }

  build() {
    Column({ space: 3 }) {
      // 点击追加子组件
      Button('Add new item')
        .onClick(() => {
          this.data.push(`New item ${this.count++}`);
        })
      // 点击删除子组件
      Button('Delete item 0')
        .onClick(() => {
          this.data.splice(0, 1);
        })
      // 点击交换子组件
      Button('Swap item 0 and item 1')
        .onClick(() => {
          let temp: string = this.data[0];
          this.data[0] = this.data[1];
          this.data[1] = temp;
        })
      // 点击修改单个子组件
      Button('Change item 0')
        .onClick(() => {
          this.data.splice(0, 1, `Changed item ${this.count++}`);
        })
      // 点击修改多个子组件
      Button('Change all items')
        .onClick(() => {
          this.data = this.data.map((item: string) => {
            return 'Changed ' + item;
          });
        })
      List({ space: 3 }) {
        Repeat(this.data)
          .each((repeatItem: RepeatItem<string>) => {
            ListItem() {
              Row() {
                Text(repeatItem.item).fontSize(25)
              }
            }
          })
          .key((item: string) => item)
          .virtualScroll()
      }.cachedCount(5)
    }
  }
}

运行后界面如下图所示。

LazyForEach-Repeat-Migration-Demo-2

典型场景迁移

修改数据子属性

LazyForEach示例

LazyForEach可以使用@Observed与@ObjectLink装饰器实现对数据子属性的观测。当有数据子属性发生变化时,仅更新使用了该子属性的组件,从而提高性能。

示例3演示了对子属性的观测。

示例3 - 迁移前

/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 */

class MyDataSource extends BasicDataSource {
  private dataArray: StringData[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): StringData {
    return this.dataArray[index];
  }

  public pushData(data: StringData): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

@Observed
class StringData {
  message: string;

  constructor(message: string) {
    this.message = message;
  }
}

@Entry
@Component
struct MyComponent {
  private data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 0; i <= 20; i++) {
      this.data.pushData(new StringData(`Hello ${i}`));
    }
  }

  build() {
    List({ space: 3 }) {
      LazyForEach(this.data, (item: StringData, index: number) => {
        ListItem() {
          ChildComponent({ data: item })
        }
        .onClick(() => {
          item.message += '0';
        })
      }, (item: StringData, index: number) => index.toString())
    }.cachedCount(5)
  }
}

@Component
struct ChildComponent {
  @ObjectLink data: StringData;

  build() {
    Row() {
      Text(this.data.message).fontSize(50)
        .onAppear(() => {
          console.info(`appear: ${this.data.message}`);
        })
    }.margin({ left: 10, right: 10 })
  }
}

迁移Repeat

Repeat需要和状态管理V2一起使用,状态管理V2提供了@ObservedV2和@Trace装饰器对子属性进行深度观测。迁移时,需要将@Observed和@ObjectLink装饰器迁移至@ObservedV2和@Trace装饰器。

迁移后的示例如下所示。

示例3 - 迁移后

@ObservedV2
class StringData {
  @Trace message: string; // 观测子属性

  constructor(message: string) {
    this.message = message;
  }
}

@Entry
@ComponentV2
struct MyComponent {
  @Local data: StringData[] = [];

  aboutToAppear() {
    for (let i = 0; i <= 20; i++) {
      this.data.push(new StringData(`Hello ${i}`));
    }
  }

  build() {
    List({ space: 3 }) {
      Repeat(this.data)
        .each((repeatItem) => {
          ListItem() {
            Text(repeatItem.item.message).fontSize(50)
              .onAppear(() => {
                console.info(`appear: ${repeatItem.item.message}`);
              })
          }
          .onClick(() => {
            repeatItem.item.message += '0';
          })
        })
        .key((item: StringData, index: number) => index.toString())
        .virtualScroll()
    }.cachedCount(5)
  }
}

运行后界面如下图所示。

LazyForEach-Repeat-Migration-Demo-3

状态管理V2观测组件内部状态

LazyForEach示例

状态管理V2的@Local装饰器提供了观测自定义组件内部变量的能力。被@Local装饰的变量发生变化时,会通知LazyForEach更新对应的组件。

示例4演示了在LazyForEach中使用@Local装饰器观测数据变化,触发组件更新。

示例4 - 迁移前

/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 */

class MyDataSource extends BasicDataSource {
  private dataArray: StringData[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): StringData {
    return this.dataArray[index];
  }

  public pushData(data: StringData): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

@ObservedV2
class StringData {
  @Trace message: string;

  constructor(message: string) {
    this.message = message;
  }
}

@Entry
@ComponentV2
struct MyComponent {
  data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 0; i <= 20; i++) {
      this.data.pushData(new StringData(`Hello ${i}`));
    }
  }

  build() {
    List({ space: 3 }) {
      LazyForEach(this.data, (item: StringData, index: number) => {
        ListItem() {
          Row() {
            Text(item.message).fontSize(50)
              .onClick(() => {
                // 修改@ObservedV2装饰类中@Trace装饰的变量,触发刷新此处Text组件
                item.message += '!';
              })
            ChildComponent()
          }
        }
      }, (item: StringData, index: number) => index.toString())
    }.cachedCount(5)
  }
}

@ComponentV2
struct ChildComponent {
  @Local message: string = '?';

  build() {
    Row() {
      Text(this.message).fontSize(50)
        .onClick(() => {
          // 修改@Local装饰的变量,触发刷新此处Text组件
          this.message += '?';
        })
    }
  }
}

迁移Repeat

Repeat本身支持与状态管理V2联合使用,将LazyForEach相关代码修改为Repeat后,即可实现对组件内部状态变量的观测。

迁移后的示例如下所示。

示例4 - 迁移后

@ObservedV2
class StringData {
  @Trace message: string;

  constructor(message: string) {
    this.message = message;
  }
}

@Entry
@ComponentV2
struct MyComponent {
  @Local data: StringData[] = [];

  aboutToAppear() {
    for (let i = 0; i <= 20; i++) {
      this.data.push(new StringData(`Hello ${i}`));
    }
  }

  build() {
    List({ space: 3 }) {
      Repeat(this.data)
        .each((repeatItem) => {
          ListItem() {
            Row() {
              Text(repeatItem.item.message).fontSize(50)
                .onClick(() => {
                  // 修改@ObservedV2装饰类中@Trace装饰的变量,触发刷新此处Text组件
                  repeatItem.item.message += '!';
                })
              ChildComponent()
            }
          }
        })
        .key((item: StringData, index: number) => index.toString())
        .virtualScroll()
    }.cachedCount(5)
  }
}

@ComponentV2
struct ChildComponent {
  @Local message: string = '?';

  build() {
    Row() {
      Text(this.message).fontSize(50)
        .onClick(() => {
          // 修改@Local装饰的变量,触发刷新此处Text组件
          this.message += '?';
        })
    }
  }
}

运行后界面如下图所示。

LazyForEach-Repeat-Migration-Demo-4

状态管理V2观测组件外部输入

LazyForEach示例

状态管理V2的@Param装饰器提供了观测自定义组件外部输入变量的能力,可以实现父子组件间的数据同步。将父组件的变量传递给子组件,并用@Param装饰,当父组件变量发生变化时,会通知对应的组件更新。

示例5演示了在LazyForEach中使用@Param装饰器观测数据变化,触发组件更新。

示例5 - 迁移前

/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 */

class MyDataSource extends BasicDataSource {
  private dataArray: StringData[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): StringData {
    return this.dataArray[index];
  }

  public pushData(data: StringData): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

@ObservedV2
class StringData {
  @Trace message: string;

  constructor(message: string) {
    this.message = message;
  }
}

@Entry
@ComponentV2
struct MyComponent {
  data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 0; i <= 20; i++) {
      this.data.pushData(new StringData(`Hello ${i}`));
    }
  }

  build() {
    List({ space: 3 }) {
      LazyForEach(this.data, (item: StringData, index: number) => {
        ListItem() {
          ChildComponent({ data: item.message }) // 向自定义组件内传入变量
            .onClick(() => {
              item.message += '!';
            })
        }
      }, (item: StringData, index: number) => index.toString())
    }.cachedCount(5)
  }
}

@ComponentV2
struct ChildComponent {
  @Param @Require data: string = ''; // 接收来自外部的变量

  build() {
    Row() {
      Text(this.data).fontSize(50)
    }
  }
}

迁移Repeat

Repeat本身支持与状态管理V2联合使用,将LazyForEach相关代码修改为Repeat后,即可实现对组件外部输入状态变量的观测。

迁移后的示例如下所示。

示例5 - 迁移后

@ObservedV2
class StringData {
  @Trace message: string;

  constructor(message: string) {
    this.message = message;
  }
}

@Entry
@ComponentV2
struct MyComponent {
  @Local data: StringData[] = [];

  aboutToAppear() {
    for (let i = 0; i <= 20; i++) {
      this.data.push(new StringData(`Hello ${i}`));
    }
  }

  build() {
    List({ space: 3 }) {
      Repeat(this.data)
        .each((repeatItem) => {
          ListItem() {
            ChildComponent({ data: repeatItem.item.message }) // 向自定义组件内传入变量
              .onClick(() => {
                repeatItem.item.message += '!';
              })
          }
        })
        .key((item: StringData, index: number) => index.toString())
        .virtualScroll()
    }.cachedCount(5)
  }
}

@ComponentV2
struct ChildComponent {
  @Param @Require data: string = ''; // 接收来自外部的变量

  build() {
    Row() {
      Text(this.data).fontSize(50)
    }
  }
}

运行后界面如下图所示。

LazyForEach-Repeat-Migration-Demo-5

拖拽排序

LazyForEach示例

LazyForEach的onMove属性提供了拖拽排序能力。

示例6为典型用例。

示例6 - 迁移前

/** BasicDataSource代码见文档末尾BasicDataSource示例代码: string类型数组的BasicDataSource代码 */

class MyDataSource extends BasicDataSource {
  private dataArray: string[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): string {
    return this.dataArray[index];
  }

  public moveDataWithoutNotify(from: number, to: number): void {
    let tmp = this.dataArray.splice(from, 1);
    this.dataArray.splice(to, 0, tmp[0]);
  }

  public pushData(data: string): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

@Entry
@Component
struct Parent {
  private data: MyDataSource = new MyDataSource();

  aboutToAppear(): void {
    for (let i = 0; i < 100; i++) {
      this.data.pushData(i.toString());
    }
  }

  build() {
    Row() {
      List() {
        LazyForEach(this.data, (item: string) => {
          ListItem() {
            Text(item.toString())
              .fontSize(16)
              .textAlign(TextAlign.Center)
              .size({ height: 100, width: '100%' })
          }.margin(10)
          .borderRadius(10)
          .backgroundColor('#FFFFFFFF')
        }, (item: string) => item)
          .onMove((from: number, to: number) => { // 实现拖拽排序
            this.data.moveDataWithoutNotify(from, to);
          })
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#FFDCDCDC')
    }
  }
}

迁移Repeat

Repeat具有与LazyForEach相同的onMove属性。将LazyForEach相关代码修改为Repeat后,即可实现拖拽排序。

迁移后的示例如下所示。

示例6 - 迁移后

@Entry
@ComponentV2
struct Parent {
  @Local data: string[] = [];

  aboutToAppear(): void {
    for (let i = 0; i < 100; i++) {
      this.data.push(i.toString());
    }
  }

  moveData(from: number, to: number) {
    let tmp = this.data.splice(from, 1);
    this.data.splice(to, 0, tmp[0]);
  }

  build() {
    Row() {
      List() {
        Repeat(this.data)
          .each((repeatItem) => {
            ListItem() {
              Text(repeatItem.item.toString())
                .fontSize(16)
                .textAlign(TextAlign.Center)
                .size({ height: 100, width: '100%' })
            }.margin(10)
            .borderRadius(10)
            .backgroundColor('#FFFFFFFF')
          })
          .key((item: string) => item)
          .virtualScroll()
          .onMove((from: number, to: number) => { // 实现拖拽排序
            this.moveData(from, to);
          })
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#FFDCDCDC')
    }
  }
}

运行后界面如下图所示。

LazyForEach-Repeat-Migration-Demo-6

组件复用

LazyForEach示例

LazyForEach自身并不具备组件复用能力,为实现组件复用,需要与@Reusable装饰器配合使用(被@Reusable装饰的自定义组件具有复用能力)。

示例7演示了组件复用的典型场景。

示例7 - 迁移前

/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 */

class MyDataSource extends BasicDataSource {
  private dataArray: StringData[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): StringData {
    return this.dataArray[index];
  }

  public pushData(data: StringData): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

class StringData {
  message: string;

  constructor(message: string) {
    this.message = message;
  }
}

@Entry
@Component
struct MyComponent {
  data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 0; i <= 30; i++) {
      this.data.pushData(new StringData(`Hello${i}`));
    }
  }

  build() {
    List({ space: 3 }) {
      LazyForEach(this.data, (item: StringData, index: number) => {
        ListItem() {
          ChildComponent({ data: item })
            .onAppear(() => {
              console.info(`onAppear: ${item.message}`);
            })
        }
      }, (item: StringData, index: number) => index.toString())
    }.cachedCount(5)
  }
}

@Reusable
@Component
struct ChildComponent {
  @State data: StringData = new StringData('');

  aboutToAppear(): void {
    console.info(`aboutToAppear: ${this.data.message}`);
  }

  aboutToRecycle(): void {
    console.info(`aboutToRecycle: ${this.data.message}`);
  }

  // 对复用的组件进行数据更新
  aboutToReuse(params: Record<string, ESObject>): void {
    this.data = params.data as StringData;
    console.info(`aboutToReuse: ${this.data.message}`);
  }

  build() {
    Row() {
      Text(this.data.message).fontSize(50)
    }
  }
}

迁移Repeat

Repeat本身具备组件复用能力,同时也支持与状态管理V2的@ReusableV2装饰器联合使用。因此,迁移至Repeat后,其组件复用具有两种实现方案。

  1. 直接使用Repeat自身的复用能力。
  2. 使用@ReusableV2装饰器提供的复用能力。

需要注意的是,Repeat默认使能自身的复用能力,且优先级高于@ReusableV2装饰器。若要使用@ReusableV2装饰器,需要先手动关闭Repeat自身的复用能力(@ReusableV2装饰器从API version 18开始支持,Repeat从API version 18开始支持关闭自身复用能力)。

示例7 - 迁移方案1:使用Repeat自身的复用能力

Repeat本身具备复用能力,且默认开启。将LazyForEach相关代码迁移至Repeat后,便已经具备了复用能力。

修改后的示例如下。

class StringData {
  message: string;

  constructor(message: string) {
    this.message = message;
  }
}

@Entry
@ComponentV2
struct MyComponent {
  @Local data: StringData[] = [];

  aboutToAppear() {
    for (let i = 0; i <= 30; i++) {
      this.data.push(new StringData(`Hello${i}`));
    }
  }

  build() {
    List({ space: 3 }) {
      Repeat(this.data) // Repeat自身具备复用功能
        .each((repeatItem) => {
          ListItem() {
            Text(repeatItem.item.message).fontSize(50)
          }
        })
        .key((item: StringData, index: number) => index.toString())
        .virtualScroll()
    }.cachedCount(5)
  }
}

示例7 - 迁移方案2:使用@ReusableV2装饰器

若要使用@ReusableV2装饰器,首先需要通过.virtualScroll({ reusable: false })关闭Repeat自身的复用功能,再用@ReusableV2装饰需要复用的自定义组件。

相较于Repeat自身的复用,@ReusableV2装饰的自定义组件在回收和复用时,会触发aboutToRecycle和aboutToReuse两个生命周期。

使用@ReusableV2装饰器的迁移示例如下所示。

class StringData {
  message: string;

  constructor(message: string) {
    this.message = message;
  }
}

@Entry
@ComponentV2
struct MyComponent {
  @Local data: StringData[] = [];

  aboutToAppear() {
    for (let i = 0; i <= 30; i++) {
      this.data.push(new StringData(`Hello${i}`));
    }
  }

  build() {
    List({ space: 3 }) {
      Repeat(this.data)
        .each((repeatItem) => {
          ListItem() {
            ChildComponent({ data: repeatItem.item })
              .onAppear(() => {
                console.info(`onAppear: ${repeatItem.item.message}`);
              })
          }
        })
        .key((item: StringData, index: number) => index.toString())
        .virtualScroll({ reusable: false }) // 关闭Repeat自身的复用功能(API 19)
    }.cachedCount(5)
  }
}

// 使用@ReusableV2实现组件复用(API 18)
@ReusableV2
@ComponentV2
struct ChildComponent {
  @Param data: StringData = new StringData('');

  aboutToAppear(): void {
    console.info(`aboutToAppear: ${this.data.message}`);
  }

  aboutToRecycle(): void {
    console.info(`aboutToRecycle: ${this.data.message}`);
  }

  aboutToReuse(): void {
    console.info(`aboutToReuse: ${this.data.message}`);
  }

  build() {
    Row() {
      Text(this.data.message).fontSize(50)
    }
  }
}

运行后界面如下图所示。

LazyForEach-Repeat-Migration-Demo-7

模板渲染

LazyForEach示例

LazyForEach自身并不具备模板渲染能力。为实现模板渲染能力,需要开发者自己实现逻辑判断,为不同的数据项选择不同的渲染模板。

示例8演示了模板渲染的典型场景。

示例8 - 迁移前

/** BasicDataSource代码见文档末尾BasicDataSource示例代码: StringData类型数组的BasicDataSource代码 */

class MyDataSource extends BasicDataSource {
  private dataArray: StringData[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): StringData {
    return this.dataArray[index];
  }

  public pushData(data: StringData): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

class StringData {
  message: string;
  type: number;

  constructor(message: string, type: number) {
    this.message = message;
    this.type = type;
  }

  getType(): number {
    if (this.type >= 1) {
      return 1;
    } else {
      return 0;
    }
  }
}

@Entry
@Component
struct MyComponent {
  data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 0; i <= 200; i++) {
      this.data.pushData(new StringData(`Hello${i}`, i % 2));
    }
  }

  build() {
    List({ space: 3 }) {
      LazyForEach(this.data, (item: StringData, index: number) => {
        ListItem() {
          // 开发者自己实现逻辑判断,为不同的数据项选择不同的渲染模板
          if (item.getType() == 0) {
            // 模板A
            ChildComponentA({ data: item })
              .onAppear(() => {
                console.info(`type A onAppear: ${item.message}`);
              })
          } else {
            // 模板B
            ChildComponentB({ data: item })
              .onAppear(() => {
                console.info(`type B onAppear: ${item.message}`);
              })
          }
        }
      }, (item: StringData, index: number) => index.toString())
    }.cachedCount(5)
  }
}

// 使用@Reusable实现组件复用
@Reusable
@Component
struct ChildComponentA {
  @State data: StringData = new StringData('', 0);

  aboutToAppear(): void {
    console.info(`type A aboutToAppear: ${this.data.message}`);
  }

  aboutToRecycle(): void {
    console.info(`type A aboutToRecycle: ${this.data.message}`);
  }

  aboutToReuse(params: Record<string, ESObject>): void {
    this.data = params.data as StringData;
    console.info(`type A aboutToReuse: ${this.data.message}`);
  }

  build() {
    Row() {
      Text(this.data.message).fontSize(50)
      Button('Type A')
    }
  }
}

@Reusable
@Component
struct ChildComponentB {
  @State data: StringData = new StringData('', 0);

  aboutToAppear(): void {
    console.info(`type B aboutToAppear: ${this.data.message}`);
  }

  aboutToRecycle(): void {
    console.info(`type B aboutToRecycle: ${this.data.message}`);
  }

  aboutToReuse(params: Record<string, ESObject>): void {
    this.data = params.data as StringData;
    console.info(`type B aboutToReuse: ${this.data.message}`);
  }

  build() {
    Row() {
      Text(this.data.message).fontSize(50).fontColor(Color.Gray)
      Text('Type B')
    }
  }
}

迁移Repeat

Repeat本身具备模板渲染能力,开发者可以通过templateId方法为不同的数据项选择不同的模板,再通过template方法为不同的模板配置不同的组件生成函数。同时,开发者仍然可以选择自己实现逻辑判断,为不同的数据项分配不同的模板。

需要注意的是,如果开发者选择自己实现模板渲染,则需要关闭Repeat自身的复用功能。否则,Repeat在复用子组件时无法选择正确的模板,会导致渲染异常。

示例8 - 迁移方案1:使用Repeat自身的模板渲染能力

class StringData {
  message: string;
  type: number;

  constructor(message: string, type: number) {
    this.message = message;
    this.type = type;
  }

  getType(): number {
    if (this.type >= 1) {
      return 1;
    } else {
      return 0;
    }
  }
}

@Entry
@ComponentV2
struct MyComponent {
  data: StringData[] = [];

  aboutToAppear() {
    for (let i = 0; i <= 200; i++) {
      this.data.push(new StringData(`Hello${i}`, i % 2));
    }
  }

  build() {
    List({ space: 3 }) {
      Repeat(this.data)
        .each((repeatItem) => {
          ListItem() {
            Text('Default item')
          }
        })
        .template('A', (repeatItem) => { // 模板A
          ListItem() {
            Row() {
              Text(repeatItem.item.message).fontSize(50)
              Button('Type A')
            }
          }
        })
        .template('B', (repeatItem) => { // 模板B
          ListItem() {
            Row() {
              Text(repeatItem.item.message).fontSize(50).fontColor(Color.Gray)
              Text('Type B')
            }
          }
        })
        .templateId((item: StringData) => { // 为不同的数据项选择不同的模板
          if (item.getType() == 0) {
            return 'A';
          } else {
            return 'B';
          }
        })
        .key((item: StringData, index: number) => index.toString())
        .virtualScroll()
    }.cachedCount(5)
  }
}

示例8 - 迁移方案2:由开发者实现模板渲染能力

class StringData {
  message: string;
  type: number;

  constructor(message: string, type: number) {
    this.message = message;
    this.type = type;
  }

  getType(): number {
    if (this.type >= 1) {
      return 1;
    } else {
      return 0;
    }
  }
}

@Entry
@ComponentV2
struct MyComponent {
  data: StringData[] = [];

  aboutToAppear() {
    for (let i = 0; i <= 200; i++) {
      this.data.push(new StringData(`Hello${i}`, i % 2));
    }
  }

  build() {
    List({ space: 3 }) {
      Repeat(this.data)
        .each((repeatItem) => {
          ListItem() {
            // 开发者自己实现逻辑判断,为不同的数据项选择不同的渲染模板
            if (repeatItem.item.getType() == 0) {
              ChildComponentA({ data: repeatItem.item }) // 模板A
                .onAppear(() => {
                  console.info(`type A onAppear: ${repeatItem.item.message}`);
                })
            } else {
              ChildComponentB({ data: repeatItem.item }) // 模板B
                .onAppear(() => {
                  console.info(`type B onAppear: ${repeatItem.item.message}`);
                })
            }
          }
        })
        .key((item: StringData, index: number) => index.toString())
        .virtualScroll({ reusable: false }) // 关闭Repeat自身的复用功能(API 19),避免渲染异常
    }.cachedCount(5)
  }
}

// 使用@ReusableV2实现组件复用(API 18)
@ReusableV2
@ComponentV2
struct ChildComponentA {
  @Param data: StringData = new StringData('', 0);

  aboutToAppear(): void {
    console.info(`type A aboutToAppear: ${this.data.message}`);
  }

  aboutToRecycle(): void {
    console.info(`type A aboutToRecycle: ${this.data.message}`);
  }

  aboutToReuse(): void {
    console.info(`type A aboutToReuse: ${this.data.message}`);
  }

  build() {
    Row() {
      Text(this.data.message).fontSize(50)
      Button('Type A')
    }
  }
}

@ReusableV2
@ComponentV2
struct ChildComponentB {
  @Param data: StringData = new StringData('', 0);

  aboutToAppear(): void {
    console.info(`type B aboutToAppear: ${this.data.message}`);
  }

  aboutToRecycle(): void {
    console.info(`type B aboutToRecycle: ${this.data.message}`);
  }

  aboutToReuse(): void {
    console.info(`type B aboutToReuse: ${this.data.message}`);
  }

  build() {
    Row() {
      Text(this.data.message).fontSize(50).fontColor(Color.Gray)
      Text('Type B')
    }
  }
}

运行后界面如下图所示。

LazyForEach-Repeat-Migration-Demo-8

BasicDataSource示例代码

string类型数组的BasicDataSource代码

// BasicDataSource实现了IDataSource接口,用于管理listener监听,以及通知LazyForEach数据更新
class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: string[] = [];

  public totalCount(): number {
    return this.originDataArray.length;
  }

  public getData(index: number): string {
    return this.originDataArray[index];
  }

  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      console.info('add listener');
      this.listeners.push(listener);
    }
  }

  // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      console.info('remove listener');
      this.listeners.splice(pos, 1);
    }
  }

  // 通知LazyForEach组件需要重载所有子组件
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    });
  }

  // 通知LazyForEach组件需要在index对应索引处添加子组件
  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
      // 写法2:listener.onDatasetChange([{type: DataOperationType.ADD, index: index}]);
    });
  }

  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index);
      // 写法2:listener.onDatasetChange([{type: DataOperationType.CHANGE, index: index}]);
    });
  }

  // 通知LazyForEach组件需要在index对应索引处删除该子组件
  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
      // 写法2:listener.onDatasetChange([{type: DataOperationType.DELETE, index: index}]);
    });
  }

  // 通知LazyForEach组件将from索引和to索引处的子组件进行交换
  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
      // 写法2:listener.onDatasetChange([{type: DataOperationType.EXCHANGE, index: {start: from, end: to}}]);
    });
  }

  notifyDatasetChange(operations: DataOperation[]): void {
    this.listeners.forEach(listener => {
      listener.onDatasetChange(operations);
    });
  }
}

StringData类型数组的BasicDataSource代码

class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: StringData[] = [];

  public totalCount(): number {
    return this.originDataArray.length;
  }

  public getData(index: number): StringData {
    return this.originDataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      console.info('add listener');
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      console.info('remove listener');
      this.listeners.splice(pos, 1);
    }
  }

  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    });
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    });
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index);
    });
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    });
  }

  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
    });
  }

  notifyDatasetChange(operations: DataOperation[]): void {
    this.listeners.forEach(listener => {
      listener.onDatasetChange(operations);
    });
  }
}

你可能感兴趣的鸿蒙文章

openharmony 鸿蒙 arkts-rendering-control-overview

openharmony 鸿蒙 arkts-rendering-control-contentslot

openharmony 鸿蒙 arkts-rendering-control-lazyforeach

openharmony 鸿蒙 arkts-rendering-control-foreach

openharmony 鸿蒙 arkts-rendering-control-ifelse

openharmony 鸿蒙 arkts-new-rendering-control-repeat

  • 所属分类: 后端技术
  • 本文标签: 鸿蒙 软件
  • 版权声明: 本文链接 https://seaxiang.com/blog/ckbS5s2t