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]),
          );
        },
      ),
    );
  }

}
复制代码
相关文章
相关标签/搜索