一.创建页面
由于我们需要请求网络,并将返回的数据渲染到页面上,所以需要继承StatefulWidget
,本文涉及的接口,取自鸿神的玩android开放API
class ProjectListPage extends StatefulWidget {
State<StatefulWidget> createState() => _ProjectListPageState();
}
class _ProjectListPageState extends State<ProjectListPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("项目列表")
),
body: Container()
);
}
}
二.使用FutureBuilder异步初始化页面数据
通过FutureBuilder,我们可以从互联网上获取数据的过程中显示一个加载框,等获取数据成功时再渲染页面,本文的重点不是讲FutureBuilder怎么使用,就不做过多解释了,直接上代码:
class ProjectListPage extends StatefulWidget {
State<StatefulWidget> createState() => _ProjectListPageState();
}
class _ProjectListPageState extends State<ProjectListPage> {
late Future<PageModel<ProjectModel>> future;
void initState() {
// TODO: implement initState
super.initState();
future = IndexDao.getProjectList(cid: 0, start: 1);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("项目列表")
),
body: FutureBuilder<PageModel<ProjectModel>>(
future: future,
builder: (BuildContext context, AsyncSnapshot<PageModel<ProjectModel>> snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
//请求中,显示加载圈
return const Center(
child: SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(),
),
);
} else {
//请求结束
if (snapshot.hasError) {
// 请求失败,显示错误
return Text("Error: ${snapshot.error}");
} else {
// 请求成功,显示数据
return Text("data: ${snapshot.data}");
}
}
},
)
);
}
}
三.渲染列表
if (snapshot.hasError) {
// 请求失败,显示错误
return Text("Error: ${snapshot.error}");
} else {
// 请求成功,显示数据
List<ProjectModel> datas = snapshot.data?.records ?? [];
return ListView.separated(
padding: EdgeInsets.all(10),
itemBuilder: (BuildContext context, int index) {
return Container(
padding: const EdgeInsets.all(10),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5)),
color: Colors.white,
),
child: IntrinsicHeight(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
width: 120,
height: 1,
child: Image.network(datas[index].envelopePic ?? "", fit: BoxFit.cover),
),
SizedBox(width: 10,),
Expanded(
flex: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"${datas[index]?.title}",
maxLines: 2,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
fontSize: 16
),
),
const SizedBox(
height: 10,
),
Text(
"${datas[index]?.desc}",
maxLines: 2,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
fontSize: 14
)
),
],
)
)
],
),
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(color: Colors.transparent, height: 10,);
},
itemCount: datas.length
);
}
四.实现下拉刷新
直接使用Flutter内置的RefreshIndicator实现下拉刷新
int start = 1;
RefreshIndicator(
onRefresh: () {
return _refreshData();
},
child: ListView.separated(...)
);
Future<void> _refreshData() {
start = 1;
return IndexDao.getProjectList(cid: 0, start: start).then((value) {
setState(() {
datas.clear();
datas.addAll(value.records);
});
});
}
五.上拉加载更多
重点来了,我们应该在何时去加载更多数据呢?那自然是ListView滑动到底部的时候。可以通过ScrollController监听
late ScrollController _controller;
void initState() {
// TODO: implement initState
super.initState();
future = IndexDao.getProjectList(cid: 0, start: 1);
_controller = ScrollController();
_controller.addListener(() {
if(_controller.position.extentAfter == 0) {
//划动到底部了,加载更多数据
print("划动到底部了,加载更多数据");
}
});
}
Widget build(BuildContext context) {
...
return RefreshIndicator(
onRefresh: () {
return _refreshData();
},
child: ListView.separated(
controller: _controller,
...
)
);
}
也可以使用NotificationListener监听
late ScrollController _controller;
void initState() {
// TODO: implement initState
super.initState();
future = IndexDao.getProjectList(cid: 0, start: 1);
_controller = ScrollController();
}
Widget build(BuildContext context) {
return NotificationListener<ScrollEndNotification>(
onNotification: (ScrollEndNotification notification) {
if (_controller.position.extentAfter == 0) {
//滚动到底部
//加载更多数据
}
return false;
},
child: RefreshIndicator(
onRefresh: () {
return _refreshData();
},
child: ListView.separated(
controller: _controller,
...
)
)
)
}
加载更多数据,分别对应四种加载状态,more
:有更多数据,loading
: 加载中,noMore
: 没有更多数据了,error
: 请求网络出错了
enum LoadMoreStatus { more, loading, error, noMore }
我们需要根据这四种加载状态,显示不同的footer,并且,ListView的itemCount需要在原有基础上加一,预留出一个位置,显示Footer
ListView.separated(
...
itemBuilder: (BuildContext context, int index) {
if(index == datas.length) {
if(loadMoreStatus == LoadMoreStatus.more) {
return const SizedBox(
height: 40,
child: Center(
child: Text("上拉显示更多"),
),
);
} else if(loadMoreStatus == LoadMoreStatus.loading) {
return const SizedBox(
height: 40,
child: Center(
child: Text("正在加载..."),
),
);
} else if(loadMoreStatus == LoadMoreStatus.noMore) {
return const SizedBox(
height: 40,
child: Center(
child: Text("没有更多数据了"),
),
);
} else {
return const SizedBox(
height: 40,
child: Center(
child: Text("出错了-_-,上拉重新加载"),
),
);
}
} else {
...
}
},
itemCount: datas.length + 1
)
实现上拉加载更多
void _loadMoreData() {
if(loadMoreStatus == LoadMoreStatus.noMore) {
return;
}
if(loadMoreStatus == LoadMoreStatus.loading) {
return;
}
int page = start;
if(loadMoreStatus != LoadMoreStatus.error) {
page += 1;
}
setState(() {
loadMoreStatus = LoadMoreStatus.loading;
});
IndexDao.getProjectList(cid: 0, start: page).then((value) {
start = page;
setState(() {
if(value.hasNextPage) {
loadMoreStatus = LoadMoreStatus.more;
} else {
loadMoreStatus = LoadMoreStatus.noMore;
}
datas.addAll(value.records);
});
}).onError((error, stackTrace) {
setState(() {
loadMoreStatus = LoadMoreStatus.error;
});
return Future.error(error!, stackTrace);
});
}
_controller.addListener(() {
if(_controller.position.extentAfter == 0) {
//划动到底部了,加载更多数据
_loadMoreData();
}
});
六.Fixed:滑动到最后一页,下拉刷新数据,没有将加载状态重置为more
Future<void> _refreshData() {
start = 1;
setState(() {
loadMoreStatus = LoadMoreStatus.more;
});
return IndexDao.getProjectList(cid: 0, start: start).then((value) {
setState(() {
datas.clear();
datas.addAll(value.records);
hasMore = value?.hasNextPage ?? false;
if(hasMore) {
loadMoreStatus = LoadMoreStatus.more;
} else {
loadMoreStatus = LoadMoreStatus.noMore;
}
});
});
}
七.Fixed:第一页数据不足一屏时,不能触发下拉刷新和加载更多
这种情况属于极端情况,可根据实际情况考虑是否需要修复,可以使用CustomScrollView结合SliverList、SliverFillRemaining修复
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () {
return _refreshData();
},
child: CustomScrollView(
controller: _controller,
slivers: [
SliverPadding(
padding: EdgeInsets.all(10),
sliver: SliverList.separated(
itemCount: datas.length,
itemBuilder: (BuildContext context, int index) {
return Container(
padding: const EdgeInsets.all(10),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5)),
color: Colors.white,
),
child: IntrinsicHeight(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
width: 120,
height: 1,
child: Image.network(datas[index].envelopePic ?? "", fit: BoxFit.cover),
),
SizedBox(width: 10,),
Expanded(
flex: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"${datas[index]?.title}",
maxLines: 2,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
fontSize: 16
),
),
const SizedBox(
height: 10,
),
Text(
"${datas[index]?.desc}",
maxLines: 2,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
fontSize: 14
)
),
],
)
)
],
),
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(color: Colors.transparent, height: 10,);
},
),
),
//填充剩余空间
SliverFillRemaining(
hasScrollBody: false,
fillOverscroll: false,
child: Container(),
),
SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.only(bottom: 10),
height: 40,
child: Center(
child: Text(tips),
),
),
)
],
)
);
}