一.创建页面
由于我们需要请求网络,并将返回的数据渲染到页面上,所以需要继承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 initStatesuper.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 initStatesuper.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 initStatesuper.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),),),)],));
}