Flutter 輕鬆構建載入更多(loading more)
在我軟的UWP裡面有一個介面ISupportIncrementalLoading 只要你的集合繼承這個,並且實現裡面的方法,就能自動實現載入更多的這個動作。說白了就是UWP裡面UI列表控制元件跟集合一個契約。
在Flutter裡面沒有這種類似的東西,但是實際專案裡面會出現大量的列表需要載入更多。

不哭乖站起來繼續擼程式碼,Flutter bug builder 馬上上程式碼。
無圖無真相,先上一個圖。

契約類
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列表
