Flutter到鸿蒙,不是有手就行吗? (列表加载更多)

前言

接着上一篇 Flutter到鸿蒙,不是有手就行吗? (下拉刷新) - 掘金 (juejin.cn),列表是一个应用中常见的一种布局,而向上拖拽加载更多内容,是一种通用的做法。

在 Flutter 中你可以通过 loading_more_list 来快速支持加载更多效果。

在鸿蒙中你则可以使用 loading_more_list 来实现。

安装

ohpm install @candies/loading_more_list

列表状态

IndicatorStatus

我们一个列表一共有 7 种状态。

typescript 复制代码
export enum IndicatorStatus {
  none, // 初始化状态
  loadingMoreBusying, // 正在加载更多数据的状态
  fullScreenBusying, // 列表第一次加载数据之前的全屏的加载动画状态
  loadingMoreError, // 加载更多失败状态
  fullScreenError, // 全屏加载失败的状态
  noMoreLoad,  // 已经没有更多数据的状态
  empty // 列表没有数据的状态
}

而它们又可以分成 3 种大的场景

  1. 初始的状态
  • none
  1. 列表第一次加载无数据之前下状态
  • fullScreenBusying
  • fullScreenError
  • empty
  1. 列表有数据,在列表展示形式下的状态
  • loadingMoreBusying
  • loadingMoreError
  • noMoreLoad

这 3 种的绘制是利用在数据源的最后手动添加一项来实现的。

关键代码是 LoadingMoreBase 中的 totalCount 和 getData 的方法。

typescript 复制代码
lastItemIsLoadingMoreItem: boolean = true;

totalCount(): number {
  return this.length + (this.lastItemIsLoadingMoreItem ? 1 : 0);
}

getData(index: number): T | LoadingMoreItem {
  if (0 <= index && index < this.length)
    return this[index];
  if (!this.hasMore) {
    return new NoMoreLoadItem();
  }
  else if (this.indicatorStatus == IndicatorStatus.loadingMoreError) {
    return new LoadingMoreErrorItem();
  }
  else {
    // auto load more
    if (this.indicatorStatus != IndicatorStatus.loadingMoreBusying) {
      this.loadMore();
    }
    return new LoadingMoreBusyingItem();
  }
}

准备数据源

LoadingMoreBase

你需要继承 LoadingMoreBase<T> 来实现加载更多的数据源. 通过重写 loadData 方法来加载数据. 当没有数据的时候记得把 hasMore 设置为 false.

下面是一个数据源的例子

  • 重写 refresh 方法来初始化初始的值,可以配合下拉刷新调用 refresh 方法来刷新整个列表。
  • 重写 loadData方法来提供加载数据的逻辑,以及 hasMore 的判断。如果加载成功,使用 this.addAll 追加新加载的数据,并且返回 true ; 如果加载失败返回 false 。
typescript 复制代码
import {
  LoadingMoreBase,
} from '@candies/loading_more_list'
import { FeedList, TuChongSource } from './TuChongSource';
import http from '@ohos.net.http';

export class TuChongRepository extends LoadingMoreBase<FeedList> {
  public  hasMore: boolean = true;
  page: number = 1;

  public async refresh(notifyStateChanged: boolean = false): Promise<boolean> {
    this.page = 1;
    this.hasMore = true;
    return super.refresh(notifyStateChanged);
  }

  public async loadData(isLoadMoreAction: boolean): Promise<boolean> {
    try {
      let url = '';
      if (this.length == 0) {
        url = 'https://api.tuchong.com/feed-app';
      } else {
        let lastPostId = (this[this.length - 1] as FeedList).post_id;
        url =
          `https://api.tuchong.com/feed-app?post_id=${lastPostId}&page=${this.page}&type=loadmore`;
      }
      let request = http.createHttp();
      let response: http.HttpResponse = await request.request(url);

      var feedList = (JSON.parse(response.result as string) as TuChongSource).feedList;

      this.addAll(feedList);
      this.hasMore = !(feedList.length == 0 || this.length > 50);
      this.page++;
      return true;
      // test for loading more ui
      // return new Promise<boolean>((resolve) => {
      //   setTimeout(() => {
      //     this.addAll(feedList);
      //     this.hasMore = !(feedList.length == 0 || this.length > 20);
      //     this.page++;
      //     resolve(true);
      //   }, 2000);
      // });
    }
    catch (e) {
      return false;
    }
  }
}

使用

导入引用

typescript 复制代码
import {
  LoadingMoreList,
  LoadingMoreBase,
  IndicatorWidget,
  IndicatorStatus,
} from '@candies/loading_more_list'

LoadingMoreList

LoadingMoreList 是我们的加载更多组件,它的参数如下:

typescript 复制代码
/// 列表,可以是 List,Grid 或者 WaterFlow
@BuilderParam
private builder: () => void;
/// 列表的数据源
@Link sourceList: LoadingMoreBase<any>;
/// 列表的状态创建器, 只针对 [IndicatorStatus.fullScreenBusying,IndicatorStatus.fullScreenError,IndicatorStatus.empty]
@BuilderParam
indicatorBuilder?: ($$: {
  indicatorStatus: IndicatorStatus,
  sourceList: LoadingMoreBase<any>,
}) => void = this.buildIndicator;

例子

准备一个简单的数据源。

typescript 复制代码
import {
  LoadingMoreBase,
} from '@candies/loading_more_list'

export class ListData extends LoadingMoreBase<number> {
  hasMore: boolean = true;
  pageSize: number = 10;
  maxCount: number = 20;
  
  public async refresh(notifyStateChanged: boolean = false): Promise<boolean> {
    this.hasMore = true;
    return super.refresh(notifyStateChanged);
  }
  
  async loadData(isLoadMoreAction: boolean): Promise<boolean> {
    // 模拟请求延迟 1 秒
    return new Promise<boolean>((resolve) => {
      setTimeout(() => {
          var length = this.length;
          let list = [];
     
          for (let index = length; index < length + this.pageSize; index++)          {
            list.push(index);
          }
          this.addAll(list);
        
        if (this.length >= this.maxCount) {
          this.hasMore = false;
        }
        resolve(this.isSuccess);
      }, 1000);
    });
  }
}

列表(List)

我们只需要关注超出列表长度的元素构建的情况。如果在构建列表元素的时候发现这是一个 LoadingMoreItem ,那么就可以利用下面的方法创建对应的状态下的 UI 。

typescript 复制代码
          if (this.listData.isLoadingMoreItem(item))
            IndicatorWidget({
              indicatorStatus: this.listData.getLoadingMoreItemStatus(item),
              sourceList: this.listData,
            })

完整代码如下:

typescript 复制代码
import { LoadingMoreList, LoadingMoreBase, IndicatorWidget } from '@candies/loading_more_list'

@Entry
@Component
struct LoadingMoreListDemo {
  @State listData: ListData = new ListData();

  @Builder
  buildList() {
    List() {
      LazyForEach(this.listData, (item, index) => {
        ListItem() {
          // index == this.listData.length
          if (this.listData.isLoadingMoreItem(item))
            IndicatorWidget({
              indicatorStatus: this.listData.getLoadingMoreItemStatus(item),
              sourceList: this.listData,
            })
          else
            Text(`${item}`,).align(Alignment.Center).height(100)
        }.width('100%')
      },
        (item, index) => {
          return `${item}`
        }
      )
    }
    .flexGrow(1)
    .onReachEnd(() => {
      this.listData.loadMore();
    })
  }

  build() {
    Navigation() {
      LoadingMoreList({
        sourceList: this.listData,
        builder: this.buildList.bind(this),
      })
    }
    .title('LoadingMoreListDemo').titleMode(NavigationTitleMode.Mini)
  }
}

在 List 触发 onReachEnd 回调的时候,你可以手动调用 loadMore 方法。当然你也可以不手动调用,因为 LoadingMoreBase 已经自动调用过了。区别就是如果加载更多失败了,你加了这个手动调用的话,你可以通过上拉,再次触发加载更多。否则,只能依靠比如点击再次去调用 loadMore。

表格(Grid)

跟列表的类似,不过要注意最后一个元素构建有点区别。你需要为最后一个元素 GridItem 设置 columnStart 和 columnEnd 来实现元素跨列,让它占用整个一行(当然了,这个是通常的情况,你也可以根据你自身的情况设置)

typescript 复制代码
import { LoadingMoreList, LoadingMoreBase, IndicatorWidget } from '@candies/loading_more_list'

@Entry
@Component
struct LoadingMoreGridDemo {
  @State listData: ListData = new ListData();

  aboutToAppear() {
    this.listData.pageSize = 50;
    this.listData.maxCount = 100;
  }

  @Builder
  buildList() {
    Grid() {
      LazyForEach(this.listData, (item, index) => {
        // index == this.listData.length
        if (this.listData.isLoadingMoreItem(item))
        GridItem() {
          IndicatorWidget({
            indicatorStatus: this.listData.getLoadingMoreItemStatus(item),
            sourceList: this.listData,
          })
        }
        // loading more item take one row, you can define it base on your case
        .columnStart(0).columnEnd(4)
        else
        GridItem() {
          Text(`${item}`,).align(Alignment.Center)
        }.height(100).width('100%')
      },
        (item, index) => {
          return `${item}`
        }
      )
    }
    .flexGrow(1)
    .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
    .columnsGap(10)
    .rowsGap(10)

    // api10
    // .onReachEnd(() => {
    //   this.listData.loadMore();
    //
    // })
  }

  build() {
    Navigation() {
      LoadingMoreList({
        sourceList: this.listData,
        builder: this.buildList.bind(this),
      })
    }
    .title('LoadingMoreGridDemo').titleMode(NavigationTitleMode.Mini)
  }
}

瀑布流(WaterFlow)

官方的瀑布流提供了 footer 回调,可以用于创建最后一个元素的样式。

首先我们需要把 lastItemIsLoadingMoreItem 设置成 false 。

this.listData.lastItemIsLoadingMoreItem = false;

然后利用 footer 回调来创建加载更多的组件。

typescript 复制代码
  @Builder
  buildFooter() {
    if (!this.listData.hasMore)
      IndicatorWidget({
        indicatorStatus: IndicatorStatus.noMoreLoad,
      })
    else if (this.listData.indicatorStatus == IndicatorStatus.loadingMoreError)
      IndicatorWidget({
        indicatorStatus: IndicatorStatus.loadingMoreError,
        sourceList: this.listData,
      })
    else
      IndicatorWidget({
        indicatorStatus: IndicatorStatus.loadingMoreBusying,
      })
  }

完整代码如下:

typescript 复制代码
import { LoadingMoreList, IndicatorWidget, IndicatorStatus } from '@candies/loading_more_list'

@Entry
@Component
struct LoadingMoreWaterFlowDemo {
 @State listData: TuChongRepository = new TuChongRepository();

 aboutToAppear() {
   this.listData.lastItemIsLoadingMoreItem = false;
 }

 @Builder
 buildFooter() {
   if (!this.listData.hasMore)
     IndicatorWidget({
       indicatorStatus: IndicatorStatus.noMoreLoad,
     })
   else if (this.listData.indicatorStatus == IndicatorStatus.loadingMoreError)
     IndicatorWidget({
       indicatorStatus: IndicatorStatus.loadingMoreError,
       sourceList: this.listData,
     })
   else
     IndicatorWidget({
       indicatorStatus: IndicatorStatus.loadingMoreBusying,
     })
 }

 @Builder
 buildList() {
   WaterFlow({
     footer: this.buildFooter.bind(this)
   }) {
     LazyForEach(this.listData, (item, index) => {
       FlowItem() {
         TuChongImageListItem({ item: item, index: index })
       }
     },
       (item, index) => {
         var feedList = item as FeedList;
         if ('post_id' in feedList) {
           return feedList.post_id;
         }
         return `${item}`
       }
     )
   }
   .columnsTemplate("1fr 1fr")
   .columnsGap(10)
   .rowsGap(5)
   .flexGrow(1)
   .onReachEnd(() => {
     this.listData.loadMore();
   })
 }

 build() {
   Navigation() {
     LoadingMoreList({
       sourceList: this.listData,
       builder: this.buildList.bind(this),
     })
   }
   .title('LoadingMoreWaterFlowDemo').titleMode(NavigationTitleMode.Mini)
 }
}

自定状态组件

如果我们不自定义状态组件的话,默认是提供 IndicatorWidget,为各种状态状态创建 UI 。

我们通过创建一个 CustomIndicatorWidget,并且通过 indicatorBuilder 回调以及对最后一个元素的处理,来创建自定义的状态效果。

完整代码如下:

typescript 复制代码
import {
  LoadingMoreList,
  LoadingMoreBase,
  IndicatorWidget,
  IndicatorStatus,
} from '@candies/loading_more_list'

@Entry
@Component
struct LoadingMoreCustomIndicatorDemo {
  @State listData: ListData = new ListData();

  @Builder
  buildList() {
    List() {
      LazyForEach(this.listData, (item, index) => {
        ListItem() {
          // index == this.listData.length
          if (this.listData.isLoadingMoreItem(item))
            CustomIndicatorWidget({
              indicatorStatus: this.listData.getLoadingMoreItemStatus(item),
              sourceList: this.listData,
            })
          else
            Text(`${item}`,).align(Alignment.Center).height(100)
        }.width('100%')
      },
        (item, index) => {
          return `${item}`
        }
      )
    }
    .flexGrow(1)
    .onReachEnd(() => {
      this.listData.loadMore();

    })
  }

  @Builder
  buildIndicator($$: {
    indicatorStatus: IndicatorStatus,
    sourceList: LoadingMoreBase<any>,
  }) {
    CustomIndicatorWidget({ indicatorStatus: $$.indicatorStatus, sourceList: $$.sourceList, })
  }

  build() {
    Navigation() {
      LoadingMoreList({
        sourceList: this.listData,
        builder: this.buildList.bind(this),
        indicatorBuilder: this.buildIndicator.bind(this),
      })
    }
    .title('LoadingMoreCustomIndicatorDemo').titleMode(NavigationTitleMode.Mini)
  }
}


@Component
export struct CustomIndicatorWidget {
  /// Source list based on the [LoadingMoreBase].
  indicatorStatus: IndicatorStatus;
  sourceList: LoadingMoreBase<any>;

  build() {
    if (this.indicatorStatus == IndicatorStatus.none)
      Column()
    else if (this.indicatorStatus == IndicatorStatus.fullScreenBusying)
    Row() {
      Text('正在加载...不要着急',)
      LoadingProgress().width(50).height(50).margin({ left: 10 })
    }.justifyContent(FlexAlign.Center).width('100%').height('100%')
    else if (this.indicatorStatus == IndicatorStatus.fullScreenError)
    Row() {
      Text('好像出现了问题呢?点击重新刷新',)
    }.justifyContent(FlexAlign.Center)
    .width('100%').height('100%').onClick((event) => {
      this.sourceList.errorRefresh();
    })
    else if (this.indicatorStatus == IndicatorStatus.empty)
    Row() {
      Text('这里只有空气呀!',)
    }.justifyContent(FlexAlign.Center).width('100%').height('100%')
    else if (this.indicatorStatus == IndicatorStatus.loadingMoreBusying)
    Row() {
      Text('正在加载...不要使劲拖了',)
      LoadingProgress().width(40).height(40).margin({ left: 10 })
    }.justifyContent(FlexAlign.Center).width('100%').height(50).backgroundColor('#22808080')
    else if (this.indicatorStatus == IndicatorStatus.loadingMoreError)
    Row() {
      Text('网络有点不对劲?点击再次加载试试!',)
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height(50)
    .backgroundColor('#22808080')
    .onClick((event) => {
      this.sourceList.errorRefresh();
    })
    else if (this.indicatorStatus == IndicatorStatus.noMoreLoad)
    Row() {
      Text('已经到了我的下线,不要再拖了',)
    }.justifyContent(FlexAlign.Center).width('100%').backgroundColor('#22808080').height(50)
    else
      Column()
  }
}

学废了

IDataSource

在 ArkUI 中,长列表我们需要用到 LazyForEach 来提供性能。

LazyForEach:数据懒加载-渲染控制-学习ArkTS语言-入门-HarmonyOS应用开发

而 LazyForEach 会使用到 IDataSource,这个接口跟微软的 UWP里面的接口 ISupportIncrementalLoading 是类似的,说白了,是 ui 和 数据源之间的一种契约。

typescript 复制代码
class DataSourceBase implements IDataSource{
  totalCount(): number {
    throw new Error('Method not implemented.');
  }

  getData(index: number) {
    throw new Error('Method not implemented.');
  }

  registerDataChangeListener(listener: DataChangeListener) {
    throw new Error('Method not implemented.');
  }

  unregisterDataChangeListener(listener: DataChangeListener) {
    throw new Error('Method not implemented.');
  }
}

我们将 IDataSource 的方法都实现一下,并且继承于 Array<T> ,这样,一个用于 LazyForEach 的数据源就做好了。

typescript 复制代码
export class DataSourceBase<T> extends Array<T> implements IDataSource {
  // IDataSource start
  private listeners: DataChangeListener[] = [];

  get isEmpty(): boolean {
    return this.length == 0
  }

  totalCount(): number {
    return this.length;
  }

  getData(index: number): T {
    return this[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const position = this.listeners.indexOf(listener);
    if (position >= 0) {
      this.listeners.splice(position, 1);
    }
  }

  // IDataSource end

  notifyDataReload(): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataReloaded();
    })
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataAdd(index);
    })
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataChange(index);
    })
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataDelete(index);
    })
  }

  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataMove(from, to);
    })
  }
}

在上面的实现中,我们添加了很多个通知方法,当我们的列表数据发生改变的时候,调用对应的方法,就可以通知监听了该数据源变化的观察者。至于为啥能通知到 ui ,具体代码在

frameworks/core/components_v2/common/element_proxy.cpp · OpenHarmony/arkui_ace_engine - 码云 - 开源中国 (gitee.com)

而 LoadingMoreBase 基于 DataSourceBase,针对 totalCount 和 getData 方法做了特殊处理来支持加载更多/失败/没有更多的 ui 实现。

typescript 复制代码
export abstract class LoadingMoreBase<T> extends DataSourceBase<T | LoadingMoreItem> {
  public  abstract hasMore: boolean;
  private isLoading: boolean = false;
  indicatorStatus: IndicatorStatus = IndicatorStatus.none;
  lastItemIsLoadingMoreItem: boolean = true;

  totalCount(): number {
    return this.length + (this.lastItemIsLoadingMoreItem ? 1 : 0);
  }

  getData(index: number): T | LoadingMoreItem {
    if (0 <= index && index < this.length)
      return this[index];
    if (!this.hasMore) {
      return new NoMoreLoadItem();
    }
    else if (this.indicatorStatus == IndicatorStatus.loadingMoreError) {
      return new LoadingMoreErrorItem();
    }
    else {
      // auto load more
      if (this.indicatorStatus != IndicatorStatus.loadingMoreBusying) {
        this.loadMore();
      }
      return new LoadingMoreBusyingItem();
    }
  }
}

结语

实际上,在 Flutter 中还支持,多个列表嵌套滚动。

而在 ArkUI 中需要 api 10 的支持,发布的 har 包里面没有看到版本限制的配置,等后续有更好的方案之后,再考虑增加支持。

与 Flutter 中可以将整个渲染布局过程都自己定义相比,ArkUI 中只能靠现有的 api 和 组件 进行组合,确实缺少了很多可操作的可能'

将更多的可能性都放在官方的维护上面,会有一种深深地无力感。 不知道 ArkUI 是否还有变化的可能,希望官方能看到开发者的诉求。

爱 鸿蒙,爱糖果,欢迎加入Harmony Candies,一起生产可爱的鸿蒙小糖果QQ群:981630644

相关推荐
行者-全栈开发1 小时前
【鸿蒙心迹】深色模式适配全链路——从配置到自定义(HarmonyOS 7.x)
华为·harmonyos
陈随易1 小时前
C++26 草案被否,二十八年来头一回
前端·后端·程序员
ZhengEnCi1 小时前
Agent 工具怎么折叠海量输出才「爽」:Trae、Claude Code 的渐进式披露设计原理
前端·agent
IT_陈寒5 小时前
Redis内存暴涨,没想到是AOF重写捣的鬼
前端·人工智能·后端
ikoala5 小时前
让 Codex 自己查账:一个省 token 的小技巧
前端·后端·ai编程
ikoala5 小时前
AI 写页面像说明书,修 Bug 像贴膏药?把这两句话写进 AGENTS.md
前端·javascript·ai编程
好的佩奇6 小时前
Dart 之扩展方法
android·flutter·dart
去伪存真7 小时前
AI Agent Harness 深度测评与技术选型实战
前端·agent
好的佩奇8 小时前
蓝牙之经典蓝牙
android·flutter·蓝牙
FRing0310 小时前
vue2干货总结7(自定义指令、插槽、单页应用程序、路由的使用(VueRouter))
开发语言·前端·vue.js·vue2