在《APP 開發從 0 到 1(三)佈局與 ListView》我們完成了 ListView,這篇文章將做 ListView 下拉載入和載入更多。
ListView 下拉載入
Flutter 提供了 RefreshIndicator 下拉重新整理元件,可以輕鬆讓我們實現 Material Design 風格的下拉重新整理效果。
引數詳解
//下拉重新整理元件
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,
})
效果預覽
完整程式碼
廢話不多說,直接上完整程式碼,你可細品下哦。
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class ListViewPage extends StatefulWidget {
@override
ListViewPageState createState() => new ListViewPageState();
}
class ListViewPageState extends State<ListViewPage> {
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<Null> _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,判斷是否滑到底,然後載入下一頁。
效果預覽
完整程式碼
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<BlogListPage> {
List<Blog> _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<Null> _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: <Widget>[title, summary],
),
);
var blogItem = new GestureDetector(
//點選事件
onTap: () => navigateToMovieDetailPage(blog.objectId, index),
child: new Column(
children: <Widget>[
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);
}));
}
}