Problem Description
When using LazyForEach to load List items, after reassigning the data array to refresh data and notifying the component to refresh via the onDataReloaded method, the List component stops scrolling after a certain distance. The issue is illustrated below:
Problem code example:
// Index.ets
import { BaseDataSource } from './BaseDataSource';
import { StringListModel } from './StringListModel';
@Entry
@Component
struct MyComponent {
private dataSource: BaseDataSource<string> = StringListModel.instance().getDataSource();
aboutToAppear() {
StringListModel.instance().load()
}
aboutToDisappear(): void {
StringListModel.instance().release()
}
build() {
Column() {
Button('Refresh Data - Add hello')
.onClick(() => {
StringListModel.instance().loadByKeyword('hello')
})
List({ space: 3 }) {
LazyForEach(this.dataSource, (item: string) => {
ListItem() {
Row() {
Text(item).fontSize(14)
.onAppear(() => {
console.info('appear:' + item )
})
}
.justifyContent(FlexAlign.Center)
.width('100%')
.margin({ left: 10, right: 10 })
}
}, (item: string) => item)
}
.height(0)
.layoutWeight(1)
.onReachEnd(() => {
StringListModel.instance().loadMore()
})
}
}
}
// BaseDataSource.ets
export class BaseDataSource<T> implements IDataSource {
private datas: Array<T> = []
private listeners: Array<DataChangeListener> = []
totalCount(): number {
return this.datas.length
}
getData(index: number): T {
return this.datas[index]
}
registerDataChangeListener(listener: DataChangeListener): void {
let index = this.listeners.indexOf(listener)
if (index < 0) {
this.listeners.push(listener)
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
let index = this.listeners.indexOf(listener)
if (index >= 0) {
this.listeners.splice(index, 1)
}
}
private notifyDataReloaded() {
for (let listener of this.listeners) {
listener.onDataReloaded()
}
}
private notifyDataAdd(index: number, addCount: number = 1) {
for (let listener of this.listeners) {
let addOperation: DataAddOperation = {
type: DataOperationType.ADD,
index,
count: addCount
}
listener.onDatasetChange([addOperation])
}
}
private notifyDataDelete(index: number, delCount: number = 1) {
for (let listener of this.listeners) {
let delOperation: DataDeleteOperation = {
type: DataOperationType.DELETE,
index,
count: delCount
}
listener.onDatasetChange([delOperation])
}
}
private notifyDataChange(index: number) {
for (let listener of this.listeners) {
listener.onDataChange(index)
}
}
setDatas(datas: Array<T>) {
this.datas = datas
this.notifyDataReloaded()
}
addData(data: T) {
this.addDatas([data])
}
addDatas(datas: Array<T>) {
let insertIndex = this.datas.length
let addCount = datas.length
this.datas.push(...datas)
this.notifyDataAdd(insertIndex, addCount)
}
delData(index: number) {
this.datas.splice(index, 1)
this.notifyDataDelete(index)
}
delAllData() {
let delCount = this.datas.length
this.datas = []
this.notifyDataDelete(0, delCount)
}
changeData(index: number, data: T) {
if (index < 0 || index >= this.datas.length) {
return
}
this.datas[index] = data
this.notifyDataChange(index)
}
release() {
this.datas = []
this.listeners = []
}
}
// DataLoader.ets
export class DataLoader {
static async getByPage(page: number, pageSize: number) {
let datas: Array<string> = []
for (let i = 0; i < pageSize; i++) {
datas.push(`Page ${page}-${i}`)
}
return datas
}
static async getByKeyWord(keyword: string, page: number, pageSize: number) {
let datas: Array<string> = []
for (let i = 0; i < pageSize; i++) {
datas.push(`${keyword}-Page ${page}-${i}`)
}
return datas
}
static async getByFirstLetter(letter: string, page: number, pageSize: number) {
let datas: Array<string> = []
for (let i = 0; i < pageSize; i++) {
datas.push(`${letter}-Page ${page}-${i}`)
}
return datas
}
}
// StringListModel.ets
import { BaseDataSource } from './BaseDataSource'
import { DataLoader } from './DataLoader'
const MIN_PAGE_SIZE = 30
export class StringListModel {
private static sInstance: StringListModel | null
private stringDataSource: BaseDataSource<string> = new BaseDataSource()
private curPage: number = 0
private pageSize: number = MIN_PAGE_SIZE
private hasMoreData: boolean = true
private keyword?: string
private firstLetter?: string
private constructor() {
}
static instance() {
if (!StringListModel.sInstance) {
StringListModel.sInstance = new StringListModel()
}
return StringListModel.sInstance
}
setPageSize(pageSize: number) {
if (pageSize >= MIN_PAGE_SIZE) {
this.pageSize = pageSize
}
}
getDataSource() {
return this.stringDataSource
}
load() {
this.keyword = undefined
this.firstLetter = undefined
this.curPage = 0
this.hasMoreData = true
this.loadData(true)
}
loadByKeyword(keyword: string) {
if (this.keyword !== undefined && this.keyword === keyword) {
return
}
this.keyword = keyword
this.firstLetter = undefined
this.curPage = 0
this.hasMoreData = true
this.loadData(true)
}
loadByFirstLetter(firstLetter: string) {
if (this.firstLetter !== undefined && this.firstLetter === firstLetter) {
return
}
this.firstLetter = firstLetter
this.keyword = undefined
this.curPage = 0
this.hasMoreData = true
this.loadData(true)
}
loadMore() {
if (!this.hasMoreData) {
return
}
this.curPage++
this.loadData(false)
}
private loadData(isFirstPage: boolean) {
let loadCallback = (datas: Array<string>) => {
if (!datas || datas.length === 0) {
this.hasMoreData = false
return
}
this.hasMoreData = datas.length === this.pageSize
if (isFirstPage && this.stringDataSource.totalCount() > 0) {
this.stringDataSource.setDatas(datas)
} else {
this.stringDataSource.addDatas(datas)
}
}
if (this.keyword !== undefined) {
DataLoader.getByKeyWord(this.keyword, this.curPage, this.pageSize).then(loadCallback)
} else if (this.firstLetter !== undefined) {
DataLoader.getByFirstLetter(this.firstLetter, this.curPage, this.pageSize).then(loadCallback)
} else {
DataLoader.getByPage(this.curPage, this.pageSize).then(loadCallback)
}
}
release() {
this.stringDataSource.release()
StringListModel.sInstance = null
}
}
Background Knowledge
- onDataReloaded notifies the component to reload all data. Items with unchanged keys reuse existing subcomponents, while those with changed keys are rebuilt.
- onDatasetChange is used for batch data operations. It notifies the component to refresh according to the provided dataOperations array and cannot be mixed with other data manipulation interfaces.
Troubleshooting Process
From the provided code, the custom method notifyDataReloaded calls onDataReloaded to reload data. When onDataReloaded is triggered, subcomponents whose keys remain the same are reused rather than rebuilt. Since refreshing data through onDataReloaded does not change keys, LazyForEach reuses old subcomponents instead of recreating them, causing the List to stop scrolling after a certain distance.
Analysis Conclusion
When refreshing data, a data addition operation should be performed to rebuild subcomponents rather than reuse existing ones.
Solution
- Solution 1:
Replace onDataReloaded with onDatasetChange in the notifyDataReloaded method. onDatasetChange supports batch data operations (additions, reloads, etc.) and avoids subcomponent reuse.
private notifyDataReloaded() {
for (let listener of this.listeners) {
let reloadOperation: DataReloadOperation = {
type: DataOperationType.RELOAD
}
listener.onDatasetChange([reloadOperation])
}
}
- Solution 2: Add data insertion functionality in the setDatas method that calls notifyDataReloaded.
setDatas(datas: Array<T>) {
this.datas = datas
let insertIndex = this.datas.length
let addCount = datas.length
this.datas.push(...datas)
this.notifyDataAdd(insertIndex, addCount)
}
- Solution 3: Modify setDatas to first delete all data before adding new ones.
if (isFirstPage && this.stringDataSource.totalCount() > 0) {
this.stringDataSource.delAllData()
this.stringDataSource.addDatas(datas)
} else {
this.stringDataSource.addDatas(datas)
}
When refreshing data, a data addition operation should be performed to rebuild subcomponents rather than reuse existing ones.
Verification Result
All three approaches achieve the same corrective effect, as shown below:


Top comments (0)