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 中的 totalCountgetData 的方法。

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 设置 columnStartcolumnEnd 来实现元素跨列,让它占用整个一行(当然了,这个是通常的情况,你也可以根据你自身的情况设置)

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,针对 totalCountgetData 方法做了特殊处理来支持加载更多/失败/没有更多的 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

相关推荐
一起养小猫19 小时前
Flutter for OpenHarmony 实战:记忆棋游戏完整开发指南
flutter·游戏·harmonyos
小马_xiaoen20 小时前
Proxy 与 Reflect 从入门到实战:ES6 元编程核心特性详解
前端·javascript·ecmascript·es6
hoiii18720 小时前
MATLAB SGM(半全局匹配)算法实现
前端·算法·matlab
飞羽殇情20 小时前
基于React Native鸿蒙跨平台开发构建完整电商预售系统数据模型,完成参与预售、支付尾款、商品信息展示等
react native·react.js·华为·harmonyos
Betelgeuse7621 小时前
【Flutter For OpenHarmony】TechHub技术资讯界面开发
flutter·ui·华为·交互·harmonyos
会编程的土豆21 小时前
新手前端小细节
前端·css·html·项目
广州华水科技21 小时前
单北斗GNSS在桥梁形变监测中的应用与技术进展分析
前端
我讲个笑话你可别哭啊21 小时前
鸿蒙ArkTS快速入门
前端·ts·arkts·鸿蒙·方舟开发框架
CherryLee_121021 小时前
基于poplar-annotation前端插件封装文本标注组件及使用
前端·文本标注
铅笔侠_小龙虾21 小时前
Flutter 安装&配置
flutter