前言
最近Google开源的跨平台移动开发框架Flutter非常火热,推出了1.0的正式版,趁着热度,我也是抽空粗略地学习了一下。目前网上Flutter相关的资料和开源项目也非常多了,在学习的过程中给了我很多帮助。因此,我想通过一系列文章记录一下自己学习Flutter遇到的一些问题,既是对自身技术的巩固,也方便日后即时查阅。
本文介绍一下列表的下拉刷新和上拉加载,作为移动端最常见的场景之一,在Flutter中是怎样实现的呢?
1.下拉刷新
和Android原生开发中的SwipeRefreshLayout效果相似,Flutter中也提供了一个Material风格的下拉刷新组件RefreshIndicator,用于实现下拉刷新功能。
构造方法如下:
const RefreshIndicator({
Key key,
@required this.child,
this.displacement = 40.0, // 下拉距离
@required this.onRefresh, // 刷新回调方法,返回类型必须为Future
this.color, // 刷新进度条颜色,默认当前主题颜色
this.backgroundColor, // 背景颜色
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
})
使用时,我们需要用RefreshIndicator去包裹ListView,并指定下拉刷新回调方法onRefresh,完整代码如下:
import 'package:flutter/material.dart';
class ListViewPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ListViewPageState();
}
}
class _ListViewPageState extends State<ListViewPage> {
// ListView数据集合
List<String> _list = List.generate(20, (i) => '原始数据${i + 1}');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表的下拉刷新和上拉加载'),
),
body: Container(
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text(_list[index]),
),
itemCount: _list.length,
),
onRefresh: _handleRefresh,
),
),
);
}
// 下拉刷新方法
Future<Null> _handleRefresh() async {
// 模拟数据的延迟加载
await Future.delayed(Duration(seconds: 2), () {
setState(() {
// 在列表开头添加几条数据
List<String> _refreshData = List.generate(5, (i) => '下拉刷新数据${i + 1}');
_list.insertAll(0, _refreshData);
});
});
}
}
这里定义了一个下拉刷新回调方法_handleRefresh(),每次下拉刷新都会调用该方法,在该方法中利用Future.delayed()
模拟网络请求延迟加载数据。需要注意,该方法的返回值必须是Future类型。
// 下拉刷新方法
Future<Null> _handleRefresh() async {
// 模拟数据的延迟加载
await Future.delayed(Duration(seconds: 2), () {
setState(() {
// 在列表开头添加几条数据
List<String> _refreshData = List.generate(5, (i) => '下拉刷新数据${i + 1}');
_list.insertAll(0, _refreshData);
});
});
}
这样,一个简单的列表下拉刷新的效果就实现了。
我们在实际开发中有一种场景是:进入页面自动请求数据并显示加载进度圈,可以通过在根Widget中添加一个显示加载进度的组件(比如ProgressIndicator),加载数据前后动态显示和隐藏该组件来实现。但是既然我们已经使用了RefreshIndicator,可不可以直接利用它的下拉刷新进度圈呢?当然是可以的,这时候就需要利用RefreshIndicatorState了。
RefreshIndicator是一个StatefulWidget,它的State由RefreshIndicatorState管理,我们可以通过RefreshIndicatorState来改变RefreshIndicator的状态,实现利用代码动态显示刷新进度圈。使用时需要使用GlobalKey对RefreshIndicatorState进行管理(我也不知道这样说是否准确。。。),需要显示刷新进度圈时再调用RefreshIndicatorState的
show()
方法即可,完整代码如下:
import 'package:flutter/material.dart';
class ListViewPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ListViewPageState();
}
}
class _ListViewPageState extends State<ListViewPage> {
// ListView数据集合
List<String> _list = new List();
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
@override
void initState() {
super.initState();
// 显示加载进度圈
_showRefreshLoading();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表的下拉刷新和上拉加载'),
),
body: Container(
child: RefreshIndicator(
key: _refreshIndicatorKey,
child: ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text(_list[index]),
),
itemCount: _list.length,
),
onRefresh: _handleRefresh,
),
),
);
}
// 显示加载进度圈
_showRefreshLoading() {
// 这里使用延时操作是由于在执行刷新操作时_refreshIndicatorKey还未与RefreshIndicator关联
Future.delayed(const Duration(seconds: 0), () {
_refreshIndicatorKey.currentState.show();
});
}
// 下拉刷新方法
Future<Null> _handleRefresh() async {
// 模拟数据的延迟加载
await Future.delayed(Duration(seconds: 2), () {
setState(() {
// 在列表开头添加几条数据
List<String> _refreshData = List.generate(5, (i) => '下拉刷新数据${i + 1}');
_list.insertAll(0, _refreshData);
});
});
}
}
在initState()
中执行了_showRefreshLoading()方法,需要注意由于initState()是在build()之前调用的,此时_refreshIndicatorKey还没有和Widget关联,直接调用_refreshIndicatorKey.currentState.show()
会报错。解决方法就是通过Future.delay,设置延迟时间为0,保证执行show()方法时RefreshIndicatorState已经被赋值。调用show()之后会自动调用onRefresh指定的回调方法_handleRefresh(),同时显示加载进度圈,整个刷新效果看着还是比较自然的。
2.上拉加载
相比于下拉刷新,上拉加载的实现要相对麻烦一些。我查阅了一下网上的资料,实现上拉加载可以有两种方式:第一种是通过指定ListView的controller属性,类型是ScrollController,通过ScrollController可以判断ListView是否滑动到了底部,再进行上拉加载的处理;第二种是利用NotificationListener,监听ListVIew的滑动状态,当ListView滑动到底部时,进行上拉加载处理。
方法一 利用ScrollController实现上拉加载更多
ListView有一个controller属性,类型是ScrollController,通过ScrollController可以控制ListView的滑动状态,判断ListVIew是否滑动到了底部。判断的方式如下:
ScrollController _scrollController;
@override
void initState() {
super.initState();
// 初始化ScrollController
_scrollController = ScrollController();
// 监听ListView是否滚动到底部
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent) {
// 滑动到了底部
print('滑动到了底部');
// 这里可以执行上拉加载逻辑
_loadMore();
}
});
}
@override
void dispose() {
super.dispose();
// 这里不要忘了将监听移除
_scrollController.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表的下拉刷新和上拉加载'),
),
body: Container(
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text(_list[index]),
),
itemCount: _list.length,
controller: _scrollController,
),
onRefresh: _handleRefresh,
),
),
);
}
_scrollController.position.pixels
表示ListView当前滑动的距离,_scrollController.position.maxScrollExtent
表示ListView可以滑动的最大距离,因此pixels >= maxScrollExtent
就表示ListView已经滑动到了底部,这时执行加载更多的逻辑即可,这里依然是用Future.delayed()
来模拟数据的延迟加载。当然不要忘记在dispose()方法中调用_scrollController.dispose()
来移除监听,防止内存泄漏。
// 上拉加载
Future<Null> _loadMore() async {
// 模拟数据的延迟加载
await Future.delayed(Duration(seconds: 2), () {
setState(() {
List<String> _loadMoreData = List.generate(5, (i) => '上拉加载数据${i + 1}');
_list.addAll(_loadMoreData);
});
});
}
这里还有一个小问题,就是在加载数据的过程中,继续上滑列表有可能会重复执行加载更多方法,为什么我说是有可能呢?加载更多方法是在滑动监听中通过判断执行的,也就是说如果我们在新数据还未加载出来时继续上滑列表,如果没有产生滑动偏移量,就不会执行addListener中的声明的逻辑;但是如果上滑的过程中产生了偏移量(哈哈,说不定你手滑了呢),就会进入到监听方法中,导致重复执行加载更多方法。解决这个问题的方法很简单,我们只需要声明一个变量isLoading来标识是否正在上拉加载就可以了,在加载数据前后更新isLoading的值。
bool isLoading = false; // 是否正在加载,防止多次请求加载下一页
// 上拉加载
Future<Null> _loadMore() async {
if (!isLoading) {
setState(() {
isLoading = true;
});
// 模拟数据的延迟加载
await Future.delayed(Duration(seconds: 2), () {
setState(() {
isLoading = false;
List<String> _loadMoreData =
List.generate(5, (i) => '上拉加载数据${i + 1}');
_list.addAll(_loadMoreData);
});
});
}
}
这样就实现了列表滑动到底部上拉加载更多数据的效果,目前还有一点需要优化的地方,一般我们在加载更多时会在ListView底部显示一个加载进度圈,提示用户此时正在加载数据。实现方法很简单,就是为ListView添加一个Footer布局。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表的下拉刷新和上拉加载'),
),
body: Container(
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) {
if (index < _list.length) {
return ListTile(
title: Text(_list[index]),
);
} else {
// 最后一项,显示加载更多布局
return _buildLoadMoreItem();
}
},
itemCount: _list.length + 1,
controller: _scrollController,
),
onRefresh: _handleRefresh,
),
),
);
}
// 加载更多布局
Widget _buildLoadMoreItem() {
return Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("加载中..."),
),
);
}
这里为了简单,只用了一个Text提示用户正在加载,实际开发中可以根据需求定制自己的加载布局。添加该布局的方法是将ListView的itemCount指定为_list.length + 1
,即添加一个item,然后itemBuilder中再根据index判断是否为最后一项,返回相应的布局就行了。值得一提的是,其实这里也是简单处理了,加载更多布局始终被添加到列表的最后一项,在实际应用中,我们需要根据具体情况来添加该布局。比如说,当数据集合为空或者数据全部加载完成后,就不需要显示加载更多布局,还有一种情况是数据没有填满整个屏幕时,此时显示加载更多布局就会很奇怪。
到这里,我们基本上就实现了列表的上拉加载更多。
完整的代码如下:
import 'package:flutter/material.dart';
class ListViewPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ListViewPageState();
}
}
class _ListViewPageState extends State<ListViewPage> {
// ListView数据集合
List<String> _list = List.generate(20, (i) => '原始数据${i + 1}');
ScrollController _scrollController;
bool isLoading = false; // 是否正在加载更多
@override
void initState() {
super.initState();
// 初始化ScrollController
_scrollController = ScrollController();
// 监听ListView是否滚动到底部
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent) {
// 滑动到了底部
print('滑动到了底部');
// 这里可以执行上拉加载逻辑
_loadMore();
}
});
}
@override
void dispose() {
super.dispose();
_scrollController.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表的下拉刷新和上拉加载'),
),
body: Container(
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) {
if (index < _list.length) {
return ListTile(
title: Text(_list[index]),
);
} else {
// 最后一项,显示加载更多布局
return _buildLoadMoreItem();
}
},
itemCount: _list.length + 1,
controller: _scrollController,
),
onRefresh: _handleRefresh,
),
),
);
}
// 加载更多布局
Widget _buildLoadMoreItem() {
return Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("加载中..."),
),
);
}
// 下拉刷新方法
Future<Null> _handleRefresh() async {
// 模拟数据的延迟加载
await Future.delayed(Duration(seconds: 2), () {
setState(() {
// 在列表开头添加几条数据
List<String> _refreshData = List.generate(5, (i) => '下拉刷新数据${i + 1}');
_list.insertAll(0, _refreshData);
});
});
}
// 上拉加载
Future<Null> _loadMore() async {
if (!isLoading) {
setState(() {
isLoading = true;
});
// 模拟数据的延迟加载
await Future.delayed(Duration(seconds: 2), () {
setState(() {
isLoading = false;
List<String> _loadMoreData =
List.generate(5, (i) => '上拉加载数据${i + 1}');
_list.addAll(_loadMoreData);
});
});
}
}
}
方法二 利用NotificationListener实现上拉加载更多
NotificationListener是一个Widget,可以监听子Widget发出的Notification。ListView在滑动的过程中会发出ScrollNotification类型的通知,我们可以通过监听该通知得到ListView的滑动状态,判断是否滑动到了底部。NotificationListener有一个onNotification属性,定义了监听的回调方法,通过它来处理加载更多逻辑。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表的下拉刷新和上拉加载'),
),
body: Container(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollNotification) {
if (scrollNotification.metrics.pixels >=
scrollNotification.metrics.maxScrollExtent) {
// 滑动到了底部
// 加载更多
_loadMore();
}
return false;
},
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) {
if (index < _list.length) {
return ListTile(
title: Text(_list[index]),
);
} else {
// 最后一项,显示加载更多布局
return _buildLoadMoreItem();
}
},
itemCount: _list.length + 1,
),
onRefresh: _handleRefresh,
),
)),
);
}
判断ListView是否滑动到底部的逻辑和方法一相同,依然是通过比较ListView当前滑动的距离和可以滑动的最大距离。加载更多的逻辑也和方法一是一样的,这里就不多说了。
完整的代码如下:
import 'package:flutter/material.dart';
class ListViewPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ListViewPageState();
}
}
class _ListViewPageState extends State<ListViewPage> {
// ListView数据集合
List<String> _list = List.generate(20, (i) => '原始数据${i + 1}');
bool isLoading = false; // 是否正在加载更多
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表的下拉刷新和上拉加载'),
),
body: Container(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollNotification) {
if (scrollNotification.metrics.pixels >=
scrollNotification.metrics.maxScrollExtent) {
// 滑动到了底部
// 加载更多
_loadMore();
}
return false;
},
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) {
if (index < _list.length) {
return ListTile(
title: Text(_list[index]),
);
} else {
// 最后一项,显示加载更多布局
return _buildLoadMoreItem();
}
},
itemCount: _list.length + 1,
),
onRefresh: _handleRefresh,
),
)),
);
}
// 加载更多布局
Widget _buildLoadMoreItem() {
return Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("加载中..."),
),
);
}
// 下拉刷新方法
Future<Null> _handleRefresh() async {
// 模拟数据的延迟加载
await Future.delayed(Duration(seconds: 2), () {
setState(() {
// 在列表开头添加几条数据
List<String> _refreshData = List.generate(5, (i) => '下拉刷新数据${i + 1}');
_list.insertAll(0, _refreshData);
});
});
}
// 上拉加载
Future<Null> _loadMore() async {
if (!isLoading) {
setState(() {
isLoading = true;
});
// 模拟数据的延迟加载
await Future.delayed(Duration(seconds: 2), () {
setState(() {
isLoading = false;
List<String> _loadMoreData =
List.generate(5, (i) => '上拉加载数据${i + 1}');
_list.addAll(_loadMoreData);
});
});
}
}
}
总结
1.列表的下拉刷新是通过包裹一层RefreshIndicator,自定义onRefresh回调方法实现的
2.列表上拉加载的基本思路是监听列表滑动状态,当列表滑动到底部时,调用定义好的加载更多逻辑。监听列表滑动状态有两种方式:ScrollController和NotificationListener,这两种方式的实现差不多,选择自己用得习惯的就好了,在使用ScrollController时要记得移除监听。
网友评论