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

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

前言
最近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);
    });
  });
}

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

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

我们在实际开发中有一种场景是:进入页面自动请求数据并显示加载进度圈,可以通过在根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(),同时显示加载进度圈,整个刷新效果看着还是比较自然的。

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

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判断是否为最后一项,返回相应的布局就行了。值得一提的是,其实这里也是简单处理了,加载更多布局始终被添加到列表的最后一项,在实际应用中,我们需要根据具体情况来添加该布局。比如说,当数据集合为空或者数据全部加载完成后,就不需要显示加载更多布局,还有一种情况是数据没有填满整个屏幕时,此时显示加载更多布局就会很奇怪。
到这里,我们基本上就实现了列表的上拉加载更多。

《Flutter学习小计: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}');
  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下拉刷新与加载更多

    原文作者:快乐丸
    原文地址: https://www.jianshu.com/p/73a83e525743
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞