Flutter 常用ListView和RefreshIndicator组件实现列表页面的上拉加载更多下拉刷新

更新于 1月14日  约 16 分钟

APP中最常见的就是列表页面,上拉加载更多,下拉刷新,在FlutterListView 是最常用的可滚动组件之一,这里我主要使用ListView实现列表加载,并配合RefreshIndicator组件实现下拉刷新;还会使用到ListView的嵌套使用等。

创建 Stateful Widgets

import 'dart:convert';
import 'package:app/common/httpUtil.dart';
import 'package:app/common/toast.dart';
import 'package:app/api/Api.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

class Questionnaire extends StatefulWidget {
  @override
  _QuestionnaireState createState() => _QuestionnaireState();
}

class _QuestionnaireState extends State<Questionnaire> {
  // 列表视图(`ListView`)中要显示的数据。
  List questionnaireList = new List();

  ScrollController _scrollController = new ScrollController();

  bool isLoading = true;

  // 总页数
  int totalPages = 1;

  // 当前页数
  int pageno = 0;  
  
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("问卷调查"),
      ),
      body: Container(
         child: _buildList(),
      ),
      resizeToAvoidBottomPadding: false,
    );
  }
 } 

使用dio获取数据


  void _getMoreData() async {
    if (isLoading) {
      try {
       // size每页数据条数、start起始页 0,1,2,...
        String url = "/xxxx/xxxlist.json?size=10&start=" + pageno.toString();
        // print('接口 pageno:' + pageno.toString());
        Response response = await dio.get(url);
        // print(response);

        setState(() {
          // 处理返回数据
          // 总页数
          totalPages = response.data['totalPages'];
          // print(totalPages);
          questionnaireList.addAll(response.data['content']);
          // print(questionnaireList);
        });
      } on DioError catch (e) {
        //catch 提示
        print('catch 提示: ' + e.toString());
        if (e.response != null) {
          print(e.response.data);
          dynamic rtn = jsonDecode(e.response.data.toString()); // 解析接口返回的json数据
          // print(rtn['status']);
          if (rtn['status'] == 401) {
            autoLogin().then((val) => initState()); // 自动登录后 调用initState更新页面
          }
        } else {
          showToast("数据加载失败");
          print(e.request);
          print(e.message);
        }
      } finally {
        setState(() {
          isLoading = false;
        });
      }
    }
  }

以上使用的dio是https://segmentfault.com/a/1190000021567794#item-2-1 这篇文章提到的创建了一个全局共用的dio。

下拉刷新

build中body改为包裹一层RefreshIndicator组件,onRefresh为重新获取数据的方法。

    body: RefreshIndicator(
        onRefresh: _onRefresh,
        child: _buildList(),
      ),
  /*
   * 下拉刷新方法 
   */
  Future<Null> _onRefresh() async {
    questionnaireList.clear();
    setState(() {
      isLoading = true;
    });
    _getMoreData();
  }

上拉加载更多

ListView 支持 scrollController 事件绑定,当用户在ListView中滑动时,会出发 scrollController 事件。
scrollController 组件:scrollController 是一个滑动监听组件,这里我们用来控制何时加载数据。

scrollController中增加监听事件addListener,判断滑动页面时,如果没有拉到底部,并且数据不是正在加载中状态,是否是最后一页,等条件逻辑,来决定是否加载更多数据,并通过setState来更新视图。

  @override
  void initState() {
    setState(() {
      isLoading = true;
    });
    this._getMoreData();
    super.initState();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        // print('滑动到了最底部');
        if (pageno < totalPages - 1) {
          pageno++;
          // print('加载更多 pageno:' + pageno.toString());
          // 加载更多
          setState(() {
            isLoading = true;
          });
          _getMoreData();
        } else {
          // 没有更多了
          showToast("没有更多了");
        }
      }
    });
  }
  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

以上使用到的showToastFlutter 常用的提示框showToast、showLoading、showConfirmDialog写在lib/common/toast.dart中的全局共用方法。

ListView 显示

ListView builder中,我们使用条件判断来让最后一行显示暂无数据、加载中动画(ProgressBar)、数据列表。

  Widget _buildProgressIndicator() {
    return new Padding(
      padding: const EdgeInsets.all(8.0),
      child: new Center(
        child: new Opacity(
          opacity: isLoading ? 1.0 : 00,
          child: new CircularProgressIndicator(),
        ),
      ),
    );
  }
  
  Widget _buildList() {
    return ListView.builder(
      //itemCount +1 为了显示加载中progressbar和暂无数据
      itemCount: questionnaireList.length + 1,
      itemBuilder: (context, index) {
        if (questionnaireList.length == 0 && isLoading) {
          // 加载中
          return _buildProgressIndicator();
        } else if (questionnaireList.length == 0 && !isLoading) {
          // 暂无数据
          return Padding(
            padding: const EdgeInsets.all(18.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                new Text('暂无数据!'),
              ],
            ),
          );
        } else if (questionnaireList.length == index && !isLoading) {
          // 多加的那个1,其实没数据应该不显示,去掉这个显示会报错
          return _buildProgressIndicator();
        } else {
          // 列表显示
          return new GestureDetector(
            // 列表的点击tap事件
            onTap: () => _handleTapToDetail(conductStatus),
            child: Card(
              child: _buildContainer(),
            ),
          );
        }
      },
      controller: _scrollController,
    );
  }
  
  _handleTapToDetail(String conductStatus){
     //  列表的点击tap事件 进入详情或者其他操作等逻辑
  }
  
  _buildContainer(){
    // 具体显示列表内容的布局,此处略
  }

以上讲述了用ListView和RefreshIndicator组件实现列表页面的上拉加载更多下拉刷新。

ListView 嵌套 ListView 滚动问题

上述的列表是以问卷调查为例,那么这里使用到的ListView 嵌套 ListView的情况就是问卷的题目列表的展示,大家平时都做过问卷、试卷,知道题目列表包含:大题的题目,小题的题目,小题的选项等,做这种布局,就用到了ListView 嵌套。

最重要的是最外层ListView设置controller: _scrollController,并且要设置shrinkWrap: true,根据子widget的总长度来设置ListView的长度。然后内里嵌套的所有子ListView,设置shrinkWrap: true根据子widget的总长度来设置ListView的长度,并且设置 physics: new NeverScrollableScrollPhysics() 禁用问题列表子组件的滚动事件。

     new ListView(
          shrinkWrap: true, //是否根据子widget的总长度来设置ListView的长度,默认值为false
          controller: _scrollController,
          children: <Widget>[
                new Text(
                    "title",
                    overflow: TextOverflow.ellipsis,
                    maxLines: 2,
                    style: TextStyle(
                      color: Color.fromRGBO(0, 0, 0, 1.0), //opacity:不透明度
                      fontFamily: 'PingFangBold',
                      fontSize: 15.0,
                    ),
                  ),
                 Container(
                    child: _buildList(),
                  ),
          ],
        ),

  Widget _buildList() {
    return ListView.builder(
      shrinkWrap: true, //是否根据子widget的总长度来设置ListView的长度,默认值为false
      physics: new NeverScrollableScrollPhysics(), // 禁用问题列表子组件的滚动事件
      //itemCount +1 为了显示加载中和暂无数据progressbar
      itemCount: dataList.length + 1,
      itemBuilder: (context, index) {
          // 逻辑与上述相似,不再重复,省略了
      },
    );
  }

参考资料

ListView class: A scrollable list of widgets arranged linearly.
ListView

Flutter下拉刷新,上拉加载更多数据
Flutter ListView 分页加载更多效果
Flutter 问题解决总结:ScrollView 嵌套 ListView 滚动问题
flutter禁用滚动事件

阅读 226更新于 1月14日

推荐阅读
北堂棣的随记
用户专栏

记录学习过程、、、记录一点一滴、、、记录自己的思路。。。

18 人关注
41 篇文章
专栏主页
目录