[经验分享] Flutter到 OpenHarmony, 不是有手就行吗? (列表加载更多) 原创

zmtzawqlp 显示全部楼层 发表于 2023-12-20 09:32:19

前言

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

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

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

LoadingMoreList.gif LoadingMoreGrid.gif
LoadingMoreWaterFlow.gif LoadingMoreCustomIndicator.gif

安装

ohpm install @candies/loading_more_list

列表状态

IndicatorStatus

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

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

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

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

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

关键代码是 LoadingMoreBase 中的 totalCountgetData 的方法。

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
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;
    }
  }
}

使用

导入引用

import {
  LoadingMoreList,
  LoadingMoreBase,
  IndicatorWidget,
  IndicatorStatus,
} from '@candies/loading_more_list'

LoadingMoreList

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

/// 列表,可以是 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;

例子

准备一个简单的数据源。

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

if (this.listData.isLoadingMoreItem(item))
            IndicatorWidget({
              indicatorStatus: this.listData.getLoadingMoreItemStatus(item),
              sourceList: this.listData,
            })

完整代码如下:

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

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 回调来创建加载更多的组件。

@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,
      })
  }

完整代码如下:

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 回调以及对最后一个元素的处理,来创建自定义的状态效果。

完整代码如下:

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 和 数据源之间的一种契约。

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 的数据源就做好了。

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 实现。

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 中还支持,多个列表嵌套滚动。

multiple_sliver.gif

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

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

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

爱 OpenHarmony ,爱 糖果,欢迎加入Harmony Candies,一起生产可爱的.OpenHarmony 小糖果DM_20231219092555_018.jpg QQ群:981630644

DM_20231219092555_019.jpg

无用

©著作权归作者所有,转载或内容合作请联系作者

您尚未登录,无法参与评论,登录后可以:
参与开源共建问题交流
认同或收藏高质量问答
获取积分成为开源共建先驱

Copyright   ©2023  OpenHarmony开发者论坛  京ICP备2020036654号-3 |技术支持 Discuz!

返回顶部