在《Flutter 开发从 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 {
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,判断是否滑到底,然后加载下一页。
效果预览
完整代码
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);
}));
}
}