美文网首页FlutterFlutter圈子Flutter中文社区
Flutter学习小计:ListView的下拉刷新和上拉加载

Flutter学习小计:ListView的下拉刷新和上拉加载

作者: 飘逸解构 | 来源:发表于2019-03-01 11:04 被阅读6次

    前言
    最近Google开源的跨平台移动开发框架Flutter非常火热,推出了1.0的正式版,趁着热度,我也是抽空粗略地学习了一下。目前网上Flutter相关的资料和开源项目也非常多了,在学习的过程中给了我很多帮助。因此,我想通过一系列文章记录一下自己学习Flutter遇到的一些问题,既是对自身技术的巩固,也方便日后即时查阅。

    本文介绍一下列表的下拉刷新和上拉加载,作为移动端最常见的场景之一,在Flutter中是怎样实现的呢?

    1.下拉刷新

    和Android原生开发中的SwipeRefreshLayout效果相似,Flutter中也提供了一个Material风格的下拉刷新组件RefreshIndicator,用于实现下拉刷新功能。
    构造方法如下:

    const RefreshIndicator({
        Key key,
        @required this.child,
        this.displacement = 40.0, // 下拉距离
        @required this.onRefresh, // 刷新回调方法,返回类型必须为Future
        this.color, // 刷新进度条颜色,默认当前主题颜色
        this.backgroundColor, // 背景颜色
        this.notificationPredicate = defaultScrollNotificationPredicate,
        this.semanticsLabel,
        this.semanticsValue,
      }) 
    

    使用时,我们需要用RefreshIndicator去包裹ListView,并指定下拉刷新回调方法onRefresh,完整代码如下:

    import 'package:flutter/material.dart';
    
    class ListViewPage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return _ListViewPageState();
      }
    }
    
    class _ListViewPageState extends State<ListViewPage> {
      // ListView数据集合
      List<String> _list = List.generate(20, (i) => '原始数据${i + 1}');
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('列表的下拉刷新和上拉加载'),
          ),
          body: Container(
            child: RefreshIndicator(
              child: ListView.builder(
                itemBuilder: (context, index) => ListTile(
                      title: Text(_list[index]),
                    ),
                itemCount: _list.length,
              ),
              onRefresh: _handleRefresh,
            ),
          ),
        );
      }
    
      // 下拉刷新方法
      Future<Null> _handleRefresh() async {
        // 模拟数据的延迟加载
        await Future.delayed(Duration(seconds: 2), () {
          setState(() {
            // 在列表开头添加几条数据
            List<String> _refreshData = List.generate(5, (i) => '下拉刷新数据${i + 1}');
            _list.insertAll(0, _refreshData);
          });
        });
      }
    }
    

    这里定义了一个下拉刷新回调方法_handleRefresh(),每次下拉刷新都会调用该方法,在该方法中利用Future.delayed()模拟网络请求延迟加载数据。需要注意,该方法的返回值必须是Future类型。

    // 下拉刷新方法
    Future<Null> _handleRefresh() async {
      // 模拟数据的延迟加载
      await Future.delayed(Duration(seconds: 2), () {
        setState(() {
          // 在列表开头添加几条数据
          List<String> _refreshData = List.generate(5, (i) => '下拉刷新数据${i + 1}');
          _list.insertAll(0, _refreshData);
        });
      });
    }
    

    这样,一个简单的列表下拉刷新的效果就实现了。


    我们在实际开发中有一种场景是:进入页面自动请求数据并显示加载进度圈,可以通过在根Widget中添加一个显示加载进度的组件(比如ProgressIndicator),加载数据前后动态显示和隐藏该组件来实现。但是既然我们已经使用了RefreshIndicator,可不可以直接利用它的下拉刷新进度圈呢?当然是可以的,这时候就需要利用RefreshIndicatorState了。
    RefreshIndicator是一个StatefulWidget,它的State由RefreshIndicatorState管理,我们可以通过RefreshIndicatorState来改变RefreshIndicator的状态,实现利用代码动态显示刷新进度圈。使用时需要使用GlobalKey对RefreshIndicatorState进行管理(我也不知道这样说是否准确。。。),需要显示刷新进度圈时再调用RefreshIndicatorState的show()方法即可,完整代码如下:
    import 'package:flutter/material.dart';
    
    class ListViewPage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return _ListViewPageState();
      }
    }
    
    class _ListViewPageState extends State<ListViewPage> {
      // ListView数据集合
      List<String> _list = new List();
    
      final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
          GlobalKey<RefreshIndicatorState>();
    
      @override
      void initState() {
        super.initState();
        // 显示加载进度圈
        _showRefreshLoading();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('列表的下拉刷新和上拉加载'),
          ),
          body: Container(
            child: RefreshIndicator(
              key: _refreshIndicatorKey,
              child: ListView.builder(
                itemBuilder: (context, index) => ListTile(
                      title: Text(_list[index]),
                    ),
                itemCount: _list.length,
              ),
              onRefresh: _handleRefresh,
            ),
          ),
        );
      }
    
      // 显示加载进度圈
      _showRefreshLoading() {
        // 这里使用延时操作是由于在执行刷新操作时_refreshIndicatorKey还未与RefreshIndicator关联
        Future.delayed(const Duration(seconds: 0), () {
          _refreshIndicatorKey.currentState.show();
        });
      }
    
      // 下拉刷新方法
      Future<Null> _handleRefresh() async {
        // 模拟数据的延迟加载
        await Future.delayed(Duration(seconds: 2), () {
          setState(() {
            // 在列表开头添加几条数据
            List<String> _refreshData = List.generate(5, (i) => '下拉刷新数据${i + 1}');
            _list.insertAll(0, _refreshData);
          });
        });
      }
    }
    

    initState()中执行了_showRefreshLoading()方法,需要注意由于initState()是在build()之前调用的,此时_refreshIndicatorKey还没有和Widget关联,直接调用_refreshIndicatorKey.currentState.show()会报错。解决方法就是通过Future.delay,设置延迟时间为0,保证执行show()方法时RefreshIndicatorState已经被赋值。调用show()之后会自动调用onRefresh指定的回调方法_handleRefresh(),同时显示加载进度圈,整个刷新效果看着还是比较自然的。

    2.上拉加载

    相比于下拉刷新,上拉加载的实现要相对麻烦一些。我查阅了一下网上的资料,实现上拉加载可以有两种方式:第一种是通过指定ListView的controller属性,类型是ScrollController,通过ScrollController可以判断ListView是否滑动到了底部,再进行上拉加载的处理;第二种是利用NotificationListener,监听ListVIew的滑动状态,当ListView滑动到底部时,进行上拉加载处理。

    方法一 利用ScrollController实现上拉加载更多

    ListView有一个controller属性,类型是ScrollController,通过ScrollController可以控制ListView的滑动状态,判断ListVIew是否滑动到了底部。判断的方式如下:

    ScrollController _scrollController;
    
    @override
    void initState() {
      super.initState();
      // 初始化ScrollController
      _scrollController = ScrollController();
      // 监听ListView是否滚动到底部
      _scrollController.addListener(() {
        if (_scrollController.position.pixels >=
            _scrollController.position.maxScrollExtent) {
          // 滑动到了底部
          print('滑动到了底部');
          // 这里可以执行上拉加载逻辑
          _loadMore();  
        }
      });
    }
    
    @override
    void dispose() {
      super.dispose();
      // 这里不要忘了将监听移除 
      _scrollController.dispose();
    }
    
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('列表的下拉刷新和上拉加载'),
        ),
        body: Container(
          child: RefreshIndicator(
            child: ListView.builder(
              itemBuilder: (context, index) => ListTile(
                    title: Text(_list[index]),
                  ),
              itemCount: _list.length,
              controller: _scrollController,
            ),
            onRefresh: _handleRefresh,
          ),
        ),
      );
    }
    

    _scrollController.position.pixels表示ListView当前滑动的距离,_scrollController.position.maxScrollExtent表示ListView可以滑动的最大距离,因此pixels >= maxScrollExtent就表示ListView已经滑动到了底部,这时执行加载更多的逻辑即可,这里依然是用Future.delayed()来模拟数据的延迟加载。当然不要忘记在dispose()方法中调用_scrollController.dispose()来移除监听,防止内存泄漏。

    // 上拉加载
    Future<Null> _loadMore() async {
      // 模拟数据的延迟加载
      await Future.delayed(Duration(seconds: 2), () {
        setState(() {
          List<String> _loadMoreData = List.generate(5, (i) => '上拉加载数据${i + 1}');
          _list.addAll(_loadMoreData);
        });
      });
    }
    

    这里还有一个小问题,就是在加载数据的过程中,继续上滑列表有可能会重复执行加载更多方法,为什么我说是有可能呢?加载更多方法是在滑动监听中通过判断执行的,也就是说如果我们在新数据还未加载出来时继续上滑列表,如果没有产生滑动偏移量,就不会执行addListener中的声明的逻辑;但是如果上滑的过程中产生了偏移量(哈哈,说不定你手滑了呢),就会进入到监听方法中,导致重复执行加载更多方法。解决这个问题的方法很简单,我们只需要声明一个变量isLoading来标识是否正在上拉加载就可以了,在加载数据前后更新isLoading的值。

    bool isLoading = false; // 是否正在加载,防止多次请求加载下一页
    
    // 上拉加载
    Future<Null> _loadMore() async {
      if (!isLoading) {
        setState(() {
          isLoading = true;
        });
        // 模拟数据的延迟加载
        await Future.delayed(Duration(seconds: 2), () {
          setState(() {
            isLoading = false;
            List<String> _loadMoreData =
                List.generate(5, (i) => '上拉加载数据${i + 1}');
            _list.addAll(_loadMoreData);
          });
        });
      }
    }
    

    这样就实现了列表滑动到底部上拉加载更多数据的效果,目前还有一点需要优化的地方,一般我们在加载更多时会在ListView底部显示一个加载进度圈,提示用户此时正在加载数据。实现方法很简单,就是为ListView添加一个Footer布局。

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('列表的下拉刷新和上拉加载'),
        ),
        body: Container(
          child: RefreshIndicator(
            child: ListView.builder(
              itemBuilder: (context, index) {
                if (index < _list.length) {
                  return ListTile(
                    title: Text(_list[index]),
                  );
                } else {
                  // 最后一项,显示加载更多布局
                  return _buildLoadMoreItem();
                }
              },
              itemCount: _list.length + 1,
              controller: _scrollController,
            ),
            onRefresh: _handleRefresh,
          ),
        ),
      );
    }
    
    // 加载更多布局
    Widget _buildLoadMoreItem() {
      return Center(
        child: Padding(
          padding: EdgeInsets.all(10.0),
          child: Text("加载中..."),
        ),
      );
    }
    

    这里为了简单,只用了一个Text提示用户正在加载,实际开发中可以根据需求定制自己的加载布局。添加该布局的方法是将ListView的itemCount指定为_list.length + 1,即添加一个item,然后itemBuilder中再根据index判断是否为最后一项,返回相应的布局就行了。值得一提的是,其实这里也是简单处理了,加载更多布局始终被添加到列表的最后一项,在实际应用中,我们需要根据具体情况来添加该布局。比如说,当数据集合为空或者数据全部加载完成后,就不需要显示加载更多布局,还有一种情况是数据没有填满整个屏幕时,此时显示加载更多布局就会很奇怪。
    到这里,我们基本上就实现了列表的上拉加载更多。

    完整的代码如下:

    import 'package:flutter/material.dart';
    
    class ListViewPage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return _ListViewPageState();
      }
    }
    
    class _ListViewPageState extends State<ListViewPage> {
      // ListView数据集合
      List<String> _list = List.generate(20, (i) => '原始数据${i + 1}');
      ScrollController _scrollController;
      bool isLoading = false; // 是否正在加载更多
    
      @override
      void initState() {
        super.initState();
        // 初始化ScrollController
        _scrollController = ScrollController();
        // 监听ListView是否滚动到底部
        _scrollController.addListener(() {
          if (_scrollController.position.pixels >=
              _scrollController.position.maxScrollExtent) {
            // 滑动到了底部
            print('滑动到了底部');
            // 这里可以执行上拉加载逻辑
            _loadMore();
          }
        });
      }
    
      @override
      void dispose() {
        super.dispose();
        _scrollController.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('列表的下拉刷新和上拉加载'),
          ),
          body: Container(
            child: RefreshIndicator(
              child: ListView.builder(
                itemBuilder: (context, index) {
                  if (index < _list.length) {
                    return ListTile(
                      title: Text(_list[index]),
                    );
                  } else {
                    // 最后一项,显示加载更多布局
                    return _buildLoadMoreItem();
                  }
                },
                itemCount: _list.length + 1,
                controller: _scrollController,
              ),
              onRefresh: _handleRefresh,
            ),
          ),
        );
      }
    
      // 加载更多布局
      Widget _buildLoadMoreItem() {
        return Center(
          child: Padding(
            padding: EdgeInsets.all(10.0),
            child: Text("加载中..."),
          ),
        );
      }
    
      // 下拉刷新方法
      Future<Null> _handleRefresh() async {
        // 模拟数据的延迟加载
        await Future.delayed(Duration(seconds: 2), () {
          setState(() {
            // 在列表开头添加几条数据
            List<String> _refreshData = List.generate(5, (i) => '下拉刷新数据${i + 1}');
            _list.insertAll(0, _refreshData);
          });
        });
      }
    
      // 上拉加载
      Future<Null> _loadMore() async {
        if (!isLoading) {
          setState(() {
            isLoading = true;
          });
          // 模拟数据的延迟加载
          await Future.delayed(Duration(seconds: 2), () {
            setState(() {
              isLoading = false;
              List<String> _loadMoreData =
                  List.generate(5, (i) => '上拉加载数据${i + 1}');
              _list.addAll(_loadMoreData);
            });
          });
        }
      }
    }
    

    方法二 利用NotificationListener实现上拉加载更多

    NotificationListener是一个Widget,可以监听子Widget发出的Notification。ListView在滑动的过程中会发出ScrollNotification类型的通知,我们可以通过监听该通知得到ListView的滑动状态,判断是否滑动到了底部。NotificationListener有一个onNotification属性,定义了监听的回调方法,通过它来处理加载更多逻辑。

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('列表的下拉刷新和上拉加载'),
        ),
        body: Container(
            child: NotificationListener<ScrollNotification>(
          onNotification: (ScrollNotification scrollNotification) {
            if (scrollNotification.metrics.pixels >=
                scrollNotification.metrics.maxScrollExtent) {
              // 滑动到了底部
              // 加载更多
              _loadMore();
            }
            return false;
          },
          child: RefreshIndicator(
            child: ListView.builder(
              itemBuilder: (context, index) {
                if (index < _list.length) {
                 return ListTile(
                    title: Text(_list[index]),
                  );
                } else {
                  // 最后一项,显示加载更多布局
                  return _buildLoadMoreItem();
                }
              },
              itemCount: _list.length + 1,
            ),
            onRefresh: _handleRefresh,
          ),
        )),
      );
    }
    

    判断ListView是否滑动到底部的逻辑和方法一相同,依然是通过比较ListView当前滑动的距离和可以滑动的最大距离。加载更多的逻辑也和方法一是一样的,这里就不多说了。
    完整的代码如下:

    import 'package:flutter/material.dart';
    
    class ListViewPage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return _ListViewPageState();
      }
    }
    
    class _ListViewPageState extends State<ListViewPage> {
      // ListView数据集合
      List<String> _list = List.generate(20, (i) => '原始数据${i + 1}');
      bool isLoading = false; // 是否正在加载更多
    
      @override
      void initState() {
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('列表的下拉刷新和上拉加载'),
          ),
          body: Container(
              child: NotificationListener<ScrollNotification>(
            onNotification: (ScrollNotification scrollNotification) {
              if (scrollNotification.metrics.pixels >=
                  scrollNotification.metrics.maxScrollExtent) {
                // 滑动到了底部
                // 加载更多
                _loadMore();
              }
              return false;
            },
            child: RefreshIndicator(
              child: ListView.builder(
                itemBuilder: (context, index) {
                  if (index < _list.length) {
                    return ListTile(
                      title: Text(_list[index]),
                    );
                  } else {
                    // 最后一项,显示加载更多布局
                    return _buildLoadMoreItem();
                  }
                },
                itemCount: _list.length + 1,
              ),
              onRefresh: _handleRefresh,
            ),
          )),
        );
      }
    
      // 加载更多布局
      Widget _buildLoadMoreItem() {
        return Center(
          child: Padding(
            padding: EdgeInsets.all(10.0),
            child: Text("加载中..."),
          ),
        );
      }
    
      // 下拉刷新方法
      Future<Null> _handleRefresh() async {
        // 模拟数据的延迟加载
        await Future.delayed(Duration(seconds: 2), () {
          setState(() {
            // 在列表开头添加几条数据
            List<String> _refreshData = List.generate(5, (i) => '下拉刷新数据${i + 1}');
            _list.insertAll(0, _refreshData);
          });
        });
      }
    
      // 上拉加载
      Future<Null> _loadMore() async {
        if (!isLoading) {
          setState(() {
            isLoading = true;
          });
          // 模拟数据的延迟加载
          await Future.delayed(Duration(seconds: 2), () {
            setState(() {
              isLoading = false;
              List<String> _loadMoreData =
                  List.generate(5, (i) => '上拉加载数据${i + 1}');
              _list.addAll(_loadMoreData);
            });
          });
        }
      }
    }
    

    总结

    1.列表的下拉刷新是通过包裹一层RefreshIndicator,自定义onRefresh回调方法实现的
    2.列表上拉加载的基本思路是监听列表滑动状态,当列表滑动到底部时,调用定义好的加载更多逻辑。监听列表滑动状态有两种方式:ScrollControllerNotificationListener,这两种方式的实现差不多,选择自己用得习惯的就好了,在使用ScrollController时要记得移除监听。

    参考资料

    《Flutter实战》可滚动Widgets简介
    ListView下拉刷新与加载更多

    相关文章

      网友评论

        本文标题:Flutter学习小计:ListView的下拉刷新和上拉加载

        本文链接:https://www.haomeiwen.com/subject/kejxyqtx.html