Flutter 上拉加載和下拉刷新

上拉加載和下拉刷新基本上每款 app 必有的一個需求,本文不僅是講解上拉加載和下拉刷新在頁面中的實現,而是把這兩個功能放在一個 widget 中,能夠在之後的開發中複用。先來看下效果圖:android

refresh_loadmore

1、RefreshIndicator

Flutter 默認給咱們提供了一個下拉刷新的控件,如今先看看代碼是如何實現的:bash

@override
Widget build(BuildContext context) {
	return RefreshIndicator(
    onRefresh: () async{
      await Future.delayed(Duration(seconds: 3));
      return ;
    },
		child: ListView.builder(),
	);
}  
複製代碼

只需實現 onRefresh 屬性對應的函數,而後在內部模擬一個異步的耗時操做,在三秒後刷新按鈕天然就消失了。app

2、上拉加載

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 放到 ListViewcontroller 屬性中便可。異步

接下來實現加載更多的佈局,主要在 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 佈局。佈局

3、RefreshLoadMoreIndicator

實現了加載更多後,爲了之後的複用性,我把下拉刷新和上拉加載的功能都放在了一個 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();
  }

}
複製代碼

首先明確提供給外部的屬性,onRefreshonLoadMore 沒什麼疑問,真正的請求操做都必須由使用者實現,而且 onLoadMore 須要拿到 LoadingMoreState 返回值,這樣才能判斷上拉加載時佈局的變化。itemCount 是使用者列表數據的數量,這是爲了給 ListView 增長最後一行。itemBuilder 直接是使用 ListViewitem 函數,讓使用者去實現 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]),
          );
        },
      ),
    );
  }

}
複製代碼
相關文章
相關標籤/搜索