Flutter之封裝一個下拉重新整理上拉載入的listview

Getting Started
1.需求場景
在開發的過程中,經常要用到一個具有下拉重新整理和上拉載入更多功能的listview ,程式碼的實現思路基本是差不多的。所以有必要封裝一個通用的listview,方便使用。
2.需要用到的控制元件
- 下拉重新整理RefreshIndicator
- FutureBuilder:Flutter應用中的非同步模型,基於與Future互動的最新快照來構建自身的widget
- ScrollController,可以監聽listview的滑動狀態
- typedef:在Dart語言中,方法也是物件. 使用typedef,或者function-type alias來為方法型別命名, 然後可以使用命名的方法.當把方法型別賦值給一個變數的時候,typedef保留型別資訊. 具體使用方法: ofollow,noindex">dart.goodev.org/guides/lang…
3.實現思路,佈局方式
目標:外部使用BaseListView的時候,只需要傳入一個頁面請求的操作和item構造的方法就可以使用。
1. 定義typedef
將頁面請求的方法定義為PageRequest,將構造子項的方法定義為ItemBuilder。 比如下面,PageRequest的返回值是列表資料的future,引數值是當前分頁和每頁頁數。在BaseListView中定義一個 PageRequest的變數給外面賦值,然後就可以通過變數呼叫外部的非同步操作。 ItemBuilder主要是提供給外部進行自定義構造子項,引數是資料來源list和當前位置position。 根據需要可以定義更多的typedef,這裡就只定義這兩個。
//型別定義 typedef Future<List<T>> PageRequest<T>(int page, int pageSize); typedef Widget ItemBuilder<T>(List<T> list, int position); 複製程式碼
2. FutureBuilder+RefreshIndicator實現懶載入和下拉重新整理
這個之前已經實現過,可以看: github.com/LXD31256949…
3.利用ScrollController實現載入更多的功能
ListView中有一個ScrollController型別的引數,可以利用controller來監聽listview的滑動狀態,' 當滑動到底部的時候,可以loadmore操作
ListView({ Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap = false, EdgeInsetsGeometry padding, this.itemExtent, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, double cacheExtent, List<Widget> children = const <Widget>[], int semanticChildCount, }) 複製程式碼
4. 一些預設的widget
- 底部的載入菊花:當在進行loadmore操作的時候,顯示底部的載入菊花,所以當在進行loadmore操作的時候, list的長度要加1,然後把菊花這個item放到最後
- 載入資料出錯的狀態頁面,點選可以重試
- 載入資料為空的狀態頁面
4. 程式碼實現
/**這部分程式碼主要是設定滑動監聽,滑動到距離底部100單位的時候,開始進行loadmore操作 如果controller.position.pixels==controller.position.maxScrollExtent再去 進行loadmore操作的話,實際的顯示和操作會有點奇怪,所以這裡設定距離底部100 */ controller = new ScrollController(); controller.addListener(() { if (controller.position.pixels >= controller.position.maxScrollExtent - 100) { if (!isLoading) { isLoading = true; loadmore(); } } }); 複製程式碼
/** * 構造FutureBuilder */ FutureBuilder<List<T>> buildFutureBuilder() { return new FutureBuilder<List<T>>( builder: (context, AsyncSnapshot<List<T>> async) { if (async.connectionState == ConnectionState.active || async.connectionState == ConnectionState.waiting) { isLoading = true; return new Center( child: new CircularProgressIndicator(), ); } if (async.connectionState == ConnectionState.done) { isLoading = false; if (async.hasError) { //有錯誤的時候 return new RetryItem(() { refresh(); }); } else if (!async.hasData) { //返回值為空的時候 return new EmptyItem(() { refresh(); }); } else if (async.hasData) { //如果是重新整理的操作 if (widget.page == 0) { _list.addAll(async.data); } if (widget.total > 0 && widget.total <= _list.length) { widget.enableLoadmore = false; } else { widget.enableLoadmore = true; } debugPrint( "loadData hasData:page:${widget.page},pageSize:${widget.pageSize},list:${_list.length}"); //計算最終的list長度 int length = _list.length + (widget.hasHeader ? 1 : 0); return new RefreshIndicator( child: new ListView.separated( physics: AlwaysScrollableScrollPhysics(), controller: widget.enableLoadmore ? controller : null, itemBuilder: (context, index) { //TODO:頭部的更新,可能要放在外面,放在裡面的話也行,不過要封裝獲取頭部future的邏輯,然後提供一個外部builder給外部進行構造 //目前需要在外面判斷position是否為0去構造頭部 //if (widget.hasHeader && index == 0 && widget.header != null) { //return widget.header; //} //可以載入更多的時候,最後一個item顯示菊花 if (widget.enableLoadmore && index == length) { return new LoadMoreItem(); } return widget.itemBuilder(_list, index); }, itemCount: length + (widget.enableLoadmore ? 1 : 0), separatorBuilder: (BuildContext context, int index) { return new Divider(); }, ), onRefresh: refresh); } } }, future: future, ); } 複製程式碼
下面是跟獲取資料有關的幾個方法:loadmore(),refresh(),loadData()。 loadData()會呼叫之前定義的頁面請求PageRequest方法
Future refresh() async { debugPrint("loadData:refresh,list:${_list.length}"); if (!widget.enableRefresh) { return; } if (isLoading) { return; } _list.clear(); setState(() { isLoading = true; widget.page = 0; future = loadData(widget.page, widget.pageSize); futureBuilder = buildFutureBuilder(); }); } void loadmore() async { debugPrint("loadData:loadmore,list:${_list.length}"); loadData(++widget.page, widget.pageSize).then((List<T> data) { setState(() { isLoading = false; _list.addAll(data); futureBuilder = buildFutureBuilder(); }); }); } Future<List<T>> loadData(int page, int pageSize) async { debugPrint("loadData:page:$page,pageSize:$pageSize,list:${_list.length}"); return await widget.pageRequest(page, pageSize); } 複製程式碼
5.注意的問題和踩坑
- 防止FutureBuilder進行不必要的重繪:這裡我採用的方法,是將getData()賦值給一個future的成員變數, 用它來儲存getData()的結果,以避免不必要的重繪 參考文章: blog.csdn.net/u011272795/…
- FutureBuilder和RefreshIndicator的巢狀問題,到底誰是誰的child,這裡我是把RefreshIndicator作為FutureBuilder 的孩子。如果將RefreshIndicator放在外層,FutureBuilder作為child的話,當RefreshIndicator呼叫onrefreh重新整理資料並用 setState()去更新介面的時候,那FutureBuilder也會再次經歷生命週期,所以導致獲取資料的邏輯會被走兩遍