Flutter 21: 图解 ListView 下拉刷新与上拉加
2018-11-12 本文已影响63人
阿策神奇
小菜前段时间整理了两种 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 上滑动加载更多
至此,列表的下拉刷新就完成了,接下来处理【上滑加载更多】,这时我们可以借助 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 时间不长,还有很多不清楚和不理解的地方,如果有不对的地方还希望多多指教。以下是小菜公众号,欢迎闲来吐槽〜