上拉加载和下拉刷新基本上每款 app 必有的一个需求,本文不仅是讲解上拉加载和下拉刷新在页面中的实现,而是把这两个功能放在一个 widget
中,能够在之后的开发中复用。先来看下效果图:android
Flutter 默认给咱们提供了一个下拉刷新的控件,如今先看看代码是如何实现的:bash
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () async{
await Future.delayed(Duration(seconds: 3));
return ;
},
child: ListView.builder(),
);
}
复制代码
只需实现 onRefresh
属性对应的函数,而后在内部模拟一个异步的耗时操做,在三秒后刷新按钮天然就消失了。app
Flutter 并无提供一个上拉加载的控件,因此须要咱们本身去实现。关键的地方有两点:一是要监听到列表是否滑动到最底端了,二是给最底端加一个加载更多的布局。dom
ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = new ScrollController();
_scrollController.addListener((){
// 滑动到底部,去作加载更多的请求
if(_scrollController.position.pixels == _scrollController.position.maxScrollExtent){
_getMoreData();
}
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: widget.onRefresh,
child: Scrollbar(
child: ListView.builder(
controller: _scrollController,
),
}
}
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
复制代码
监听滑动到最底端咱们采用的是 ScrollController
,只须要把添加好监听函数的 scrollController
放到 ListView
的 controller
属性中便可。异步
接下来实现加载更多的布局,主要在 ListView.builder
内操做:async
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: widget.onRefresh,
child: Scrollbar(
child: ListView.builder(
itemCount: widget.itemCount + 1,
itemBuilder: (context, index){
if(index == widget.itemCount){
if(_loadingMoreState == LoadingMoreState.loading) {
return _buildFootView("正在加载");
}else if(_loadingMoreState == LoadingMoreState.complete){
return _buildFootView("加载完成");
}else if(_loadingMoreState == LoadingMoreState.fail){
return _buildFootView('加载失败');
}else if(_loadingMoreState == LoadingMoreState.noData){
return _buildFootView('已经到底啦');
}else{
return Container();
}
}
return ListTile(
leading: Icon(Icons.android),
title: Text("android"),
subtitle: Text(subtitles[index]),
);
},
controller: _scrollController,
),
}
}
}
复制代码
itemCount
数量须要加一,为了让 ListView
最后一行是加载更多的布局。这里根据状态不一样统一写在 _buildFootView
函数内。ide
看下 LoadingMoreState
枚举类的状态:函数
enum LoadingMoreState {
loading, // 正在加载时
complete, // 加载完成
fail, // 加载失败
noData, // 没有更多数据了
hide, // 隐藏布局
}
复制代码
总的来讲就是监听到滑动到底部的时机,此时去请求数据,期间根据调整 LoadingMoreState 状态来改变 ListView 最后一行的 footView 布局。布局
实现了加载更多后,为了之后的复用性,我把下拉刷新和上拉加载的功能都放在了一个 widget
中。ui
typedef RefreshCallBack = Future<void> Function();
typedef LoadMoreCallBack<LoadingMoreState> = Future<LoadingMoreState> Function();
class RefreshLoadMoreIndicator extends StatefulWidget {
RefreshCallBack onRefresh;
LoadMoreCallBack onLoadMore;
int itemCount;
IndexedWidgetBuilder itemBuilder;
RefreshLoadMoreIndicator({
@required this.onRefresh,
@required this.onLoadMore,
@required this.itemCount,
@required this.itemBuilder,
});
@override
State<StatefulWidget> createState() {
return RefreshLoadMoreIndicatorState();
}
}
复制代码
首先明确提供给外部的属性,onRefresh
和 onLoadMore
没什么疑问,真正的请求操做都必须由使用者实现,而且 onLoadMore
须要拿到 LoadingMoreState
返回值,这样才能判断上拉加载时布局的变化。itemCount
是使用者列表数据的数量,这是为了给 ListView
增长最后一行。itemBuilder
直接是使用 ListView
的 item
函数,让使用者去实现 item 布局。这是几个必需要实现的属性。
class RefreshLoadMoreIndicatorState extends State<RefreshLoadMoreIndicator>{
ScrollController _scrollController;
LoadingMoreState _loadingMoreState;
@override
void initState() {
super.initState();
_scrollController = new ScrollController();
_scrollController.addListener((){
if(_scrollController.position.pixels == _scrollController.position.maxScrollExtent){
// 若是处于非 LoadingMoreState.hide 状态,都不能再来第二次,不然会出现重复请求
if(_loadingMoreState == LoadingMoreState.loading ||
_loadingMoreState == LoadingMoreState.complete ||
_loadingMoreState == LoadingMoreState.noData ||
_loadingMoreState == LoadingMoreState.fail){
return ;
}
// 把状态调整为 LoadingMoreState.loading,此时就会显示正在加载的布局
setState(() {
_loadingMoreState = LoadingMoreState.loading;
});
// 拿到使用者返回的加载状态
Future<LoadingMoreState> future = widget.onLoadMore();
future.then((state){
setState(() {
_loadingMoreState = state;
});
// 展现500ms的布局后再隐藏 footView 布局
Timer(Duration(milliseconds: 500), (){
setState(() {
_loadingMoreState = LoadingMoreState.hide;
});
});
});
}
});
}
// footView 根据不一样的状态,决定是否显示转圈以及显示不一样的文案
Widget _buildFootView(String text){
return Container(
child: Center(
child: Padding(
padding: EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_loadingMoreState == LoadingMoreState.loading?Container(
width: 15,
height: 15,
child: CircularProgressIndicator(strokeWidth: 2,),
):Container(),
Padding(
padding: EdgeInsets.only(left: 10),
child: Text(text),
)
],
),
)
),
);
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: widget.onRefresh,
child: Scrollbar(
child: ListView.builder(
itemCount: widget.itemCount + 1,
itemBuilder: (context, index){
if(index == widget.itemCount){
if(_loadingMoreState == LoadingMoreState.loading) {
return _buildFootView("正在加载");
}else if(_loadingMoreState == LoadingMoreState.complete){
return _buildFootView("加载完成");
}else if(_loadingMoreState == LoadingMoreState.fail){
return _buildFootView('加载失败');
}else if(_loadingMoreState == LoadingMoreState.noData){
return _buildFootView('已经到底啦');
}else{
return Container();
}
}
// 依然仍是用使用者给的 item 布局,只是在此以前咱们作了关于 footView 的处理。
return widget.itemBuilder(context, index);
},
controller: _scrollController,
)
),
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
复制代码
关键代码都已注释,若须要其余的属性可根据本身的需求继续加,甚至能够支持 GridView
等布局。封装的关键思路就是只处理上拉加载状态变化后的布局变化,其他属性直接透传都沿用 ListView
的属性。
最后看下使用此控件的示例:
class RefreshDemoState extends State<RefreshDemo>{
static const List<String> models = [
'111111111',
'22222222222',
'333333333',
'44444444444',
'555555555555',
'66666666666666',
'7777777777',
'888888888888',
'99999999999999999',
'10110101010010101',
];
List<String> subtitles = [
...models,
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('refresh')),
body: RefreshLoadMoreIndicator(
onRefresh: () async{
// 模拟刷新请求
await Future.delayed(Duration(seconds: 2));
return ;
},
onLoadMore: () async{
await Future.delayed(Duration(seconds: 2));
// 模拟加载成功、加载失败、没有数据的状况。
int state = Random().nextInt(3);
if(state == 0){
setState(() {
subtitles.addAll(models);
});
return LoadingMoreState.complete;
}else if(state == 1){
return LoadingMoreState.fail;
}else{
return LoadingMoreState.noData;
}
},
itemCount: subtitles.length,
itemBuilder: (context, index){
return ListTile(
leading: Icon(Icons.android),
title: Text("android"),
subtitle: Text(subtitles[index]),
);
},
),
);
}
}
复制代码