Flutter 21: 圖解 ListView 下拉重新整理與上拉載入 (一)【RefreshIndicator】
小菜前段時間整理了兩種 ListView 的非同步載入資料時,下拉重新整理與上滑載入更多的方式,每種方式都有自己的優勢,網上也有很多大神講解過 ListView 資料流的種種處理方式,小菜根據實際遇到的情況整理一下嘗試的第三種方案。

RefreshIndicator 下拉重新整理
Flutter提供了自帶重新整理效果的 RefreshIndicator ,這也是網上大神們用的最多的 Widget 之一,使用方式也很簡單, RefreshIndicator 中提供了一個重新整理的回撥入口 onRefresh ,僅需在該回調介面中處理資料請求即可,如下:
// 重新整理時資料請求 Future<Null> _loadRefresh() async { await Future.delayed(Duration(seconds: 2), () { setState(() { dataItems.clear(); lastFileID = '0'; rowNumber = 0; _getNewsData(lastFileID, rowNumber); return null; }); }); } // 請求介面整合資料 _getNewsData(var lastID, var rowNum) async { await http .get( 'https://XXX.../getArticles?...&lastFileID=${lastID}&rowNumber=${rowNum}') .then((response) { if (response.statusCode == 200) { var jsonRes = json.decode(response.body); newsListBean = NewsListBean(jsonRes); if (lastID == '0' && rowNum == 0 && dataItems != null) { dataItems.clear(); } setState(() { if (newsListBean != null && newsListBean.list != null && newsListBean.list.length > 0) { for (int i = 0; i < newsListBean.list.length; i++) { dataItems.add(newsListBean.list[i]); } lastFileID = newsListBean.list[newsListBean.list.length - 1].fileID.toString(); rowNumber += newsListBean.list.length; } else {} }); } }); } // 繫結列表資料 @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("第三種載入方式"), ), body: new RefreshIndicator( child: ListView.builder( itemCount: items.length, itemBuilder: buildListData(context, dataItems[index]) ), onRefresh: _loadRefresh,// 重新整理回撥 )); }

ScrollController 上滑動載入更多
至此,列表的下拉重新整理就完成了,接下來處理【上滑載入更多】,這時我們可以藉助 ofollow,noindex"> ScrollController ,用來監聽列表是否滑動到底部,主要分兩步:
- 初始化時新增監聽事件,判斷是否滑動到最底部;
- ListView 中新增監聽方法。
ScrollController _scrollController = new ScrollController(); @override void initState() { super.initState(); _scrollController.addListener(() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { _getMoreData();// 當滑到最底部時呼叫 } }); _getMoreData();// 資料初始化 } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("第三種載入方式"), ), body: ListView.builder( itemCount: items.length, itemBuilder: buildListData(context, dataItems[index]), controller: _scrollController, )); }

至此,列表的下拉重新整理與上滑載入更多就基本完成了;接下來需要將兩種合併使用,也很簡單,如下:
body: new Padding( padding: EdgeInsets.all(2.0), child: RefreshIndicator( onRefresh: _loadRefresh, child: ListView.builder( itemCount: dataItems.length, physics: const AlwaysScrollableScrollPhysics(), itemBuilder: (context, index) { return buildListData(context, dataItems[index]); }, controller: _scrollController, )));
Tips:注意處理好資料介面請求內容。
小優化
優化一:【上滑載入更多】新增動畫效果
- 新增一個載入更多的佈局 Widget ;
- 在 itemCount 中將 item 個數 +1 ;
- 新增監聽判斷,當滑到最後一個 item 時展示載入更多到佈局 Widget 。
body: new Padding( padding: EdgeInsets.all(2.0), child: RefreshIndicator( onRefresh: _loadRefresh, child: ListView.builder( itemCount: dataItems.length + 1, physics: const AlwaysScrollableScrollPhysics(), itemBuilder: (context, index) { if (index == dataItems.length) { return _buildProgressIndicator(); } else { return buildListData(context, dataItems[index]); } }, controller: _scrollController, ))); // 載入更多 Widget Widget _buildProgressIndicator() { return new Padding( padding: EdgeInsets.fromLTRB(0.0, 14.0, 0.0, 14.0), child: new Opacity( opacity: isShowLoading ? 1.0 : 0.0, child: new Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ new SpinKitChasingDots(color: Colors.blueAccent, size: 26.0), new Padding( padding: EdgeInsets.fromLTRB(10.0, 0.0, 0.0, 0.0), child: new Text('正在載入中...')) ], ))); }
優化二:第一次初始化載入資料時新增loading動畫
RefreshIndicator中自帶重新整理的動畫,所以小菜只是在第一次載入資料時新增一個 loading 動畫,小菜只是填了一個小小的狀態判斷,如下包括異常情況下的失敗頁。
Widget childWidget() { Widget childWidget; if (newsListBean != null && (newsListBean.success != null && !newsListBean.success)) { isFirstLoading = false; childWidget = new Stack(children: <Widget>[ new Padding( padding: new EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 100.0), child: new Center( child: Image.asset( 'images/icon_wrong.jpg', width: 120.0, height: 120.0, ))), new Padding( padding: new EdgeInsets.fromLTRB(0.0, 100.0, 0.0, 0.0), child: new Center( child: new Text( '抱歉!暫無內容哦~', style: new TextStyle(fontSize: 18.0, color: Colors.blue), ))) ]); } else if (dataItems != null && dataItems.length != 0) { isFirstLoading = false; childWidget = new Padding( padding: EdgeInsets.all(2.0), child: RefreshIndicator( onRefresh: _loadRefresh, child: ListView.builder( itemCount: dataItems.length + 1, physics: const AlwaysScrollableScrollPhysics(), itemBuilder: (context, index) { if (index == dataItems.length) { return _buildProgressIndicator(); } else { return buildListData(context, dataItems[index]); } }, controller: _scrollController, ))); } else { if (isFirstLoading) {// 只有在第一次載入資料時才會展示自定義 loading childWidget = new Center( child: new Card( child: new Stack(children: <Widget>[ new Padding( padding: new EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 35.0), child: new Center( child: SpinKitFadingCircle( color: Colors.blueAccent, size: 30.0, ))), new Padding( padding: new EdgeInsets.fromLTRB(0.0, 35.0, 0.0, 0.0), child: new Center( child: new Text('正在載入中,莫著急哦~'), )) ])), ); } else {} } return childWidget; }

優化三:藉助Future.delayed()進行延遲載入,使資料請求銜接性更好。
_getMoreData() async { if (!isShowLoading) { setState(() { isShowLoading = true; }); await Future.delayed(Duration(seconds: 2), () { setState(() { _getNewsData(lastFileID, rowNumber); isShowLoading = false; return null; }); }); } }
小菜剛接觸 Flutter 時間不長,還有很多不清楚和不理解的地方,如果有不對的地方還希望多多指教。以下是小菜公眾號,歡迎閒來吐槽〜

公眾號.jpg