OpenHarmony开发者论坛

标题: Flutter到 OpenHarmony, 不是有手就行吗? (列表加载更多) [打印本页]

作者: zmtzawqlp    时间: 2023-12-20 09:32
标题: Flutter到 OpenHarmony, 不是有手就行吗? (列表加载更多)
[md]* [Flutter 轻松构建加载更多(loading more) - 掘金 (juejin.cn)](https://juejin.cn/post/6844903725241204750)
* [Flutter到OpenHarmony,不是有手就行吗? (下拉刷新) - 掘金 (juejin.cn)](https://juejin.cn/post/7313161940283998260)

## 前言

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

在 `Flutter` 中你可以通过
[loading_more_list](https://github.com/fluttercandies/loading_more_list) 来快速支持加载更多效果。

在 OpenHarmony 中你则可以使用 [loading_more_list](https://github.com/HarmonyCandies/loading_more_list) 来实现。

| ![LoadingMoreList.gif](https://forums-obs.openharmony.c ... 5wwkv1hw4ov394d.gif "LoadingMoreList.gif") | ![LoadingMoreGrid.gif](https://forums-obs.openharmony.c ... qyseyph74vh22q4.gif "LoadingMoreGrid.gif") |
| --- | --- |
| ![LoadingMoreWaterFlow.gif](https://forums-obs.openharmony.c ... 9oui9yxyyaaxh5a.gif "LoadingMoreWaterFlow.gif")| ![LoadingMoreCustomIndicator.gif](https://forums-obs.openharmony.c ... p2zlxlk5lzyypk5.gif "LoadingMoreCustomIndicator.gif") |

## 安装

`ohpm install @candies/loading_more_list`

## 列表状态

### IndicatorStatus

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

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

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

1. 初始的状态

* `none`

2. 列表第一次加载无数据之前下状态

* `fullScreenBusying`
* `fullScreenError`
* `empty`

3. 列表有数据,在列表展示形式下的状态

* `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应用开发](https://developer.harmonyos.com/ ... 0000001524417213-V3)

而 `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)](https://gitee.com/openharmony/ar ... ment_proxy.cpp#L605)

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

![multiple_sliver.gif](https://forums-obs.openharmony.c ... hdw88y8k7wacyup.gif "multiple_sliver.gif")


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

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

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

爱 OpenHarmony ,爱 `糖果`,欢迎加入[Harmony Candies](https://github.com/HarmonyCandies),一起生产可爱的.OpenHarmony 小糖果[![DM_20231219092555_018.jpg](https://forums-obs.openharmony.c ... obnf8gxxnhh6dkn.jpg "DM_20231219092555_018.jpg")
QQ群:981630644](https://qm.qq.com/q/ajfsyk2RcA)

![DM_20231219092555_019.jpg](https://forums-obs.openharmony.c ... u1twhjwi0i7tnw7.jpg "DM_20231219092555_019.jpg")


[/md]




欢迎光临 OpenHarmony开发者论坛 (https://forums.openharmony.cn/) Powered by Discuz! X3.5