1. 程式人生 > >Flutter 開發從 0 到 1(四)ListView 下拉載入和載入更多

Flutter 開發從 0 到 1(四)ListView 下拉載入和載入更多

![](https://img-blog.csdnimg.cn/img_convert/447c12d917bbe417b46706e2edfc69d0.png) 在《[APP 開發從 0 到 1(三)佈局與 ListView](https://mp.weixin.qq.com/s/Bia6VzhLJmOTq3qYDsX_Ag)》我們完成了 ListView,這篇文章將做 ListView 下拉載入和載入更多。 ## ListView 下拉載入 Flutter 提供了 RefreshIndicator 下拉重新整理元件,可以輕鬆讓我們實現 Material Design 風格的下拉重新整理效果。 ### 引數詳解 ```dart //下拉重新整理元件 const RefreshIndicator ({ Key key, @required this.child, this.displacement: 40.0, //觸發下拉重新整理的距離 @required this.onRefresh, //下拉回調方法,方法需要有async和await關鍵字,沒有await,重新整理圖示立馬消失,沒有async,重新整理圖示不會消失 this.color, //進度指示器前景色,預設為系統主題色 this.backgroundColor, //背景色 this.notificationPredicate: defaultScrollNotificationPredicate, }) ``` ### 效果預覽 ![](https://img-blog.csdnimg.cn/img_convert/5024ba1c573eed6e60abc25a56c2e128.png) ### 完整程式碼 廢話不多說,直接上完整程式碼,你可細品下哦。 ```dart import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class ListViewPage extends StatefulWidget { @override ListViewPageState createState() => new ListViewPageState(); } class ListViewPageState extends State { List list = new List(); //列表要展示的資料 @override void initState() { super.initState(); getData(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('AndBlog'), ), body: RefreshIndicator( onRefresh: _onRefresh, child: ListView.builder( itemCount: list.length, itemBuilder: (BuildContext context, int index) { return ListTile( title: Text(list[index]), ); }, )), floatingActionButton: FloatingActionButton( tooltip: 'Increment', child: Icon(Icons.account_box), onPressed: () { print("FloatingActionButton"); }, elevation: 30, ), // This trailing comma makes auto-formatting nicer for build methods. ); } Future _onRefresh() async { await Future.delayed(Duration(seconds: 3), () { print('refresh'); setState(() { list = List.generate(20, (i) => '哈嘍,我是下拉重新整理的資料 $i'); }); }); } Future getData() async { await Future.delayed(Duration(seconds: 2), () { setState(() { list = List.generate(30, (i) => '哈嘍,我是原始資料 $i'); }); }); } } ``` ## ListView 載入更多 Flutter 沒有直接提供載入更多元件,但我們可以在 ListView 監聽 ScrollController,判斷是否滑到底,然後載入下一頁。 ### 效果預覽 ![](https://img-blog.csdnimg.cn/img_convert/ca51b7b0f2e6cfc691feb4fc5bf6f181.png) ### 完整程式碼 ```dart import 'package:flutter/material.dart'; import 'package:flutter_andblog/andblog/common/color_common.dart'; import 'package:flutter_andblog/andblog/common/http_common.dart'; import 'package:flutter_andblog/andblog/detail/blog_detail_page.dart'; import 'package:http/http.dart' as http; import 'blog.dart'; class BlogListPage extends StatefulWidget { @override BlogListPageState createState() => new BlogListPageState(); } class BlogListPageState extends State { List _blogList = []; String loadMoreText = "正在載入中..."; TextStyle loadMoreTextStyle = new TextStyle(color: const Color(0xFF4483f6), fontSize: 14.0); ScrollController scrollController = new ScrollController(); var hasData = true; var page = 0; @override void initState() { super.initState(); //一進頁面就請求介面 _getBlogListData(); scrollController.addListener(() { if (scrollController.position.pixels == scrollController.position.maxScrollExtent) { //已經滑到底了 if (hasData) { //還有資料,載入下一頁 setState(() { loadMoreText = "正在載入中..."; loadMoreTextStyle = new TextStyle(color: const Color(0xFF4483f6), fontSize: 14.0); }); page++; print("page=" + page.toString()); _getBlogListData(); } else { setState(() { loadMoreText = "沒有更多資料"; loadMoreTextStyle = new TextStyle(color: const Color(0xFF999999), fontSize: 14.0); }); } } }); } @override void dispose() { scrollController.dispose(); super.dispose(); } //網路請求 Future _getBlogListData() async { //一頁載入8條資料,skip為跳過的資料,比如載入第二頁(page=1),skip跳過前8條資料,即顯示第9-16條資料 var skip = page * 8; print("blog_list_url=" + HttpCommon.blog_list_url + skip.toString()); var response = await http.get(HttpCommon.blog_list_url + skip.toString(), headers: HttpCommon.headers()); if (response.statusCode == 200) { // setState 相當於 runOnUiThread setState(() { var data = Blog.decodeData(response.body); if (data.length < 8) { //某頁資料小於8,表明沒有下一頁了 hasData = false; } else { hasData = true; } _blogList.addAll(data); print("_blogList.length0=" + _blogList.length.toString()); }); } } @override Widget build(BuildContext context) { var content; if (_blogList.length == 0) { content = new Center( // 可選引數 child: child: new CircularProgressIndicator(), ); } else { content = _contentList(); } return Scaffold( backgroundColor: ColorCommon.backgroundColor, appBar: AppBar( title: Text('AndBlog'), ), body: content, floatingActionButton: FloatingActionButton( tooltip: 'Increment', child: Icon(Icons.account_box), onPressed: () { print("FloatingActionButton"); }, elevation: 30, ), // This trailing comma makes auto-formatting nicer for build methods. ); } Widget _contentList() { print("_blogList.length=" + _blogList.length.toString()); return new RefreshIndicator( onRefresh: _onRefresh, child: ListView.builder( itemCount: _blogList.length + 1, itemBuilder: (BuildContext context, int index) { if (index == _blogList.length) { return _buildProgressMoreIndicator(); } else { return _blogItem(index); } }, controller: scrollController, )); } Future _onRefresh() async { await Future.delayed(Duration(seconds: 1), () { print('refresh'); setState(() { page = 0; _blogList.clear(); _getBlogListData(); }); }); } Widget _buildProgressMoreIndicator() { return new Padding( padding: const EdgeInsets.all(15.0), child: new Center( child: new Text(loadMoreText, style: loadMoreTextStyle), ), ); } Widget _blogItem(int index) { Blog blog = _blogList[index]; var date = new Padding( padding: const EdgeInsets.only( top: 20.0, left: 10.0, right: 10.0, ), child: new Text( blog.date, textAlign: TextAlign.center, style: TextStyle(color: ColorCommon.dateColor, fontSize: 18), )); var cover = new Padding( padding: const EdgeInsets.only( top: 10.0, left: 10.0, right: 10.0, ), child: new ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(10.0), topRight: Radius.circular(10.0)), child: new Image.network( 'http://pic1.win4000.com/wallpaper/2020-04-21/5e9e676001e20.jpg', ))); var title = new Text( blog.title, style: TextStyle(color: ColorCommon.titleColor, fontSize: 22), ); var summary = new Padding( padding: const EdgeInsets.only( top: 5.0, ), child: new Text(blog.summary, textAlign: TextAlign.left, style: TextStyle(color: ColorCommon.summaryColor, fontSize: 18))); var titleSummary = new Container( padding: const EdgeInsets.all(10.0), alignment: Alignment.topLeft, decoration: new BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( bottomLeft: Radius.circular(10.0), bottomRight: Radius.circular(10.0)), shape: BoxShape.rectangle, ), margin: const EdgeInsets.only(left: 10, right: 10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [title, summary], ), ); var blogItem = new GestureDetector( //點選事件 onTap: () => navigateToMovieDetailPage(blog.objectId, index), child: new Column( children: [ date, cover, titleSummary, ], ), ); return blogItem; } // 跳轉頁面 navigateToMovieDetailPage(String blogId, Object imageTag) { Navigator.of(context) .push(new MaterialPageRoute(builder: (BuildContext context) { return new BlogDetailPage(blogId, imageTag: imageTag); })); } } ``` ![](https://img-blog.csdnimg.cn/img_convert/45fa2187b80f14c09791773c4ec47