一、前言
下拉刷新上拉加载更多的ListView是移动端开发中常用的组件,Flutter中官方提供了ListView
控件来显示列表,并且提供了RefreshIndicator
来支持下拉刷新,但是并没有提供上拉加载更多功能的组件,通过网上查阅发现上拉加载更多的实现思路大多可分为以下两种:
1.通过ListView的ScrollController来判断列表是否滑动到最底部,如果是则加载更多。
ListView.builder(
controller: _scrollController,
...
),
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
print('滑动到了最底部');
_getMore();
}
2.通过数据源来控制界面渲染哪个组件,当数据源循环渲染的 index 跟数据源一样长时就渲染加载更多组件,让其显示出来,同时调用加载更多方法,获取数据,再通过state实现组件ui的更新。
ListView.builder(
itemBuilder: _itemBuilder,
.....
),
Widget _itemBuilder(BuildContext context, int index) {
if (index == items.length) {
....
_getMore();
return _loadingMoreView();
}
}
以上两种思路都能达到上拉加载更多的目的,但是第一种思路需要进行滑动位置对比,在不同平台表现上相对应该没第二种思路好。
网上有很多封装好的ListView,但是很多都是功能不全、不易扩展或者界面显示不友好、存在bug等问题,使用起来诸多不便。最近,在Flutter中文网上学习flutter,看到了封装好的InfiniteListView
组件,支持分页加载、上拉加载更多和下拉刷新,并且能自定义各种加载样式、异常样式、添加头部、分割线等,使用很方便,目前为止也没出现啥异常。
但是InfiniteListView有以下几个问题:
- 不支持代码控制ListView的刷新,只在第一次加载时刷新和手动下拉刷新。
- 没有相关详细使用教程,源码注释少。
为了解决以上问题,本文将对InfiniteListView的使用过程、源码理解、注意问题,以及扩充进行介绍。
二、InfiniteListView的使用
项目地址:https://github.com/flutterchina/flukit
InfiniteListView源码地址:https://github.com/flutterchina/flukit/blob/master/package_src/lib/src/infinite_listview.dart
1、引入flukit插件
在pubspec.yaml中,添加插件依赖:
dependencies:
flukit: ^1.0.2
在Terminal中执行flutter packages get
命令下载插件。
2、使用Demo:
return Scaffold(
body: InfiniteListView<RepoBean>(
onRetrieveData: (int page, List<RepoBean> items, bool refresh) async {
//网络请求数据
var data = await NetApi(context).getRepos(
queryParameters: {
'page': page,
'page_size': 30,
},
);
//把请求到的新数据添加到items中
items.addAll(data);
// 如果接口返回的数量等于'page_size',则认为还有数据,反之则认为最后一页
return data.length == 30;
},
itemBuilder: (List list, int index, BuildContext ctx) {
// 自己去根据数据构建列表项
return RepoItem(list[index]);
},
),
);
最主要的是onRetrieveData
用来获取列表需要的数据,然后添加到items中回调。itemBuilder
是根据items数据来渲染每一个列表项。
如果只是使用InfiniteListView
上拉加载更多、下拉刷新、分页等基础功能,那么根据demo简单调整即可,如果想实现更丰富的功能比如自定义Loading样式、扩充其他功能,需要对源码进行阅读。
三、InfiniteListView源码浅析
1、参数
在平时使用InfiniteListView,基本都是在使用以下这些参数,所以理解这些参数是非常必要的。
InfiniteListView({
Key key,
@required this.onRetrieveData, //获取列表需要的数据
@required this.itemBuilder, //列表每一个item的样式构建
this.initFailBuilder, //加载失败样式显示
this.initLoadingBuilder, //第一次加载列表的loading样式
this.scrollController, //ListView的ScrollController
this.pageSize = 30, //pageSize,默认每页加载30条数据
this.loadMoreErrorViewBuilder, //加载更多失败的样式
this.loadingBuilder, //加载更多的loading样式
this.headerBuilder, //ListView的header样式
this.noMoreViewBuilder, //没有更多的样式
this.sliver = false, //是否为Sliver组件
this.separatorBuilder, //分割线样式
this.emptyBuilder, //数据为空或者没有数据的样式
this.initState, //初始化
this.physics, //ScrollPhysics
}) : super(key: key);
每个参数的参数类型需要查看源码来使用,举两个例子:
1.1 separatorBuilder(分割线)的使用:
参数类型 :
typedef IndexedItemBuilder<T> = Widget Function(
List<T> list, int index, BuildContext ctx);
final IndexedItemBuilder<T> separatorBuilder;
使用demo:
InfiniteListView<RepoBean>(
onRetrieveData: (int page, List<RepoBean> items, bool refresh) async {
var data = await NetApi(context).searchRepos(
keyWords: curSearchWords,
queryParameters: {
'page': page,
'page_size': 30,
},
);
items.addAll(data);
return data.length == 30;
},
itemBuilder: (List list, int index, BuildContext ctx) {
return RepoItem(list[index]);
},
separatorBuilder: (List list,int index,BuildContext ctx){
//当index>3时即从第五个item开始 添加分割线
if(index>3){
return Container(
width: double.infinity,
height: 10,
color: Colors.greenAccent,
);
}else{
return Container(
width: 0,
height: 0,
);
}
},
)
效果:
从第五个item起,开始出现绿色的分割线
1.2 emptyBuilder(空数据样式):
参数类型:
final Widget Function(VoidCallback refresh, BuildContext context)
emptyBuilder;
使用demo:
InfiniteListView<RepoBean>(
emptyBuilder: (VoidCallback refresh, BuildContext context) {
return listNoDataView(refresh, context);
},
onRetrieveData: (int page, List<RepoBean> items, bool refresh) async {
.......
},
itemBuilder: (List list, int index, BuildContext ctx) {
........
},
)
Widget listNoDataView(refresh, context) {
return Material(
child: InkWell(
splashColor: Theme.of(context).secondaryHeaderColor,
onTap: refresh,
child: Center(
child: Padding(
padding: EdgeInsets.only(top: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.event_busy,
color: Theme.of(context).primaryColor,
size: 150,
),
Padding(
padding: EdgeInsets.only(top: 0),
child: Text(
"没有数据",
style: TextStyle(
color: Theme.of(context).primaryColor, fontSize: 22),
))
],
),
),
),
),
);
}
效果:
显示的是自定义的没有数据样式,当点击空白样式时会执行refresh方法,回调InfiniteListView
源码中的refresh(bool pullDown)
方法,进行列表的刷新。
其他参数就不一一详细介绍,主要根据需求查看参数的类型进行使用。
2、源码浅析
InfiniteListView的源码很简单,下面主要介绍几个比较重要的方法,在实际使用中不会用到,但是如果需要扩充其功能,或者做一些更改,就需要理解主要方法的意思。其他部分可以自己去看看。
//刷新列表,参数代表是否是下拉刷新
//一般在下拉刷新、点击加载失败或空样式、初始化列表时调用
Future<void> refresh(bool pullDown) async {
if (state.loading) return;
state.loading = true;
state.noMore = false;
refreshing = true;
error = null;
update();
try {
var _items = <T>[];
//获取第一页数据
var hasMore = await widget.onRetrieveData(1, _items, pullDown);
if (_items.isEmpty ||
_items.length % widget.pageSize != 0 && hasMore != true) {
state.noMore = true;
}
state.items = _items;
state.currentPage = 1;
} catch (e) {
....
} finally { //结束loading
state.loading = false;
refreshing = false;
}
......
}
//构建ListView
Widget _build() {
//用于下拉刷新
return RefreshIndicator(
onRefresh: () => refresh(true),
child: ListView.builder(
physics: widget.physics,
controller: widget.scrollController,
itemCount: state.items.length + (_hasHeader ? 2 : 1),
itemBuilder: _itemBuilder,
),
);
}
//构建每一个item项
Widget _itemBuilder(BuildContext context, int index) {
......
if (index == state.items.length) {
........
//加载更多
loadMore();
return _loadingMoreView();
} else {
//构建item内容和分割线
var w = widget.itemBuilder(state.items, index, context);
if (widget.separatorBuilder != null) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
w,
widget.separatorBuilder(state.items, index, context),
],
);
}
return w;
}
}
四、扩充
有时候InfiniteListView
提供的参数不满足我们的需求,这时可以对InfiniteListView
进行扩充。
例如,添加一个参数来用代码控制InfiniteListView的刷新,可以做如下扩充:
- 1. 将InfiniteListView代码复制出来进行更改(因为源代码不属于本项目 ),换个名字例如MyInfiniteListView。
- 2. 添加一个参数refreshKey用来控制刷新。
MyInfiniteListView({
Key key,
@required this.onRetrieveData,
@required this.itemBuilder,
........
this.refreshKey
});
final Key refreshKey;
- 3. 给RefreshIndicator添加key来控制ListView的刷新。
Widget _build() {
return RefreshIndicator(
key: widget.refreshKey,
onRefresh: () => refresh(true),
child: ListView.builder(
physics: widget.physics,
controller: widget.scrollController,
itemCount: state.items.length + (_hasHeader ? 2 : 1),
itemBuilder: _itemBuilder,
),
);
}
至此扩充已经完成,下面是如何去使用:
//这个key用来在不是手动下拉,而是点击某个button或其它操作时,代码直接触发下拉刷新
final GlobalKey<RefreshIndicatorState> refreshIndicatorKey =
new GlobalKey<RefreshIndicatorState>();
......
MyInfiniteListView<UserBean>(
//传入refreshKey参数
refreshKey: refreshIndicatorKey,
onRetrieveData: (int page, List<UserBean> items, bool refresh) async {
......
},
itemBuilder: (List list, int index, BuildContext ctx) {
......
},
)
......
//在需要代码控制刷新的地方,使ListView刷新
refreshIndicatorKey.currentState.show();
总结:InfiniteListView
是一个比较好用的下拉刷新上拉加载更多的列表控件,本身支持很多自定义的样式,能很好的应用于各个列表页面。如果有其他需要的功能,可以根据需求自己去灵活的扩充。
五、 项目实战
项目地址:用flutter实现的一款界面精美的Github App
介绍:用Flutter实现的一款界面精美、功能较全、体验良好的Github客户端。支持多语言、换肤等功能。代码简单易懂且有充分的注释,很适用于学习Flutter。