【鸿蒙开发】第三十九章 LazyForEach:数据懒加载
目录
1 背景
2 使用限制
键值生成规则
组件创建规则
首次渲染
非首次渲染
改变数据子属性
使用状态管理V2
拖拽排序
1 背景
LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。
2 使用限制
- LazyForEach必须在容器组件内使用,仅有List、Grid、Swiper以及WaterFlow组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。
- 容器组件内使用LazyForEach的时候,只能包含一个LazyForEach。以List为例,同时包含ListItem、ForEach、LazyForEach的情形是不推荐的;同时包含多个LazyForEach也是不推荐的。
- LazyForEach在每次迭代中,必须创建且只允许创建一个子组件;即LazyForEach的子组件生成函数有且只有一个根组件。
- 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。
- 允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。
- 键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件渲染出现问题。
- LazyForEach必须使用DataChangeListener对象进行更新,对第一个参数dataSource重新赋值会异常;dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。
- 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。
- LazyForEach必须和@Reusable装饰器一起使用才能触发节点复用。使用方法:将@Reusable装饰在LazyForEach列表的组件上,见使用规则。
键值生成规则
在LazyForEach循环渲染过程中,系统会为每个item生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。
LazyForEach提供了一个名为keyGenerator的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即(item: Object, index: number) => { return viewId + '-' + index.toString(); }, viewId在编译器转换过程中生成,同一个LazyForEach组件内其viewId是一致的。
组件创建规则
在确定键值生成规则后,LazyForEach的第二个参数itemGenerator函数会根据组件创建规则为数据源的每个数组项创建组件。组件的创建包括两种情况:LazyForEach首次渲染和LazyForEach非首次渲染。
首次渲染
生成不同键值
在LazyForEach首次渲染时,会根据上述键值生成规则为数据源的每个数组项生成唯一键值,并创建相应的组件。
/** 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)}
}
在上述代码中,键值生成规则是keyGenerator函数的返回值item。在LazyForEach循环渲染时,其为数据源数组项依次生成键值Hello 0、Hello 1 ... Hello 20,并创建对应的ListItem子组件渲染到界面上。
运行效果如下图所示。
图1 LazyForEach正常首次渲染
键值相同时错误渲染
当不同数据项生成的键值相同时,框架的行为是不可预测的。例如,在以下代码中,LazyForEach渲染的数据项键值均相同,在滑动过程中,LazyForEach会对划入划出当前页面的子组件进行预加载,而新建的子组件和销毁的原子组件具有相同的键值,框架可能存在取用缓存错误的情况,导致子组件渲染有问题。
/** 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) => 'same key')}.cachedCount(5)}
}
运行效果如下图所示。
图2 LazyForEach存在相同键值
非首次渲染
当LazyForEach数据源发生变化,需要再次渲染时,开发者应根据数据源的变化情况调用listener对应的接口,通知LazyForEach做相应的更新,各使用场景如下。
添加数据
/** 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 })}.onClick(() => {// 点击追加子组件this.data.pushData(`Hello ${this.data.totalCount()}`);})}, (item: string) => item)}.cachedCount(5)}
}
当我们点击LazyForEach的子组件时,首先调用数据源data的pushData方法,该方法会在数据源末尾添加数据并调用notifyDataAdd方法。在notifyDataAdd方法内会又调用listener.onDataAdd方法,该方法会通知LazyForEach在该处有数据添加,LazyForEach便会在该索引处新建子组件。
运行效果如下图所示。
图3 LazyForEach添加数据
删除数据
/** 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 getAllData(): string[] {return this.dataArray;}public pushData(data: string): void {this.dataArray.push(data);}public deleteData(index: number): void {this.dataArray.splice(index, 1);this.notifyDataDelete(index);}
}@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, index: number) => {ListItem() {Row() {Text(item).fontSize(50).onAppear(() => {console.info("appear:" + item)})}.margin({ left: 10, right: 10 })}.onClick(() => {// 点击删除子组件this.data.deleteData(this.data.getAllData().indexOf(item));})}, (item: string) => item)}.cachedCount(5)}
}
当我们点击LazyForEach的子组件时,首先调用数据源data的deleteData方法,该方法会删除数据源对应索引处的数据并调用notifyDataDelete方法。在notifyDataDelete方法内会又调用listener.onDataDelete方法,该方法会通知LazyForEach在该处有数据删除,LazyForEach便会在该索引处删除对应子组件。
运行效果如下图所示。
图4 LazyForEach删除数据
交换数据
/** 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 getAllData(): string[] {return this.dataArray;}public pushData(data: string): void {this.dataArray.push(data);}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);}
}@Entry
@Component
struct MyComponent {private moved: number[] = [];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, index: number) => {ListItem() {Row() {Text(item).fontSize(50).onAppear(() => {console.info("appear:" + item)})}.margin({ left: 10, right: 10 })}.onClick(() => {this.moved.push(this.data.getAllData().indexOf(item));if (this.moved.length === 2) {// 点击交换子组件this.data.moveData(this.moved[0], this.moved[1]);this.moved = [];}})}, (item: string) => item)}.cachedCount(5)}
}
当我们首次点击LazyForEach的子组件时,在moved成员变量内存入要移动的数据索引,再次点击LazyForEach另一个子组件时,我们将首次点击的子组件移到此处。调用数据源data的moveData方法,该方法会将数据源对应数据移动到预期的位置并调用notifyDataMove方法。在notifyDataMove方法内会又调用listener.onDataMove方法,该方法通知LazyForEach在该处有数据需要移动,LazyForEach便会将from和to索引处的子组件进行位置调换。
运行效果如下图所示。
图5 LazyForEach交换数据
改变单个数据
/** 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);}public changeData(index: number, data: string): void {this.dataArray.splice(index, 1, data);this.notifyDataChange(index);}
}@Entry
@Component
struct MyComponent {private moved: number[] = [];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, index: number) => {ListItem() {Row() {Text(item).fontSize(50).onAppear(() => {console.info("appear:" + item)})}.margin({ left: 10, right: 10 })}.onClick(() => {this.data.changeData(index, item + '00');})}, (item: string) => item)}.cachedCount(5)}
}
当我们点击LazyForEach的子组件时,首先改变当前数据,然后调用数据源data的changeData方法,在该方法内会调用notifyDataChange方法。在notifyDataChange方法内会又调用listener.onDataChange方法,该方法通知LazyForEach组件该处有数据发生变化,LazyForEach便会在对应索引处重建子组件。
运行效果如下图所示。
图6 LazyForEach改变单个数据
改变多个数据
/** 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);}public reloadData(): void {this.notifyDataReload();}public modifyAllData(): void {this.dataArray = this.dataArray.map((item: string) => {return item + '0';})}
}@Entry
@Component
struct MyComponent {private moved: number[] = [];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, index: number) => {ListItem() {Row() {Text(item).fontSize(50).onAppear(() => {console.info("appear:" + item)})}.margin({ left: 10, right: 10 })}.onClick(() => {this.data.modifyAllData();this.data.reloadData();})}, (item: string) => item)}.cachedCount(5)}
}
当我们点击LazyForEach的子组件时,首先调用data的modifyAllData方法改变了数据源中的所有数据,然后调用数据源的reloadData方法,在该方法内会调用notifyDataReload方法。在notifyDataReload方法内会又调用listener.onDataReloaded方法,通知LazyForEach需要重建所有子节点。LazyForEach会将原所有数据项和新所有数据项一一做键值比对,若有相同键值则使用缓存,若键值不同则重新构建。
运行效果如下图所示。
图7 LazyForEach改变多个数据
精准批量修改数据
/** 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 operateData(): void {console.info(JSON.stringify(this.dataArray));this.dataArray.splice(4, 0, this.dataArray[1]);this.dataArray.splice(1, 1);let temp = this.dataArray[4];this.dataArray[4] = this.dataArray[6];this.dataArray[6] = tempthis.dataArray.splice(8, 0, 'Hello 1', 'Hello 2');this.dataArray.splice(12, 2);console.info(JSON.stringify(this.dataArray));this.notifyDatasetChange([{ type: DataOperationType.MOVE, index: { from: 1, to: 3 } },{ type: DataOperationType.EXCHANGE, index: { start: 4, end: 6 } },{ type: DataOperationType.ADD, index: 8, count: 2 },{ type: DataOperationType.DELETE, index: 10, count: 2 }]);}public init(): void {this.dataArray.splice(0, 0, 'Hello a', 'Hello b', 'Hello c', 'Hello d', 'Hello e', 'Hello f', 'Hello g', 'Hello h','Hello i', 'Hello j', 'Hello k', 'Hello l', 'Hello m', 'Hello n', 'Hello o', 'Hello p', 'Hello q', 'Hello r');}
}@Entry
@Component
struct MyComponent {private data: MyDataSource = new MyDataSource();aboutToAppear() {this.data.init()}build() {Column() {Text('change data').fontSize(10).backgroundColor(Color.Blue).fontColor(Color.White).borderRadius(50).padding(5).onClick(() => {this.data.operateData();})List({ space: 3 }) {LazyForEach(this.data, (item: string, index: number) => {ListItem() {Row() {Text(item).fontSize(35).onAppear(() => {console.info("appear:" + item)})}.margin({ left: 10, right: 10 })}}, (item: string) => item + new Date().getTime())}.cachedCount(5)}}
}
onDatasetChange接口允许开发者一次性通知LazyForEach进行数据添加、删除、移动和交换等操作。在上述例子中,点击“change data”文本后,第二项数据被移动到第四项位置,第五项与第七项数据交换位置,并且从第九项开始添加了数据"Hello 1"和"Hello 2",同时从第十一项开始删除了两项数据。
图8 LazyForEach改变多个数据
第二个例子,直接给数组赋值,不涉及 splice 操作。operations直接从比较原数组和新数组得到。
/** 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 operateData(): void {this.dataArray =['Hello x', 'Hello 1', 'Hello 2', 'Hello b', 'Hello c', 'Hello e', 'Hello d', 'Hello f', 'Hello g', 'Hello h']this.notifyDatasetChange([{ type: DataOperationType.CHANGE, index: 0 },{ type: DataOperationType.ADD, index: 1, count: 2 },{ type: DataOperationType.EXCHANGE, index: { start: 3, end: 4 } },]);}public init(): void {this.dataArray = ['Hello a', 'Hello b', 'Hello c', 'Hello d', 'Hello e', 'Hello f', 'Hello g', 'Hello h'];}
}@Entry
@Component
struct MyComponent {private data: MyDataSource = new MyDataSource();aboutToAppear() {this.data.init()}build() {Column() {Text('Multi-Data Change').fontSize(10).backgroundColor(Color.Blue).fontColor(Color.White).borderRadius(50).padding(5).onClick(() => {this.data.operateData();})List({ space: 3 }) {LazyForEach(this.data, (item: string, index: number) => {ListItem() {Row() {Text(item).fontSize(35).onAppear(() => {console.info("appear:" + item)})}.margin({ left: 10, right: 10 })}}, (item: string) => item + new Date().getTime())}.cachedCount(5)}}
}
图9 LazyForEach改变多个数据
使用该接口时有如下注意事项。
- onDatasetChange与其它操作数据的接口不能混用。
- 传入onDatasetChange的operations,其中每一项operation的index均从修改前的原数组内寻找。因此,operations中的index跟操作Datasource中的index不总是一一对应的,而且不能是负数。
第一个例子清楚地显示了这一点:
// 修改之前的数组
["Hello a","Hello b","Hello c","Hello d","Hello e","Hello f","Hello g","Hello h","Hello i","Hello j","Hello k","Hello l","Hello m","Hello n","Hello o","Hello p","Hello q","Hello r"]
// 修改之后的数组
["Hello a","Hello c","Hello d","Hello b","Hello g","Hello f","Hello e","Hello h","Hello 1","Hello 2","Hello i","Hello j","Hello m","Hello n","Hello o","Hello p","Hello q","Hello r"]
"Hello b" 从第2项变成第4项,因此第一个 operation 为 { type: DataOperationType.MOVE, index: { from: 1, to: 3 } }。
"Hello e" 跟 "Hello g" 对调了,而 "Hello e" 在修改前的原数组中的 index=4,"Hello g" 在修改前的原数组中的 index=6, 因此第二个 operation 为 { type: DataOperationType.EXCHANGE, index: { start: 4, end: 6 } }。
"Hello 1","Hello 2" 在 "Hello h" 之后插入,而 "Hello h" 在修改前的原数组中的 index=7,因此第三个 operation 为 { type: DataOperationType.ADD, index: 8, count: 2 }。
"Hello k","Hello l" 被删除了,而 "Hello k" 在原数组中的 index=10,因此第四个 operation 为 { type: DataOperationType.DELETE, index: 10, count: 2 }。
- 调用一次onDatasetChange,一个index对应的数据只能被操作一次,若被操作多次,LazyForEach仅使第一个操作生效。
- 部分操作可以由开发者传入键值,LazyForEach不会再去重复调用keygenerator获取键值,需要开发者保证传入的键值的正确性。
- 若本次操作集合中有RELOAD操作,则其余操作全不生效。
改变数据子属性
若仅靠LazyForEach的刷新机制,当item变化时若想更新子组件,需要将原来的子组件全部销毁再重新构建,在子组件结构较为复杂的情况下,靠改变键值去刷新渲染性能较低。因此框架提供了@Observed与@ObjectLink机制进行深度观测,可以做到仅刷新使用了该属性的组件,提高渲染性能。开发者可根据其自身业务特点选择使用哪种刷新方式。
/** 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 moved: number[] = [];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: StringDatabuild() {Row() {Text(this.data.message).fontSize(50).onAppear(() => {console.info("appear:" + this.data.message)})}.margin({ left: 10, right: 10 })}
}
此时点击LazyForEach子组件改变item.message时,重渲染依赖的是ChildComponent的@ObjectLink成员变量对其子属性的监听,此时框架只会刷新Text(this.data.message),不会去重建整个ListItem子组件。
图10 LazyForEach改变数据子属性
使用状态管理V2
状态管理V2提供了@ObservedV2与@Trace装饰器可以实现对属性的深度观测,使用@Local和@Param可以实现对子组件的刷新管理,仅刷新使用了对应属性的组件。
嵌套类属性变化观测
/** 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 {firstLayer: FirstLayer;constructor(firstLayer: FirstLayer) {this.firstLayer = firstLayer;}
}class FirstLayer {secondLayer: SecondLayer;constructor(secondLayer: SecondLayer) {this.secondLayer = secondLayer;}
}class SecondLayer {thirdLayer: ThirdLayer;constructor(thirdLayer: ThirdLayer) {this.thirdLayer = thirdLayer;}
}@ObservedV2
class ThirdLayer {@Trace forthLayer: String;constructor(forthLayer: String) {this.forthLayer = forthLayer;}
}@Entry
@ComponentV2
struct MyComponent {private data: MyDataSource = new MyDataSource();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(new StringData(new FirstLayer(new SecondLayer(new ThirdLayer('Hello' + i)))));}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: StringData, index: number) => {ListItem() {Text(item.firstLayer.secondLayer.thirdLayer.forthLayer.toString()).fontSize(50).onClick(() => {item.firstLayer.secondLayer.thirdLayer.forthLayer += '!';})}}, (item: StringData, index: number) => index.toString())}.cachedCount(5)}
}
@ObservedV2与@Trace用于装饰类以及类中的属性,配合使用能深度观测被装饰的类和属性。示例中,展示了深度嵌套类结构下,通过@ObservedV2和@Trace实现对多层嵌套属性变化的观测和子组件刷新。当点击子组件Text修改被@Trace修饰的嵌套类最内层的类成员属性时,仅重新渲染依赖了该属性的组件。
组件内部状态
/** 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 += '?';})}}
}
@Local使得自定义组件内被修饰的变量具有观测其变化的能力,该变量必须在组件内部进行初始化。示例中,点击Text组件修改item.message会触发变量更新并刷新使用该变量的组件,ChildComponent中@Local装饰的变量message变化时也能刷新子组件。
组件外部输入
/** 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)}}
}
使用@Param装饰器可以让子组件接受外部输入的参数,实现父子组件之间的数据同步。在MyComponent中创建子组件时,将变量item.message传递,使用@Param修饰的变量data与之关联。点击ListItem中的组件修改item.message,数据变化会从父组件传递到子组件,并且触发子组件的刷新。
拖拽排序
当LazyForEach在List组件下使用,并且设置了onMove事件,可以使能拖拽排序。拖拽排序离手后,如果数据位置发生变化,则会触发onMove事件,上报数据移动原始索引号和目标索引号。在onMove事件中,需要根据上报的起始索引号和目标索引号修改数据源。onMove中修改数据源不需要调用DataChangeListener中接口通知数据源变化。
/** 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")}}
}
图11 LazyForEach拖拽排序效果图
相关文章:

【鸿蒙开发】第三十九章 LazyForEach:数据懒加载
目录 1 背景 2 使用限制 键值生成规则 组件创建规则 首次渲染 非首次渲染 改变数据子属性 使用状态管理V2 拖拽排序 1 背景 LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架…...

HTTP-
一.HTTP 1.什么是HTTP HTTP(超文本传输协议)是一种工作在应用层的协议.主要用于网站,就是浏览器和服务器之间的数据传输. 小知识:什么是超文本传输协议 文本:是字符串.(能在utf8/gbk码表上找到合法字符) 超文本:不仅可以传输字符串,也可以传输图片,html等 富文本:word文档 2.HT…...
创建型模式 - 原型模式 (Prototype Pattern)
创建型模式 -原型模式 (Prototype Pattern) 它允许通过复制现有对象来创建新对象,而无需知道对象的具体创建细节。在 Java 中,可以通过实现 Cloneable 接口和重写 clone() 方法来实现原型模式。 有深、浅两种克隆 类实现 Cloneable 接口就可以深克隆如果…...
Android 8.0 (API 26) 对广播机制做了哪些变化
大部分隐式广播无法通过静态注册接收,除了以下白名单广播: ACTION_BOOT_COMPLETED ACTION_TIMEZONE_CHANGED ACTION_LOCALE_CHANGED ACTION_MY_PACKAGE_REPLACED ACTION_PACKAGE_ADDED ACTION_PACKAGE_REMOVED 需要以动态注册方案替换: cl…...

Unity汽车笔记
汽车的移动和转向 我们知道,汽车的前进后退是变速运动。按w,汽车开始加速,到最大速度后保持匀速,松开w,汽车受到阻力加速。如果按s减速,则以更大的加速度减速。后退反之。 按A/D时前轮偏转。只有前进后退…...
html中rel、href、src、url的区别
1.url url(统一资源定位符):是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。 2.href href:Hypertext Reference的缩写。 意思是超文本引用。 3.rel rel:relatio…...
【idea问题排查技巧】
以下是针对 IDEA 中 日志打标(动态标记) 和 全链路追踪 功能的分步详解,结合具体场景和操作截图说明,帮助快速掌握实战技巧。 一、动态日志打标:不修改代码输出关键信息 1. 断点日志打印(非侵入式打标) 场景:在调试时,需要临时查看某个变量的值,但不想修改代码添加…...

SQL: DDL,DML,DCL,DTL,TCL,
Structured Query Language,结构化查询语言, 是一种用于管理和操作关系数据库的标准编程语言。 sql的分类 DQL(Data Query Language):数据查询语言 DDL(Data Definition Language):数据定义语…...
WordPress R+L Carrier Edition sql注入漏洞复现(CVE-2024-13481)(附脚本)
免责申明: 本文所描述的漏洞及其复现步骤仅供网络安全研究与教育目的使用。任何人不得将本文提供的信息用于非法目的或未经授权的系统测试。作者不对任何由于使用本文信息而导致的直接或间接损害承担责任。如涉及侵权,请及时与我们联系,我们将尽快处理并删除相关内容。 0x0…...
DeepSeek基础之机器学习
文章目录 一、核心概念总结(一)机器学习基本定义(二)基本术语(三)假设空间(四)归纳偏好(五)“没有免费的午餐”定理(NFL 定理) 二、重…...

QSplashScreen --软件启动前的交互
目录 QSplashScreen 类介绍 使用方式 项目中使用 THPrinterSplashScreen头文件 THPrinterSplashScreen实现代码 使用代码 使用效果 QSplashScreen 类介绍 QSplashScreen 是 Qt 中的一个类,用于显示启动画面。它通常在应用程序启动时显示,以向用户显…...
Python 循环嵌套
Python 循环嵌套 引言 在编程语言中,循环嵌套是一种常见且强大的编程技术。它允许开发者重复执行一段代码块,并在每个循环迭代中执行另一个循环。在Python中,循环嵌套广泛应用于数据分析和算法实现等领域。本文将详细介绍Python循环嵌套的概念、语法以及应用场景。 循环嵌…...

性能测试项目实战
项目介绍和部署 项目背景 轻商城项目是一个现在流行的电商项目。我们需要综合评估该项目中各个关键接口的性能,并给出优化建议,以满足项目上线后的性能需要。 项目功能架构 前台商城:购物车、订单、支付、优惠券等 后台管理系统:商…...

Web自动化之Selenium实战案例2:东方财富网股吧评论爬取
上一篇文章,我们使用Selenium完成了网页内文件的自动下载,本文我们将使用Selenium来爬取东方财富网股吧内笔记的评论数据。 网页内容分析 网页内容的分析是web自动化中的关键一步。通过分析网页结构,我们可以确定需要抓取的数据位置以及操作元素的方式。…...

【Java八股文】09-计算机操作系统面试篇
文章目录 计算机操作系统面试篇用户态和内核态的区别?用户态和内核态的区别? 进程管理线程和进程的区别是什么?进程,线程,协程的区别是什么?创建一个协程的过程线程运行过程中申请到的东西在切换时是否全部…...

【Redis】在Java中以及Spring环境下操作Redis
Java环境下: 1.创建maven 项目 2.导入依赖 <!-- redis --><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>4.3.2</version></dependency> 此处使用的是Jedis&…...

MFC—加法器
1.需要为编辑框添加变量 2.在cpp文件中的按钮中添加代码 void CMFCAddtionDlg::OnBnClickedButton1() {// TODO: 在此添加控件通知处理程序代码UpdateData(true);//把控件里的值更新给变量m_add m_add1 m_add2;//加法UpdateData(false);//把控件相加的值赋值给控件 }...

SpringBoot五:Web开发
精心整理了最新的面试资料,有需要的可以自行获取 点击前往百度网盘获取 点击前往夸克网盘获取 要解决的问题 导入静态资源首页jsp,模板引擎Thymeleaf装配扩展SpringMVC增删改查拦截器国际化(非重点) 可以使用以下方式处理静态…...

电机控制的空间矢量调制 (SVPWM)
目录 概述 1 电机控制的空间矢量调制 (SVPWM)介绍 2 实现原理 2.1 设计要求 2.2 SVPWM 的实现 3 SVPWM的C语言 3.1 代码文件 3.2 STM32G4平台上验证 4 源代码文件 概述 本文主要介绍电机控制的空间矢量调制 (SVPWM),空间矢量调制 (SVPWM) 是感应电机和永磁…...
jupyterhub on k8s 配置用户名密码 简单版
如果只是小组内使用 不想共用密码 也不想搞复杂认证方案 那么就直接通过map(用户名,密码md5值)来制定密码 config.yaml部分内容 hub:config:JupyterHub:shutdown_on_logout: true # 用户logout 自动stop jupyter pod,家目录下所有文件会被保存到pvc 即启动后之前家目录下…...

MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...
Java 8 Stream API 入门到实践详解
一、告别 for 循环! 传统痛点: Java 8 之前,集合操作离不开冗长的 for 循环和匿名类。例如,过滤列表中的偶数: List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...

屋顶变身“发电站” ,中天合创屋面分布式光伏发电项目顺利并网!
5月28日,中天合创屋面分布式光伏发电项目顺利并网发电,该项目位于内蒙古自治区鄂尔多斯市乌审旗,项目利用中天合创聚乙烯、聚丙烯仓库屋面作为场地建设光伏电站,总装机容量为9.96MWp。 项目投运后,每年可节约标煤3670…...
CSS设置元素的宽度根据其内容自动调整
width: fit-content 是 CSS 中的一个属性值,用于设置元素的宽度根据其内容自动调整,确保宽度刚好容纳内容而不会超出。 效果对比 默认情况(width: auto): 块级元素(如 <div>)会占满父容器…...
JS设计模式(4):观察者模式
JS设计模式(4):观察者模式 一、引入 在开发中,我们经常会遇到这样的场景:一个对象的状态变化需要自动通知其他对象,比如: 电商平台中,商品库存变化时需要通知所有订阅该商品的用户;新闻网站中࿰…...

破解路内监管盲区:免布线低位视频桩重塑停车管理新标准
城市路内停车管理常因行道树遮挡、高位设备盲区等问题,导致车牌识别率低、逃费率高,传统模式在复杂路段束手无策。免布线低位视频桩凭借超低视角部署与智能算法,正成为破局关键。该设备安装于车位侧方0.5-0.7米高度,直接规避树枝遮…...
Python 高效图像帧提取与视频编码:实战指南
Python 高效图像帧提取与视频编码:实战指南 在音视频处理领域,图像帧提取与视频编码是基础但极具挑战性的任务。Python 结合强大的第三方库(如 OpenCV、FFmpeg、PyAV),可以高效处理视频流,实现快速帧提取、压缩编码等关键功能。本文将深入介绍如何优化这些流程,提高处理…...

基于江科大stm32屏幕驱动,实现OLED多级菜单(动画效果),结构体链表实现(独创源码)
引言 在嵌入式系统中,用户界面的设计往往直接影响到用户体验。本文将以STM32微控制器和OLED显示屏为例,介绍如何实现一个多级菜单系统。该系统支持用户通过按键导航菜单,执行相应操作,并提供平滑的滚动动画效果。 本文设计了一个…...
虚幻基础:角色旋转
能帮到你的话,就给个赞吧 😘 文章目录 移动组件使用控制器所需旋转:组件 使用 控制器旋转将旋转朝向运动:组件 使用 移动方向旋转 控制器旋转和移动旋转 缺点移动旋转:必须移动才能旋转,不移动不旋转控制器…...