自定义 loadmore

写在前面

这类的库在 pub 上有很多

我为什么要自定义呢

首先是项目需要,并且这种库普适性高,抽取出来今后复用也方便点

另外记录一下编码思路,方便后续查看

pub 地址 pub 国内镜像 github

使用说明

导入说明看这里中文镜像

image.png 看看构造方法 一共 5 个属性 child 是 ListView

onLoadMore 是加载更多时的回调,由外部实现

isFinish 加载完成

delegate 是一个抽象类 image.png 有默认实现, 其中有 3 个方法,一个是根据状态给一个 widget 高度 一个是延迟加载的毫秒时间 一个是构建显示在内部的 Widget,这样就完全实现了外部可根据状态自定义 Widget

LoadMoreTextBuilder 是一个根据状态构建文字的方案,默认实现了 中文/英文文字,如果只想修改文字,使用默认样式的话,可以直接用这个即可

思路

首先考虑怎么自定义 一般来讲有 2 种方式,一个是到底部继续上拉加载,另一种是滚动到底部自动加载,我这里采取的是到底部自动加载方案

不使用上拉加载的原因是:滚动到底继续上拉不符合正常人习惯,如果是惯性滚动到底,谁知道你后面还有没有东西的

思考如何自定义

首先怎么样可以知道滚动到底了呢,最简单的方式,listview 的最后一行 build 的时候一定滚动到底了

所以我们可以使用如下的方式定义


class _ListViewDemoPageState extends State<ListViewDemoPage> {
  var count = 10;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: count + 1,
      itemBuilder: _buildItem,
    );
  }

  Widget _buildItem(BuildContext context, int index) {
    if (index == count) {
      return Text('到底了');
    }
    return Text(index.toString());
  }
}

这样的话 只要到最后了,你自然知道应该加载了

可是这个方法不优雅啊,我们应该封装为 Widget 控件,方便复用,接下来就要开始分析怎么自定义了

首先观察 ListView 的构造方法 有如下几个构造方法

ListView:同名构造方法 ListView.builder ListView.separated ListView.custom

这里 custom,需要自定义 childrenDelegate 类型为 SliverChildDelegate,我先暂时不考虑

看其他的三个,发现内部都实现了这个 Delegate image.png image.png image.png

那么我们接收一个 ListView,然后判断其中的 delegate 类型,然后分别进行处理不就可以了吗

image.png

image.png

这里我们分别处理,一个是增加一个 count,另一个因为直接获取到了 List ,把 loadmore 的 widget 添加进去就行了

这里就完成了第一部,在调用方不发生变化的情况下,我们获取 ListView,并且在底部添加了一个 loadmore 的 widget


构造 widget

这里要思考了,我们一共需要几种状态 一个是默认时的状态,这个基本很难见到,也就是空闲状态 一个是加载中 一个是加载失败 一个是没更多的数据

到底这种状态,因为控件理论上不应该控制数据,所以必须由外部传入 其他的状态包含在内部

enum LoadMoreStatus {
  /// 空闲中,表示当前等待加载
  ///
  /// wait for loading
  idle,

  /// 刷新中,不应该继续加载,等待future返回
  ///
  /// the view is loading
  loading,

  /// 刷新失败,刷新失败,这时需要点击才能刷新
  ///
  /// loading fail, need tap view to loading
  fail,

  /// 没有更多,没有更多数据了,这个状态不触发任何条件
  ///
  /// not have more data
  nomore,
}

这里写一个 enum 用于标示状态


然后就是构建 widget 了 这里我们根据一些方法得到状态,并且构建一个StatefulWidget返回 这里之所以这么做,是因为如果返回是无状态的 Widget,则二次滚动到底时,不会再次自动触发 build 方法

 Widget _buildLoadMoreView() {
    if (widget.isFinish == true) {
      this.status = LoadMoreStatus.nomore;
    } else {
      if (this.status == LoadMoreStatus.nomore) {
        this.status = LoadMoreStatus.idle;
      }
    }
    return NotificationListener<_RetryNotify>(
      child: NotificationListener<_BuildNotify>(
        child: DefaultLoadMoreView(
          status: status,
          delegate: loadMoreDelegate,
          textBuilder: widget.textBuilder,
        ),
        onNotification: _onLoadMoreBuild,
      ),
      onNotification: _onRetry,
    );
  }

这里之所以有两个 NotifycationListener 是用于捕捉内部返回的两种动作,一个是自动刷新的动作 一个是点击重试的动作

触发动作后,我这里就会修改 this.status setState,这样就会触发 loadmorev View 的状态变化

这里应用到了一个 Flutter 的机制,Notification 机制,Widget 内部通知,外部父节点 NotificationListener 进行捕获,然后根据捕获时的返回值决定是否继续传递.


DefaultLoadMoreView是一个 StatefulWidget,在内部定义了点击事件和加载事件

因为一旦显示就自动构建,所以内部会根据状态,只有当 idle 状态时会传递出一个加载的通知,然后上层的 Widget 获取 Notify 后,修改状态为 loading,并调取加载数据的借口

同理,当加载状态为错误时,内部就不会抛出 notify 了,除非在 LoadMore 的调用方修改 finish 状态,否则理论上 widget 就不会再变化,不论滚动与否,这时需要用户主动点击加载重试,这里在点击事件中抛出重试的通知,外部加载

image.png

这里之所以有一个延时,目的是为了防止 setState 太频繁造成界面不变化的问题,理论上这里大于 16ms 就可以了


然后我们回到捕获处 image.png

后记

总体代码只有 300 行,可以在 pub 里直接使用,目前最新版本为 0.1.1 pub 地址 pub 国内镜像 github

欢迎 issue 欢迎 star