文档管理中心

如何解决LazyForEach刷新数据时List无法滑动问题

问题现象

通过LazyForEach加载的List列表项,给数据数组重新赋值刷新数据之后,并通过onDataReloaded方法通知组件重新刷新时,List组件滑动一段距离之后无法继续滑动。

问题代码示例参考如下:

收起
自动换行
深色代码主题
复制
  1. import { BaseDataSource } from './BaseDataSource';
  2. import { StringListModel } from './StringListModel';
  3. @Entry
  4. @Component
  5. struct MyComponent {
  6. private dataSource: BaseDataSource<string> = StringListModel.instance().getDataSource();
  7. aboutToAppear() {
  8. StringListModel.instance().load();
  9. }
  10. aboutToDisappear(): void {
  11. StringListModel.instance().release();
  12. }
  13. build() {
  14. Column() {
  15. Button('刷新数据-添加hello')
  16. .onClick(() => {
  17. StringListModel.instance().loadByKeyword('hello');
  18. });
  19. List({ space: 3 }) {
  20. LazyForEach(this.dataSource, (item: string) => {
  21. ListItem() {
  22. Row() {
  23. Text(item).fontSize(50)
  24. .onAppear(() => {
  25. console.info('appear:', `${item}`);
  26. });
  27. }
  28. .justifyContent(FlexAlign.Center)
  29. .width('100%')
  30. .margin({ left: 10, right: 10 });
  31. };
  32. }, (item: string) => item);
  33. }
  34. .height(0)
  35. .layoutWeight(1)
  36. .onReachEnd(() => {
  37. StringListModel.instance().loadMore();
  38. });
  39. };
  40. }
  41. }

BaseDataSource.ets:

收起
自动换行
深色代码主题
复制
  1. export class BaseDataSource<T> implements IDataSource {
  2. // 存储实际数据的数组。
  3. private datas: Array<T> = [];
  4. // 存储所有已注册的数据变更监听器。
  5. private listeners: Array<DataChangeListener> = [];
  6. totalCount(): number {
  7. return this.datas.length;
  8. }
  9. getData(index: number): T {
  10. return this.datas[index];
  11. }
  12. registerDataChangeListener(listener: DataChangeListener): void {
  13. let index = this.listeners.indexOf(listener);
  14. if (index < 0) {
  15. this.listeners.push(listener);
  16. }
  17. }
  18. unregisterDataChangeListener(listener: DataChangeListener): void {
  19. let index = this.listeners.indexOf(listener);
  20. if (index >= 0) {
  21. this.listeners.splice(index, 1);
  22. }
  23. }
  24. private notifyDataReloaded() {
  25. for (let listener of this.listeners) {
  26. listener.onDataReloaded();
  27. }
  28. }
  29. private notifyDataAdd(index: number, addCount: number = 1) {
  30. for (let listener of this.listeners) {
  31. let addOperation: DataAddOperation = {
  32. type: DataOperationType.ADD,
  33. index,
  34. count: addCount
  35. };
  36. listener.onDatasetChange([addOperation]);
  37. }
  38. }
  39. private notifyDataDelete(index: number, delCount: number = 1) {
  40. for (let listener of this.listeners) {
  41. let delOperation: DataDeleteOperation = {
  42. type: DataOperationType.DELETE,
  43. index,
  44. count: delCount
  45. };
  46. listener.onDatasetChange([delOperation]);
  47. }
  48. }
  49. private notifyDataChange(index: number) {
  50. for (let listener of this.listeners) {
  51. listener.onDataChange(index);
  52. }
  53. }
  54. setDatas(datas: Array<T>) {
  55. this.datas = datas;
  56. this.notifyDataReloaded();
  57. }
  58. addData(data: T) {
  59. this.addDatas([data]);
  60. }
  61. addDatas(datas: Array<T>) {
  62. let insertIndex = this.datas.length;
  63. let addCount = datas.length;
  64. this.datas.push(...datas);
  65. this.notifyDataAdd(insertIndex, addCount);
  66. }
  67. delData(index: number) {
  68. this.datas.splice(index, 1);
  69. this.notifyDataDelete(index);
  70. }
  71. delAllData() {
  72. let delCount = this.datas.length;
  73. this.datas = [];
  74. this.notifyDataDelete(0, delCount);
  75. }
  76. changeData(index: number, data: T) {
  77. if (index < 0 || index >= this.datas.length) {
  78. return;
  79. }
  80. this.datas[index] = data;
  81. this.notifyDataChange(index);
  82. }
  83. release() {
  84. this.datas = [];
  85. this.listeners = [];
  86. }
  87. }

DataLoader.ets:

收起
自动换行
深色代码主题
复制
  1. export class DataLoader {
  2. static async getByPage(page: number, pageSize: number) {
  3. let datas: Array<string> = []
  4. for (let i = 0; i < pageSize; i++) {
  5. datas.push(`${page}-${i}`)
  6. }
  7. return datas
  8. }
  9. static async getByKeyWord(keyword: string, page: number, pageSize: number) {
  10. let datas: Array<string> = []
  11. for (let i = 0; i < pageSize; i++) {
  12. datas.push(`${keyword}-${page}-${i}`)
  13. }
  14. return datas
  15. }
  16. static async getByFirstLetter(letter: string, page: number, pageSize: number) {
  17. let datas: Array<string> = []
  18. for (let i = 0; i < pageSize; i++) {
  19. datas.push(`${letter}-${page}-${i}`)
  20. }
  21. return datas
  22. }
  23. }

StringListModel.ets:

收起
自动换行
深色代码主题
复制
  1. import { BaseDataSource } from './BaseDataSource';
  2. import { DataLoader } from './DataLoader';
  3. const MIN_PAGE_SIZE = 30;
  4. export class StringListModel {
  5. private static sInstance: StringListModel | null;
  6. private stringDataSource: BaseDataSource<string> = new BaseDataSource();
  7. private curPage: number = 0;
  8. private pageSize: number = MIN_PAGE_SIZE;
  9. private hasMoreData: boolean = true;
  10. private keyword?: string;
  11. private firstLetter?: string;
  12. private constructor() {
  13. }
  14. static instance() {
  15. if (!StringListModel.sInstance) {
  16. StringListModel.sInstance = new StringListModel();
  17. }
  18. return StringListModel.sInstance;
  19. }
  20. // 设置每页加载的数量。
  21. setPageSize(pageSize: number) {
  22. if (pageSize >= MIN_PAGE_SIZE) {
  23. this.pageSize = pageSize;
  24. }
  25. }
  26. getDataSource() {
  27. return this.stringDataSource;
  28. }
  29. // 加载第一页成语数据。
  30. load() {
  31. this.keyword = undefined;
  32. this.firstLetter = undefined;
  33. this.curPage = 0;
  34. this.hasMoreData = true;
  35. this.loadData(true);
  36. }
  37. // 通过关键字来查找并加载第一页成语数据。
  38. loadByKeyword(keyword: string) {
  39. if (this.keyword !== undefined && this.keyword === keyword) {
  40. return;
  41. }
  42. this.keyword = keyword;
  43. this.firstLetter = undefined;
  44. this.curPage = 0;
  45. this.hasMoreData = true;
  46. this.loadData(true);
  47. }
  48. // 通过拼音首字母来查找并加载第一页成语数据。
  49. loadByFirstLetter(firstLetter: string) {
  50. if (this.firstLetter !== undefined && this.firstLetter === firstLetter) {
  51. return;
  52. }
  53. this.firstLetter = firstLetter;
  54. this.keyword = undefined;
  55. this.curPage = 0;
  56. this.hasMoreData = true;
  57. this.loadData(true);
  58. }
  59. // 加载更多成语数据。
  60. loadMore() {
  61. if (!this.hasMoreData) {
  62. return;
  63. }
  64. this.curPage++;
  65. this.loadData(false);
  66. }
  67. private loadData(isFirstPage: boolean) {
  68. let loadCallback = (datas: Array<string>) => {
  69. if (!datas || datas.length === 0) {
  70. this.hasMoreData = false;
  71. return;
  72. }
  73. this.hasMoreData = datas.length === this.pageSize;
  74. if (isFirstPage && this.stringDataSource.totalCount() > 0) {
  75. this.stringDataSource.setDatas(datas);
  76. } else {
  77. this.stringDataSource.addDatas(datas);
  78. }
  79. };
  80. if (this.keyword !== undefined) {
  81. DataLoader.getByKeyWord(this.keyword, this.curPage, this.pageSize).then(loadCallback);
  82. } else if (this.firstLetter !== undefined) {
  83. DataLoader.getByFirstLetter(this.firstLetter, this.curPage, this.pageSize).then(loadCallback);
  84. } else {
  85. DataLoader.getByPage(this.curPage, this.pageSize).then(loadCallback);
  86. }
  87. }
  88. // 在页面销毁时调用。
  89. release() {
  90. this.stringDataSource.release();
  91. StringListModel.sInstance = null;
  92. }
  93. }

问题效果预览:

效果预览

背景知识

  • onDataReloaded能够通知组件重新加载所有数据,键值没有变化的数据项会使用原先的子组件,键值发生变化的会重建子组件,重新加载数据完成后调用。
  • onDatasetChange进行批量的数据处理后,调用onDatasetChange接口通知组件按照dataOperations刷新组件,与其它操作数据的接口不能混用。

问题定位

由问题代码可知,自定义方法notifyDataReloaded调用了onDataReloaded通知数据的重新加载,而调用onDataReloaded时,键值没有变化的数据项会使用原先的子组件,键值发生变化的会重建子组件,而单纯的调用onDataReloaded刷新数据并不会更新键值,因此在问题图中,当LazyForEach刷新数据之后,由于没有继续重建子组件,而是复用了原先的子组件,导致List列表项下滑到一定程度就无法继续下滑。

分析结论

在刷新数据时需要进行数据添加操作,使子组件得以重建而非复用。

修改建议

  • 方案一:在自定义方法notifyDataReloaded中使用onDatasetChange方法代替onDataReloaded进行组件重新加载数据,onDatasetChange能够进行批量的数据处理,包括数据添加、重载所有数据等操作,可以避免复用原先的子组件。也可以对调用notifyDataReloaded方法的setDatas方法添加数据添加功能,修改后的BaseDataSource:
    收起
    自动换行
    深色代码主题
    复制
    1. export class BaseDataSource<T> implements IDataSource {
    2. private datas: Array<T> = [];
    3. private listeners: Array<DataChangeListener> = [];
    4. totalCount(): number {
    5. return this.datas.length;
    6. }
    7. getData(index: number): T {
    8. return this.datas[index];
    9. }
    10. registerDataChangeListener(listener: DataChangeListener): void {
    11. let index = this.listeners.indexOf(listener);
    12. if (index < 0) {
    13. this.listeners.push(listener);
    14. }
    15. }
    16. unregisterDataChangeListener(listener: DataChangeListener): void {
    17. let index = this.listeners.indexOf(listener);
    18. if (index >= 0) {
    19. this.listeners.splice(index, 1);
    20. }
    21. }
    22. private notifyDataReloaded() {
    23. for (let listener of this.listeners) {
    24. let reloadOperation: DataReloadOperation = {
    25. type: DataOperationType.RELOAD
    26. };
    27. listener.onDatasetChange([reloadOperation]);
    28. }
    29. }
    30. private notifyDataAdd(index: number, addCount: number = 1) {
    31. for (let listener of this.listeners) {
    32. let addOperation: DataAddOperation = {
    33. type: DataOperationType.ADD,
    34. index,
    35. count: addCount
    36. };
    37. listener.onDatasetChange([addOperation]);
    38. }
    39. }
    40. private notifyDataDelete(index: number, delCount: number = 1) {
    41. for (let listener of this.listeners) {
    42. let delOperation: DataDeleteOperation = {
    43. type: DataOperationType.DELETE,
    44. index,
    45. count: delCount
    46. };
    47. listener.onDatasetChange([delOperation]);
    48. }
    49. }
    50. private notifyDataChange(index: number) {
    51. for (let listener of this.listeners) {
    52. listener.onDataChange(index);
    53. }
    54. }
    55. setDatas(datas: Array<T>) {
    56. this.datas = datas;
    57. let insertIndex = this.datas.length;
    58. let addCount = datas.length;
    59. this.datas.push(...datas);
    60. this.notifyDataAdd(insertIndex, addCount);
    61. }
    62. addData(data: T) {
    63. this.addDatas([data]);
    64. }
    65. addDatas(datas: Array<T>) {
    66. let insertIndex = this.datas.length;
    67. let addCount = datas.length;
    68. this.datas.push(...datas);
    69. this.notifyDataAdd(insertIndex, addCount);
    70. }
    71. delData(index: number) {
    72. this.datas.splice(index, 1);
    73. this.notifyDataDelete(index);
    74. }
    75. delAllData() {
    76. let delCount = this.datas.length;
    77. this.datas = [];
    78. this.notifyDataDelete(0, delCount);
    79. }
    80. changeData(index: number, data: T) {
    81. if (index < 0 || index >= this.datas.length) {
    82. return;
    83. }
    84. this.datas[index] = data;
    85. this.notifyDataChange(index);
    86. }
    87. release() {
    88. this.datas = [];
    89. this.listeners = [];
    90. }
    91. }
  • 方案二:将setDatas方法替换为先删除后再添加的方法,修改后StringListModel:
    收起
    自动换行
    深色代码主题
    复制
    1. import { BaseDataSource } from './BaseDataSource';
    2. import { DataLoader } from './DataLoader';
    3. const MIN_PAGE_SIZE = 30;
    4. export class StringListModel {
    5. private static sInstance: StringListModel | null;
    6. private stringDataSource: BaseDataSource<string> = new BaseDataSource();
    7. private curPage: number = 0;
    8. private pageSize: number = MIN_PAGE_SIZE;
    9. private hasMoreData: boolean = true;
    10. private keyword?: string;
    11. private firstLetter?: string;
    12. private constructor() {
    13. }
    14. static instance() {
    15. if (!StringListModel.sInstance) {
    16. StringListModel.sInstance = new StringListModel();
    17. }
    18. return StringListModel.sInstance;
    19. }
    20. /**
    21. * 设置每页加载的数量
    22. * @param pageSize 每页加载数量不能小于最小加载数量
    23. */
    24. setPageSize(pageSize: number) {
    25. if (pageSize >= MIN_PAGE_SIZE) {
    26. this.pageSize = pageSize;
    27. }
    28. }
    29. getDataSource() {
    30. return this.stringDataSource;
    31. }
    32. /**
    33. * 加载第一页成语数据
    34. */
    35. load() {
    36. this.keyword = undefined;
    37. this.firstLetter = undefined;
    38. this.curPage = 0;
    39. this.hasMoreData = true;
    40. this.loadData(true);
    41. }
    42. /**
    43. * 通过关键字来查找并加载第一页成语数据
    44. * @param keyword
    45. */
    46. loadByKeyword(keyword: string) {
    47. if (this.keyword !== undefined && this.keyword === keyword) {
    48. return;
    49. }
    50. this.keyword = keyword;
    51. this.firstLetter = undefined;
    52. this.curPage = 0;
    53. this.hasMoreData = true;
    54. this.loadData(true);
    55. }
    56. /**
    57. * 通过拼音首字母来查找并加载第一页成语数据
    58. * @param firstLetter
    59. */
    60. loadByFirstLetter(firstLetter: string) {
    61. if (this.firstLetter !== undefined && this.firstLetter === firstLetter) {
    62. return;
    63. }
    64. this.firstLetter = firstLetter;
    65. this.keyword = undefined;
    66. this.curPage = 0;
    67. this.hasMoreData = true;
    68. this.loadData(true);
    69. }
    70. /**
    71. * 加载更多成语数据
    72. */
    73. loadMore() {
    74. if (!this.hasMoreData) {
    75. return;
    76. }
    77. this.curPage++;
    78. this.loadData(false);
    79. }
    80. private loadData(isFirstPage: boolean) {
    81. let loadCallback = (datas: Array<string>) => {
    82. if (!datas || datas.length === 0) {
    83. this.hasMoreData = false;
    84. return;
    85. }
    86. this.hasMoreData = datas.length === this.pageSize;
    87. if (isFirstPage && this.stringDataSource.totalCount() > 0) {
    88. this.stringDataSource.delAllData();
    89. this.stringDataSource.addDatas(datas);
    90. } else {
    91. this.stringDataSource.addDatas(datas);
    92. }
    93. };
    94. if (this.keyword !== undefined) {
    95. DataLoader.getByKeyWord(this.keyword, this.curPage, this.pageSize).then(loadCallback);
    96. } else if (this.firstLetter !== undefined) {
    97. DataLoader.getByFirstLetter(this.firstLetter, this.curPage, this.pageSize).then(loadCallback);
    98. } else {
    99. DataLoader.getByPage(this.curPage, this.pageSize).then(loadCallback);
    100. }
    101. }
    102. /**
    103. * 释放。可在页面销毁时调用,如page页中的aboutToDisappear方法中
    104. */
    105. release() {
    106. this.stringDataSource.release();
    107. StringListModel.sInstance = null;
    108. }
    109. }

常见FAQ

Q:为什么onDatasetChange方法不能与其它操作数据的接口混用?

A:onDatasetChange方法用于通知LazyForEach组件进行批量的数据处理。该方法接受一个包含多个数据操作的数组作为参数。这些操作可以包括数据的添加、删除、移动等。当在onDatasetChange中传入包含删除操作的数据集时,系统会认为这是一个不兼容的操作,从而导致崩溃。因此onDatasetChange方法不能与其他操作数据的接口混用。

Q:使用onDatasetChange方法有哪些注意点?

A:调用一次onDatasetChange时,每个index对应的数据只能被操作一次。如果多次操作同一个index,LazyForEach仅生效第一次操作。并且onDatasetChange不能与其他DataChangeListener的更新接口混用。如在同一个LazyForEach中,调用过onDataAdd接口后,不能再调用onDatasetChange接口;反之,调用过onDatasetChange接口后,也不能调用onDataAdd等其他更新接口。页面中不同LazyForEach之间互不影响。具体可参考官网指南LazyForEach中的数据更新、onDatasetChange。

在 FAQ 中进行搜索
请输入您想要搜索的关键词