DEV Community

HarmonyOS
HarmonyOS

Posted on

How to Solve the Problem of List Not Scrolling After Refreshing Data in LazyForEach

Read the original article:How to Solve the Problem of List Not Scrolling After Refreshing Data in LazyForEach

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:

9.gif

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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])
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • 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)
  }
Enter fullscreen mode Exit fullscreen mode
  • 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)
  }
Enter fullscreen mode Exit fullscreen mode

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:

s13.gif

Written by Bunyamin Akcay

Top comments (0)