HarmonyOS鸿蒙Next中WaterFlow的WaterFlowSections使用分组能力和Repeat的懒加载是不是不兼容?
HarmonyOS鸿蒙Next中WaterFlow的WaterFlowSections使用分组能力和Repeat的懒加载是不是不兼容? 如图:官方demo waterflow
/*
* Copyright (c) 2025 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CommonConstants } from '../common/constants/CommonConstants';
import Logger from '../common/utils/Logger';
import { SectionsWaterFlowDataSource } from '../viewmodel/SectionsWaterFlowDataSource';
@Component
struct ReusableFlowItem {
@State item: number = 0;
// aboutToReuse(params: Record<string, number>) {
// this.item = params.item;
// }
build() {
RelativeContainer() {
Image($rawfile(`sections/${this.item % 4}.jpg`))
.objectFit(ImageFit.Cover)
.width(CommonConstants.FULL_WIDTH)
.layoutWeight(1)
.borderRadius($r('app.float.sections_item_radius'))
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
left: { anchor: '__container__', align: HorizontalAlign.Start },
right: { anchor: '__container__', align: HorizontalAlign.End }
})
.id('image1')
Stack() {
}
.linearGradient({
angle: 0,
colors: [[$r('app.color.linearGradient_first_color'), 0.0],
[$r('app.color.linearGradient_last_color'), 1.0]]
})
.width(CommonConstants.FULL_WIDTH)
.height($r('app.float.sections_item_blur_height'))
.borderRadius($r('app.float.sections_item_radius'))
.hitTestBehavior(HitTestMode.None)
.alignRules({
bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
left: { anchor: '__container__', align: HorizontalAlign.Start },
right: { anchor: '__container__', align: HorizontalAlign.End }
})
.id('mask1')
Text('NO. ' + (this.item + 1))
.fontSize($r('app.float.sections_item_text_size'))
.fontColor(Color.White)
.alignRules({
bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
left: { anchor: '__container__', align: HorizontalAlign.Start }
})
.margin({
left: $r('app.float.sections_item_text_margin_left'),
bottom: $r("app.float.sections_item_text_margin_bottom")
})
.id('text1')
}
.width(CommonConstants.FULL_WIDTH)
.borderRadius($r('app.float.sections_item_radius'))
.backgroundColor(Color.Gray)
}
}
@Preview
@Component
export struct SectionsPage {
@State isRefreshing: boolean = false;
@State currentItem: number = -1;
@State minSize: number = CommonConstants.FLOW_ITEM_MIN_HEIGHT;
@State maxSize: number = CommonConstants.FLOW_ITEM_MAX_HEIGHT;
scroller: Scroller = new Scroller();
@State dataArray: number[] = []
private itemHeightArray: number[] = [];
@State sections: WaterFlowSections = new WaterFlowSections();
sectionMargin: Margin = {
top: 8,
left: 0,
bottom: 0,
right: 0
};
oneColumnSection: SectionOptions = {
itemsCount: 3,
crossCount: 1,
columnsGap: 5,
rowsGap: 10,
margin: this.sectionMargin,
onGetItemMainSizeByIndex: (index: number) => {
return CommonConstants.SECTION1_ITEM_SIZE;
}
};
twoColumnSection: SectionOptions = {
itemsCount: 2,
crossCount: 2,
margin: this.sectionMargin,
onGetItemMainSizeByIndex: (index: number) => {
return CommonConstants.SECTION2_ITEM_SIZE;
}
};
dataSection: SectionOptions = {
itemsCount: 20,
crossCount: 2,
margin: this.sectionMargin,
onGetItemMainSizeByIndex: (index: number) => {
return this.itemHeightArray[index % CommonConstants.REFRESH_COUNT];
}
};
@Builder
headerRefresh() {
Column() {
LoadingProgress()
.color(Color.Black)
.opacity(0.6)
.width($r('app.float.refresh_loading_width'))
.height($r('app.float.refresh_loading_height'))
}
.justifyContent(FlexAlign.Center)
}
refresh() {
this.currentItem = -1;
setTimeout(() => {
//Add new data.
this.dataArray = [];
let value = Math.floor(Math.random() * CommonConstants.REFRESH_COUNT);
for (let i = 0; i < CommonConstants.REFRESH_COUNT; i++) {
this.dataArray.push(i + value)
}
//Update sections itemsCount.
this.oneColumnSection.itemsCount = 3;
this.oneColumnSection.crossCount = 1;
this.twoColumnSection.itemsCount = 2;
this.twoColumnSection.crossCount = 2;
this.dataSection.itemsCount = 95;
this.dataSection.crossCount = 2;
this.sections.update(0, this.oneColumnSection);
this.sections.update(1, this.twoColumnSection);
this.sections.update(2, this.dataSection);
this.isRefreshing = false;
}, CommonConstants.REFRESH_TIMEOUT);
}
loadMore(last: number) {
setTimeout(() => {
let totalCount = this.dataArray.length;
const index: number = this.dataArray.length;
const tmpArrays: number[]= []
if (last + CommonConstants.LOAD_MORE_COUNT >= totalCount) {
for (let i = 0; i < CommonConstants.LOAD_MORE_COUNT; i++) {
// this.dataArray.splice(this.dataArray.length, 0, this.dataArray.length);
tmpArrays.push((index + i))
// this.dataArray.push(this.dataArray.length)
}
this.dataArray = [...this.dataArray,...tmpArrays]
Logger.error(`dataArray =>${this.dataArray.length}`)
//Update sections itemsCount.
this.dataSection.itemsCount += CommonConstants.LOAD_MORE_COUNT;
this.sections.update(2, this.dataSection);
Logger.error(`sections =>${this.sections.length}`)
}
}, CommonConstants.LOAD_MORE_TIMEOUT);
}
getSize() {
let ret = Math.floor(Math.random() * this.maxSize);
return (ret > this.minSize ? ret : this.minSize);
}
setItemSizeArray() {
for (let i = 0; i < CommonConstants.REFRESH_COUNT; i++) {
this.itemHeightArray.push(this.getSize());
}
}
initSections() {
let sectionOptions: SectionOptions[] = [];
let count = 0;
let oneOrTwo = 0;
// let dataCount = this.dataSource.totalCount();
let dataCount = this.dataArray.length;
while (count < dataCount) {
if (dataCount - count < 96) {
this.dataSection.itemsCount = dataCount - count;
sectionOptions.push(this.dataSection);
break;
}
if (oneOrTwo++ % 2 === 0) {
sectionOptions.push(this.oneColumnSection);
count += this.oneColumnSection.itemsCount;
} else {
sectionOptions.push(this.twoColumnSection);
count += this.twoColumnSection.itemsCount;
}
}
this.sections.splice(0, 0, sectionOptions);
}
removeItem(item: number): void {
let index = this.dataArray.indexOf(item);
this.dataArray.splice(index, 1);
const sections: Array<SectionOptions> = this.sections.values();
let newSection: SectionOptions;
let tmpIndex = 0;
let sectionIndex = 0;
for(let i = 0; i < sections.length; i++) {
tmpIndex += sections[i].itemsCount;
if (index < tmpIndex) {
sectionIndex = i;
break;
}
}
newSection = sections[sectionIndex];
newSection.itemsCount -= 1;
if (newSection.crossCount && newSection.crossCount > newSection.itemsCount) {
newSection.crossCount = newSection.itemsCount;
}
this.sections.update(sectionIndex, newSection);
}
aboutToAppear() {
for (let i = 0; i < CommonConstants.REFRESH_COUNT; i++) {
this.dataArray.push(i);
}
this.setItemSizeArray();
this.initSections();
}
build() {
Column({ space: 0 }) {
Refresh({ refreshing: $$this.isRefreshing, builder: this.headerRefresh()}) {
WaterFlow({ scroller: this.scroller, sections: this.sections}) {
// LazyForEach(this.dataSource, (item: number) => {
// FlowItem() {
// this.buildItem(item)
// }
// .transition({ type: TransitionType.Delete, opacity: 0 })
// .priorityGesture(LongPressGesture()
// .onAction(() => {
// this.currentItem = item;
// }))
// .width(CommonConstants.FULL_WIDTH)
// .borderRadius($r('app.float.sections_item_radius'))
// .backgroundColor(Color.Gray)
// }, (item: string) => item)
Repeat(this.dataArray)
.each((repeatItem: RepeatItem<number>) =>{
FlowItem() {
this.buildItem(repeatItem.item)
}
.transition({ type: TransitionType.Delete, opacity: 0 })
.priorityGesture(LongPressGesture()
.onAction(() => {
this.currentItem = repeatItem.item;
}))
.width(CommonConstants.FULL_WIDTH)
.borderRadius($r('app.float.sections_item_radius'))
.backgroundColor(Color.Gray)
})
.virtualScroll({ totalCount: this.dataArray.length })
}
.cachedCount(CommonConstants.CACHED_COUNT)
.columnsGap($r('app.float.sections_margin'))
.rowsGap($r('app.float.sections_margin'))
//Fix last item not full display.
.padding({ bottom: $r('app.float.water_flow_margin_bottom') })
.width(CommonConstants.FULL_WIDTH)
.height(CommonConstants.FULL_HEIGHT)
.layoutWeight(1)
//For better experience, pre load data.
.onScrollIndex((first: number, last: number) => {
this.loadMore(last);
})
}
.refreshOffset(CommonConstants.REFRESH_OFFSET)
.onRefreshing(() => {
this.refresh();
})
}
}
@Builder
buildItem(item: number) {
Stack() {
Row() {
Button($r('app.string.delete'))
.fontColor(Color.Red)
.backgroundColor(Color.White)
.onClick(() => {
this.getUIContext().animateTo({ duration: CommonConstants.DELETE_ANIMATION_TIME }, () => {
this.removeItem(item);
});
});
}
.width(CommonConstants.FULL_WIDTH)
.height(CommonConstants.FULL_HEIGHT)
.borderRadius($r('app.float.sections_item_radius'))
.justifyContent(FlexAlign.Center)
.zIndex(1)
.visibility(this.currentItem === item ? Visibility.Visible : Visibility.Hidden)
.backgroundColor($r('app.color.delete_background_color'));
ReusableFlowItem({ item: item });
}
}
}
更多关于HarmonyOS鸿蒙Next中WaterFlow的WaterFlowSections使用分组能力和Repeat的懒加载是不是不兼容?的实战教程也可以访问 https://www.itying.com/category-93-b0.html
开发者你好,Repeat需要配合状态管理V2使用,由于WaterFlowSections定义在框架中,开发者无法使用@Trace标注其属性,此时可以使用makeObserved替代。
【修改建议】
一般情况下可以使用@Trace装饰器修饰复杂对象的内部属性达到观察效果,但是自定义sections的类型已经封装好了,无法使用@Trace。此时可以使用工具类UIUtils的makeObserved方法使得复杂对象sections的内部属性可观测。
import { UIUtils } from '@kit.ArkUI'
sections: WaterFlowSections = UIUtils.makeObserved(new WaterFlowSections())
【常见FAQ】
Q:makeObserve如何在@ComponentV1中使用呢?
A:makeObserved可以用在@Component装饰的自定义组件中,但不能和状态管理V1的状态变量装饰器配合使用,如果一起使用,则会抛出运行时异常。具体可参考makeObserved限制条件。
以下是根据你提供的官方demo修改后的代码:
/*
* Copyright (c) 2025 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CommonConstants } from '../common/constants/CommonConstants';
import Logger from '../common/utils/Logger';
import { SectionsWaterFlowDataSource } from '../viewmodel/SectionsWaterFlowDataSource';
import { UIUtils } from '@kit.ArkUI';
@Component
struct ReusableFlowItem {
@State item: number = 0;
// aboutToReuse(params: Record<string, number>) {
// this.item = params.item;
// }
build() {
RelativeContainer() {
Image($rawfile(`sections/${this.item % 4}.jpg`))
.objectFit(ImageFit.Cover)
.width(CommonConstants.FULL_WIDTH)
.layoutWeight(1)
.borderRadius($r('app.float.sections_item_radius'))
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
left: { anchor: '__container__', align: HorizontalAlign.Start },
right: { anchor: '__container__', align: HorizontalAlign.End }
})
.id('image1')
Stack() {
}
.linearGradient({
angle: 0,
colors: [[$r('app.color.linearGradient_first_color'), 0.0],
[$r('app.color.linearGradient_last_color'), 1.0]]
})
.width(CommonConstants.FULL_WIDTH)
.height($r('app.float.sections_item_blur_height'))
.borderRadius($r('app.float.sections_item_radius'))
.hitTestBehavior(HitTestMode.None)
.alignRules({
bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
left: { anchor: '__container__', align: HorizontalAlign.Start },
right: { anchor: '__container__', align: HorizontalAlign.End }
})
.id('mask1')
Text('NO. ' + (this.item + 1))
.fontSize($r('app.float.sections_item_text_size'))
.fontColor(Color.White)
.alignRules({
bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
left: { anchor: '__container__', align: HorizontalAlign.Start }
})
.margin({
left: $r('app.float.sections_item_text_margin_left'),
bottom: $r("app.float.sections_item_text_margin_bottom")
})
.id('text1')
}
.width(CommonConstants.FULL_WIDTH)
.borderRadius($r('app.float.sections_item_radius'))
.backgroundColor(Color.Gray)
}
}
@Preview
@ComponentV2
export struct SectionsPage {
isRefreshing: boolean = false;
currentItem: number = -1;
minSize: number = CommonConstants.FLOW_ITEM_MIN_HEIGHT;
maxSize: number = CommonConstants.FLOW_ITEM_MAX_HEIGHT;
scroller: Scroller = new Scroller();
@Local dataArray: number[] = []
private itemHeightArray: number[] = [];
sections: WaterFlowSections = UIUtils.makeObserved(new WaterFlowSections());
sectionMargin: Margin = {
top: 8,
left: 0,
bottom: 0,
right: 0
};
oneColumnSection: SectionOptions = {
itemsCount: 3,
crossCount: 1,
columnsGap: 5,
rowsGap: 10,
margin: this.sectionMargin,
onGetItemMainSizeByIndex: (index: number) => {
return CommonConstants.SECTION1_ITEM_SIZE;
}
};
twoColumnSection: SectionOptions = {
itemsCount: 2,
crossCount: 2,
margin: this.sectionMargin,
onGetItemMainSizeByIndex: (index: number) => {
return CommonConstants.SECTION2_ITEM_SIZE;
}
};
dataSection: SectionOptions = {
itemsCount: 20,
crossCount: 2,
margin: this.sectionMargin,
onGetItemMainSizeByIndex: (index: number) => {
return this.itemHeightArray[index % CommonConstants.REFRESH_COUNT];
}
};
@Builder
headerRefresh() {
Column() {
LoadingProgress()
.color(Color.Black)
.opacity(0.6)
.width($r('app.float.refresh_loading_width'))
.height($r('app.float.refresh_loading_height'))
}
.justifyContent(FlexAlign.Center)
}
refresh() {
this.currentItem = -1;
setTimeout(() => {
//Add new data.
this.dataArray = [];
let value = Math.floor(Math.random() * CommonConstants.REFRESH_COUNT);
for (let i = 0; i < CommonConstants.REFRESH_COUNT; i++) {
this.dataArray.push(i + value)
}
//Update sections itemsCount.
this.oneColumnSection.itemsCount = 3;
this.oneColumnSection.crossCount = 1;
this.twoColumnSection.itemsCount = 2;
this.twoColumnSection.crossCount = 2;
this.dataSection.itemsCount = 95;
this.dataSection.crossCount = 2;
this.sections.update(0, this.oneColumnSection);
this.sections.update(1, this.twoColumnSection);
this.sections.update(2, this.dataSection);
this.isRefreshing = false;
}, CommonConstants.REFRESH_TIMEOUT);
}
loadMore(last: number) {
setTimeout(() => {
let totalCount = this.dataArray.length;
const index: number = this.dataArray.length;
const tmpArrays: number[]= []
if (last + CommonConstants.LOAD_MORE_COUNT >= totalCount) {
for (let i = 0; i < CommonConstants.LOAD_MORE_COUNT; i++) {
// this.dataArray.splice(this.dataArray.length, 0, this.dataArray.length);
tmpArrays.push((index + i))
// this.dataArray.push(this.dataArray.length)
}
this.dataArray = [...this.dataArray,...tmpArrays]
Logger.error(`dataArray =>${this.dataArray.length}`)
//Update sections itemsCount.
this.dataSection.itemsCount += CommonConstants.LOAD_MORE_COUNT;
this.sections.update(2, this.dataSection);
Logger.error(`sections =>${this.sections.length}`)
}
}, CommonConstants.LOAD_MORE_TIMEOUT);
}
getSize() {
let ret = Math.floor(Math.random() * this.maxSize);
return (ret > this.minSize ? ret : this.minSize);
}
setItemSizeArray() {
for (let i = 0; i < CommonConstants.REFRESH_COUNT; i++) {
this.itemHeightArray.push(this.getSize());
}
}
initSections() {
let sectionOptions: SectionOptions[] = [];
let count = 0;
let oneOrTwo = 0;
// let dataCount = this.dataSource.totalCount();
let dataCount = this.dataArray.length;
while (count < dataCount) {
if (dataCount - count < 96) {
this.dataSection.itemsCount = dataCount - count;
sectionOptions.push(this.dataSection);
break;
}
if (oneOrTwo++ % 2 === 0) {
sectionOptions.push(this.oneColumnSection);
count += this.oneColumnSection.itemsCount;
} else {
sectionOptions.push(this.twoColumnSection);
count += this.twoColumnSection.itemsCount;
}
}
this.sections.splice(0, 0, sectionOptions);
}
removeItem(item: number): void {
let index = this.dataArray.indexOf(item);
this.dataArray.splice(index, 1);
const sections: Array<SectionOptions> = this.sections.values();
let newSection: SectionOptions;
let tmpIndex = 0;
let sectionIndex = 0;
for(let i = 0; i < sections.length; i++) {
tmpIndex += sections[i].itemsCount;
if (index < tmpIndex) {
sectionIndex = i;
break;
}
}
newSection = sections[sectionIndex];
newSection.itemsCount -= 1;
if (newSection.crossCount && newSection.crossCount > newSection.itemsCount) {
newSection.crossCount = newSection.itemsCount;
}
this.sections.update(sectionIndex, newSection);
}
aboutToAppear() {
for (let i = 0; i < CommonConstants.REFRESH_COUNT; i++) {
this.dataArray.push(i);
}
this.setItemSizeArray();
this.initSections();
}
build() {
Column({ space: 0 }) {
Refresh({ refreshing: $$this.isRefreshing, builder: this.headerRefresh()}) {
WaterFlow({ scroller: this.scroller, sections: this.sections}) {
// LazyForEach(this.dataSource, (item: number) => {
// FlowItem() {
// this.buildItem(item)
// }
// .transition({ type: TransitionType.Delete, opacity: 0 })
// .priorityGesture(LongPressGesture()
// .onAction(() => {
// this.currentItem = item;
// }))
// .width(CommonConstants.FULL_WIDTH)
// .borderRadius($r('app.float.sections_item_radius'))
// .backgroundColor(Color.Gray)
// }, (item: string) => item)
Repeat(this.dataArray)
.each((repeatItem: RepeatItem<number>) =>{
FlowItem() {
this.buildItem(repeatItem.item)
}
.transition({ type: TransitionType.Delete, opacity: 0 })
.priorityGesture(LongPressGesture()
.onAction(() => {
this.currentItem = repeatItem.item;
}))
.width(CommonConstants.FULL_WIDTH)
.borderRadius($r('app.float.sections_item_radius'))
.backgroundColor(Color.Gray)
})
.virtualScroll({ totalCount: this.dataArray.length })
}
.cachedCount(CommonConstants.CACHED_COUNT)
.columnsGap($r('app.float.sections_margin'))
.rowsGap($r('app.float.sections_margin'))
//Fix last item not full display.
.padding({ bottom: $r('app.float.water_flow_margin_bottom') })
.width(CommonConstants.FULL_WIDTH)
.height(CommonConstants.FULL_HEIGHT)
.layoutWeight(1)
//For better experience, pre load data.
.onScrollIndex((first: number, last: number) => {
this.loadMore(last);
})
}
.refreshOffset(CommonConstants.REFRESH_OFFSET)
.onRefreshing(() => {
this.refresh();
})
}
}
@Builder
buildItem(item: number) {
Stack() {
Row() {
Button($r('app.string.delete'))
.fontColor(Color.Red)
.backgroundColor(Color.White)
.onClick(() => {
this.getUIContext().animateTo({ duration: CommonConstants.DELETE_ANIMATION_TIME }, () => {
this.removeItem(item);
});
});
}
.width(CommonConstants.FULL_WIDTH)
.height(CommonConstants.FULL_HEIGHT)
.borderRadius($r('app.float.sections_item_radius'))
.justifyContent(FlexAlign.Center)
.zIndex(1)
.visibility(this.currentItem === item ? Visibility.Visible : Visibility.Hidden)
.backgroundColor($r('app.color.delete_background_color'));
ReusableFlowItem({ item: item });
}
}
}
更多关于HarmonyOS鸿蒙Next中WaterFlow的WaterFlowSections使用分组能力和Repeat的懒加载是不是不兼容?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
WaterFlowSections中splice全部和update会有性能区分吗
如果加上懒加载能力,之后分页面,就会报错
[(100000:100000:scope)] Children count = 100 and doesn't match the number provided in Sections, which is 120.
在HarmonyOS Next中,WaterFlowSections的分组能力与Repeat的懒加载机制存在不兼容性。WaterFlowSections要求数据在初始化时完全确定分组结构,而Repeat的懒加载会动态加载数据项,导致分组无法预先完整构建。这种设计差异使得两者无法协同工作,需避免在分组场景下使用懒加载的Repeat。
在HarmonyOS Next中,WaterFlowSections的分组能力与Repeat的懒加载确实存在兼容性问题。从您提供的代码可以看出:
- Repeat组件通过
virtualScroll实现懒加载,但这种方式与WaterFlowSections的分组机制存在冲突 - 数据更新问题:当使用
sections.update()更新分组配置时,Repeat组件可能无法正确响应数据变化 - 性能影响:Repeat的虚拟滚动与WaterFlowSections的分组布局计算会产生双重开销
建议解决方案:
- 使用
LazyForEach替代Repeat组件,LazyForEach与WaterFlowSections的兼容性更好 - 或者考虑使用单一的数据源管理方式,避免混合使用不同渲染策略
代码中注释掉的LazyForEach部分实际上是更推荐的实现方式,建议恢复使用LazyForEach来确保分组功能正常工作。

