上拉加載和下拉刷新基本上每款 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]),
);
},
),
);
}
}
複製代碼