Flutter 轻松构建加载更多(loading more)

12,300 阅读5分钟

在我软的UWP里面有一个接口ISupportIncrementalLoading 只要你的集合继承这个,并且实现里面的方法,就能自动实现加载更多的这个动作。说白了就是UWP里面UI列表控件跟集合一个契约。

在Flutter里面没有这种类似的东西,但是实际项目里面会出现大量的列表需要加载更多。

QQ群:181398081

不哭乖站起来继续写bug,Flutter bug builder 马上上代码。

无图无真相,先上一个图。 首先我们也来定义个LoadingMoreBase 契约类

class LoadingMoreBase<T> extends ListBase<T>
    with _LoadingMoreBloc<T>, RefreshBase {
  var _array = <T>[];

  @override
  T operator [](int index) {
    // TODO: implement []
    return _array[index];
  }

  @override
  void operator []=(int index, T value) {
    // TODO: implement []=
    _array[index] = value;
  }

  bool get hasMore => true;
  bool isLoading = false;

  IndicatorStatus indicatorStatus = IndicatorStatus.None;

  Future<bool> loadMore() async {
    if (isLoading || !hasMore) return true;
    // TODO: implement loadMore

    var preStatus = indicatorStatus;
    indicatorStatus = this.length == 0
        ? IndicatorStatus.FullScreenBusying
        : IndicatorStatus.LoadingMoreBusying;

    if (preStatus == IndicatorStatus.Error) {
      onStateChanged(this);
    }
    isLoading = true;
    var isSuccess = await loadData();
    isLoading = false;
    if (isSuccess) {
      if (this.length == 0) indicatorStatus = IndicatorStatus.Empty;
    } else {
      indicatorStatus = IndicatorStatus.Error;
    }
    onStateChanged(this);
    return isSuccess;
  }

  Future<bool> loadData() async {
    return true;
  }

  @override
  Future<bool> onRefresh() async {
    // TODO: implement OnRefresh
  }

  @override
  int get length => _array.length;
  set length(int newLength) => _array.length = newLength;

  @override
  void onStateChanged(LoadingMoreBase<T> source) {
    // TODO: implement notice
    super.onStateChanged(source);
  }
}

class _LoadingMoreBloc<T> {
  final _rebuild = new StreamController<LoadingMoreBase<T>>.broadcast();
  Stream<LoadingMoreBase<T>> get rebuild => _rebuild.stream;

  void onStateChanged(LoadingMoreBase<T> source) {
    if (!_rebuild?.isClosed) _rebuild.sink.add(source);
  }

  void dispose() {
    _rebuild?.close();
  }
}

继承于ListBase 方便后面继承

3个重要的方法: 用于加载更多

 Future<bool> loadMore() async

用于刷新(重置列表)

 Future<bool> onRefresh() async 

用于获取数据,loadmore会调用这个方法,一般我们override的这个方法,loadmore里面有一些状态控制,如果你需要overrdie loadmore方法,注意查看下之前里面的状态控制代码

 Future<bool> loadData() async

3个重要的属性: hasMore 判断是否还有更多 isLoading 判断是否正在获取数据 indicatorStatus 判断当前列表的状态

_LoadingMoreBloc 可以通过这个类来通知streambuilder更新UI

下面是如何继承使用这个base 类

class TuChongRepository extends LoadingMoreBase<TuChongItem> {
  int pageindex = 1;

  @override
  // TODO: implement hasMore
  bool _hasMore = true;
  bool get hasMore => _hasMore && length < 20;

  @override
  Future<bool> onRefresh() async {
    // TODO: implement onRefresh
    pageindex = 1;
    return loadMore();
  }

  @override
  Future<bool> loadData() async {
    // TODO: implement getData
    String url = "";
    if (this.length == 0) {
      url = "https://api.tuchong.com/feed-app";
    } else {
      int lastPostId = this[this.length - 1].post_id;
      url =
          "https://api.tuchong.com/feed-app?post_id=${lastPostId}&page=${pageindex}&type=loadmore";
    }
    bool isSuccess = false;
    try {
      //to show loading more clearly, in your app,remove this
      await Future.delayed(Duration(milliseconds: 500, seconds: 1));

      var result = await HttpFactory.getInstance().getHttpClient().get(url);

      var source = TuChongSource.fromJson(json.decode(result.body));
      if (pageindex == 1) {
        this.clear();
      }

      source.feedList.forEach((item) {
        if (item.hasImage && !this.contains(item) && hasMore) {
          this.add(item);
        }
      });

      _hasMore = source.feedList.length != 0;
      pageindex++;
      isSuccess = true;
    } catch (exception) {
      isSuccess = false;
      print(exception);
    }
    return isSuccess;
  }
}

将你请求列表的代码加到getData方法里面,这样数据源的准备就好了。

下面说说UI组件 这一部分分为ListView/GridView 和SliverList/SliverGrid

ListView/GridView

LoadingMoreList 里面的部分代码,StreamBuilder为更新UI,NotificationListener为了监听滑动状态

class LoadingMoreList<T> extends StatelessWidget {
  final ListConfig<T> listConfig;

  LoadingMoreList(this.listConfig,{Key key})
      : super(key: key);
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<LoadingMoreBase>(
      builder: (d, s) {
        return NotificationListener<ScrollNotification>(
          //key: _key,
          onNotification: _handleScrollNotification,
          child: NotificationListener<OverscrollIndicatorNotification>(
              onNotification: _handleGlowNotification,
              child: listConfig.buildContent(context, s.data)),
        );
      },
      stream: listConfig.sourceList?.rebuild,
    );
  }
}

ListConfig 里面提供了ListView/GridView的全部参数,这里我也提供了去掉滚动越界效果(就是列表滚不动的时候出现的水波纹效果)的2个属性showGlowLeading/showGlowTrailing。

final Axis scrollDirection;
  final bool reverse;
  final ScrollController controller;
  final bool primary;
  final ScrollPhysics physics;
  final bool shrinkWrap;
  final EdgeInsetsGeometry padding;
  final double itemExtent;
  final int itemCount;
  final bool addAutomaticKeepAlives;
  final bool addRepaintBoundaries;
  final bool addSemanticIndexes;
  final double cacheExtent;
  final int semanticChildCount;

  /// Whether to show the overscroll glow on the side with negative scroll
  /// offsets.
  final bool showGlowLeading;

  /// Whether to show the overscroll glow on the side with positive scroll
  /// offsets.
  final bool showGlowTrailing;

  ListConfig(
    @required itemBuilder,
    @required sourceList, {
    this.showGlowLeading: true,
    this.showGlowTrailing: true,
    LoadingMoreIndicatorBuilder indicatorBuilder,
    SliverGridDelegate gridDelegate,
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
    this.controller,
    this.primary,
    this.physics,
    this.shrinkWrap = false,
    this.padding,
    this.itemExtent,
    this.itemCount,
    this.addAutomaticKeepAlives = true,
    this.addRepaintBoundaries = true,
    this.addSemanticIndexes = true,
    this.cacheExtent,
    this.semanticChildCount,
  }) : super(itemBuilder, sourceList,
            indicatorBuilder: indicatorBuilder, gridDelegate: gridDelegate);

sourceList 就是之前我们完成的loadingmore 数据源 itemBuilder 是每个item长什么样子

Demo code

class ListViewDemo extends StatefulWidget {
  @override
  _ListViewDemoState createState() => _ListViewDemoState();
}

class _ListViewDemoState extends State<ListViewDemo> {
  TuChongRepository listSourceRepository;
  @override
  void initState() {
    // TODO: implement initState
    listSourceRepository = new TuChongRepository();
    super.initState();
  }

  @override
  void dispose() {
    listSourceRepository?.dispose();
    // TODO: implement dispose
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Column(
        children: <Widget>[
          AppBar(
            title: Text("ListViewDemo"),
          ),
          Expanded(
            child: LoadingMoreList(
                ListConfig<TuChongItem>(
                    ItemBuilder.itemBuilder, listSourceRepository,
//                    showGlowLeading: false,
//                    showGlowTrailing: false,
                    padding: EdgeInsets.all(0.0)),),
          )
        ],
      ),
    );
  }
}

这样子实现了一个加载更多的ListView,如果是GridView的话请给gridDelegate赋值.

SliverList/SliverGrid 支持多个loadmore列表 SliverListConfig 里面包含了SliverList/SliverGrid里面的参数

//config for SliverList and SliverGrid
class SliverListConfig<T> extends LoadingMoreListConfig<T> {
  //whether show no more  .
  bool showNoMore = true;
  //whether show fullscreenLoading for multiple sliver
  bool showFullScreenLoading = true;

  final bool addAutomaticKeepAlives;
  final bool addRepaintBoundaries;
  final bool addSemanticIndexes;
  final SemanticIndexCallback semanticIndexCallback;
  final int semanticIndexOffset;
  final int childCount;

  SliverListConfig(
    @required itemBuilder,
    @required sourceList, {
    LoadingMoreIndicatorBuilder indicatorBuilder,
    SliverGridDelegate gridDelegate,
    this.addAutomaticKeepAlives = true,
    this.addRepaintBoundaries = true,
    this.addSemanticIndexes = true,
    this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
    this.semanticIndexOffset = 0,
    this.childCount,
  }) : super(itemBuilder, sourceList,
            indicatorBuilder: indicatorBuilder, gridDelegate: gridDelegate);

LoadingMoreCustomScrollView使用来创建Sliver组件,它包括了CustomScrollView的属性以及showGlowLeading/showGlowTrailing

//support for LoadingMoreSliverList
class LoadingMoreCustomScrollView extends StatefulWidget {
  final List<Widget> slivers;
  final Axis scrollDirection;
  final bool reverse;
  final ScrollController controller;
  final bool primary;
  final ScrollPhysics physics;
  final bool shrinkWrap;
  final double cacheExtent;
  final int semanticChildCount;

  /// Whether to show the overscroll glow on the side with negative scroll
  /// offsets.
  final bool showGlowLeading;

  /// Whether to show the overscroll glow on the side with positive scroll
  /// offsets.
  final bool showGlowTrailing;

  LoadingMoreCustomScrollView({
    Key key,
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
    this.controller,
    this.primary,
    this.physics,
    this.shrinkWrap = false,
    this.cacheExtent,
    this.slivers = const <Widget>[],
    this.semanticChildCount,
    this.showGlowLeading: true,
    this.showGlowTrailing: true,
  })  : assert(slivers != null),
        super(key: key);

Demo code

简单的一个Sliver

class SliverListDemo extends StatefulWidget {
  @override
  _SliverListDemoState createState() => _SliverListDemoState();
}

class _SliverListDemoState extends State<SliverListDemo> {
  TuChongRepository listSourceRepository;
  @override
  void initState() {
    // TODO: implement initState
    listSourceRepository = new TuChongRepository();
    super.initState();
  }

  @override
  void dispose() {
    listSourceRepository?.dispose();
    // TODO: implement dispose
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {

    return Material(
      child: LoadingMoreCustomScrollView(
        slivers: <Widget>[
          SliverAppBar(
            pinned: true,
            title: Text("SliverListDemo"),
          ),
          LoadingMoreSliverList(
              SliverListConfig<TuChongItem>(
                ItemBuilder.itemBuilder, listSourceRepository,
                //isLastOne: false
              ))
        ],
      ),
    );
  }
}

多个Sliver

class MultipleSliverDemo extends StatefulWidget {
  @override
  _MultipleSliverDemoState createState() => _MultipleSliverDemoState();
}

class _MultipleSliverDemoState extends State<MultipleSliverDemo> {
  TuChongRepository listSourceRepository;
  TuChongRepository listSourceRepository1;

  @override
  void initState() {
    // TODO: implement initState
    listSourceRepository = new TuChongRepository();
    listSourceRepository1 = new TuChongRepository();
    super.initState();
  }

  @override
  void dispose() {
    listSourceRepository?.dispose();
    listSourceRepository1?.dispose();
    // TODO: implement dispose
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: LoadingMoreCustomScrollView(
        slivers: <Widget>[
          SliverAppBar(
            pinned: true,
            title: Text("MultipleSliverDemo"),
          ),
          LoadingMoreSliverList(SliverListConfig<TuChongItem>(
            ItemBuilder.itemBuilder,
            listSourceRepository,
          )),
          SliverToBoxAdapter(
            child: Container(
              alignment: Alignment.center,
              child: Text("Next list"),
              color: Colors.blue,
              height: 100.0,
            ),
          ),
          SliverPersistentHeader(
            delegate: CommonSliverPersistentHeaderDelegate(
                Container(
                  alignment: Alignment.center,
                  child: Text("Pinned Content"),
                  color: Colors.red,
                ),
                100.0),
            pinned: true,
          ),
          LoadingMoreSliverList(SliverListConfig<TuChongItem>(
            ItemBuilder.itemBuilder,
            listSourceRepository1,
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              crossAxisSpacing: 3.0,
              mainAxisSpacing: 3.0,
//                    childAspectRatio: 0.5
            ),
          ))
        ],
      ),
    );
  }
}

那么怎么自定义这些状态的显示内容呢? 我在config里面提供了indicatorBuilder,你可以根据当前list的状态自定义显示效果

demo code

 @override
  Widget build(BuildContext context) {
    return Material(
      child: Column(
        children: <Widget>[
          AppBar(
            title: Text("CustomIndicatorDemo"),
          ),
          Expanded(
            child: LoadingMoreList(
              ListConfig<TuChongItem>(
                  ItemBuilder.itemBuilder, listSourceRepository,
                  indicatorBuilder: _buildIndicator,
                  padding: EdgeInsets.all(0.0),
              ),
            ),
          )
        ],
      ),
    );
  }

  //you can use IndicatorWidget or build yourself widget
  //in this demo, we define all status.
  Widget _buildIndicator(BuildContext context, IndicatorStatus status) {
    Widget widget;
    bool full = (status == IndicatorStatus.FullScreenBusying);
    double height = 35.0;
    switch (status) {
      case IndicatorStatus.None:
        widget = Container(height: 0.0);
        height = 0.0;
        break;
      case IndicatorStatus.LoadingMoreBusying:
      case IndicatorStatus.FullScreenBusying:
        double indicatorSize = full ? 30.0 : 15.0;
        widget = Row(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Container(
                margin: EdgeInsets.only(right: 15.0),
                height: indicatorSize,
                width: indicatorSize,
                child: getIndicator(context)),
            (!full
                ? Text(
                    "正在加载...慌什么慌",
                  )
                : Text("正在加载...慌什么慌",
                    style: TextStyle(
                        fontWeight: FontWeight.bold, fontSize: 28.0))),
          ],
        );
        break;
      case IndicatorStatus.Error:
        widget = Text(
          "加载失败,搞个川川",
        );
        break;
      case IndicatorStatus.NoMoreLoad:
        widget = Text("没有了,不要拖了");
        break;
      case IndicatorStatus.Empty:
        widget = EmptyWidget(
          "这里只有空",
        );
        break;
    }

    widget = Container(
        width: double.infinity,
        height: full ? double.infinity : height,
        child: widget,
        color: Colors.grey[200],
        alignment: Alignment.center);

//    if (isSliver) {
//      if (status == IndicatorStatus.FullScreenBusying) {
//        widget = SliverFillRemaining(
//          child: widget,
//        );
//      } else if (status == IndicatorStatus.Empty) {
//        widget = SliverToBoxAdapter(
//          child: widget,
//        );
//      }
//    }
    if (status == IndicatorStatus.Error) {
      widget = GestureDetector(
        onTap: () {
          listSourceRepository.loadMore();
        },
        child: widget,
      );
    }
    return widget;
  }
}

最后放上 Github loading_more_list,如果你想要其他效果或者有什么不明白的地方,都请告诉我。

自此,妈妈再也不会担心我不会处理Flutter的列表了。Fluter 花样下拉刷新 + Flutter 轻松构建加载更多(loading more) 5分钟上手Flutter列表

QQ群:181398081