Flutter 輕鬆構建加載更多(loading more)

在我軟的UWP裏面有一個接口ISupportIncrementalLoading 只要你的集合繼承這個,而且實現裏面的方法,就能自動實現加載更多的這個動做。說白了就是UWP裏面UI列表控件跟集合一個契約。git

在Flutter裏面沒有這種相似的東西,可是實際項目裏面會出現大量的列表須要加載更多。 github

FlutterCandies QQ羣:181398081 不哭乖站起來繼續擼代碼,Flutter bug builder 立刻上代碼。

pub package

無圖無真相,先上一個圖。 json

首先咱們也來定義個 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 方便後面繼承api

3個重要的方法: 用於加載更多markdown

Future<bool> loadMore() async
複製代碼

用於刷新(重置列表)app

Future<bool> onRefresh() async 
複製代碼

用於獲取數據,loadmore會調用這個方法,通常咱們override的這個方法,loadmore裏面有一些狀態控制,若是你須要overrdie loadmore方法,注意查看下以前裏面的狀態控制代碼less

Future<bool> loadData() async
複製代碼

3個重要的屬性: hasMore 判斷是否還有更多 isLoading 判斷是否正在獲取數據 indicatorStatus 判斷當前列表的狀態async

_LoadingMoreBloc 能夠經過這個類來通知streambuilder更新UIide

下面是如何繼承使用這個base 類svg

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列表

pub package

相關文章
相關標籤/搜索