• 01_Flutter之下拉刷新和上拉加载


    一.创建页面

    由于我们需要请求网络,并将返回的数据渲染到页面上,所以需要继承StatefulWidget,本文涉及的接口,取自鸿神的玩android开放API

    class ProjectListPage extends StatefulWidget {
      
      State<StatefulWidget> createState() => _ProjectListPageState();
    }
    
    class _ProjectListPageState extends State<ProjectListPage> {
    
      
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text("项目列表")
          ),
          body: Container()
        );
      }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    二.使用FutureBuilder异步初始化页面数据

    通过FutureBuilder,我们可以从互联网上获取数据的过程中显示一个加载框,等获取数据成功时再渲染页面,本文的重点不是讲FutureBuilder怎么使用,就不做过多解释了,直接上代码:

    class ProjectListPage extends StatefulWidget {
      
      State<StatefulWidget> createState() => _ProjectListPageState();
    }
    
    class _ProjectListPageState extends State<ProjectListPage> {
    
      late Future<PageModel<ProjectModel>> future;
    
      
      void initState() {
        // TODO: implement initState
        super.initState();
        future = IndexDao.getProjectList(cid: 0, start: 1);
      }
    
      
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text("项目列表")
          ),
          body: FutureBuilder<PageModel<ProjectModel>>(
            future: future,
            builder: (BuildContext context, AsyncSnapshot<PageModel<ProjectModel>> snapshot) {
              if (snapshot.connectionState != ConnectionState.done) {
                //请求中,显示加载圈
                return const Center(
                  child: SizedBox(
                    width: 30,
                    height: 30,
                    child: CircularProgressIndicator(),
                  ),
                );
              } else {
                //请求结束
                if (snapshot.hasError) {
                // 请求失败,显示错误
                  return Text("Error: ${snapshot.error}");
                } else {
                  // 请求成功,显示数据
                  return Text("data: ${snapshot.data}");
                }
              }
            },
          )
        );
      }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    在这里插入图片描述

    三.渲染列表

    if (snapshot.hasError) {
      // 请求失败,显示错误
      return Text("Error: ${snapshot.error}");
    } else {
      // 请求成功,显示数据
      List<ProjectModel> datas = snapshot.data?.records ?? [];
      return ListView.separated(
        padding: EdgeInsets.all(10),
        itemBuilder: (BuildContext context, int index) {
          return Container(
            padding: const EdgeInsets.all(10),
            decoration: const BoxDecoration(
              borderRadius: BorderRadius.all(Radius.circular(5)),
              color: Colors.white,
            ),
            child: IntrinsicHeight(
              child: Row(
                mainAxisSize: MainAxisSize.max,
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  SizedBox(
                    width: 120,
                    height: 1,
                    child: Image.network(datas[index].envelopePic ?? "", fit: BoxFit.cover),
                  ),
                  SizedBox(width: 10,),
                  Expanded(
                    flex: 1,
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      mainAxisAlignment: MainAxisAlignment.center,
                      crossAxisAlignment: CrossAxisAlignment.stretch,
    
                      children: [
                        Text(
                          "${datas[index]?.title}",
                          maxLines: 2,
                          style: const TextStyle(
                            overflow: TextOverflow.ellipsis,
                            fontSize: 16
                          ),
                        ),
    
                        const SizedBox(
                          height: 10,
                        ),
    
                        Text(
                          "${datas[index]?.desc}",
                          maxLines: 2,
                          style: const TextStyle(
                            overflow: TextOverflow.ellipsis,
                            fontSize: 14
                          )
                        ),
                      ],
                    )
                  )
                ],
              ),
            ),
          );
        },
        separatorBuilder: (BuildContext context, int index) {
          return const Divider(color: Colors.transparent, height: 10,);
        },
        itemCount: datas.length
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    在这里插入图片描述

    四.实现下拉刷新

    直接使用Flutter内置的RefreshIndicator实现下拉刷新

    int start = 1;
    
    RefreshIndicator(
      onRefresh: () {
        return _refreshData();
      },
      child: ListView.separated(...)
    );
      
    Future<void> _refreshData() {
      start = 1;
    
      return IndexDao.getProjectList(cid: 0, start: start).then((value) {
        setState(() {
          datas.clear();
          datas.addAll(value.records);
        });
      });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述

    五.上拉加载更多

    重点来了,我们应该在何时去加载更多数据呢?那自然是ListView滑动到底部的时候。可以通过ScrollController监听

    late ScrollController _controller;
    
    
    void initState() {
      // TODO: implement initState
      super.initState();
      future = IndexDao.getProjectList(cid: 0, start: 1);
      _controller = ScrollController();
      _controller.addListener(() {
        if(_controller.position.extentAfter == 0) {
          //划动到底部了,加载更多数据
          print("划动到底部了,加载更多数据");
        }
      });
    }
    
    Widget build(BuildContext context) {
      ...
      return RefreshIndicator(
        onRefresh: () {
          return _refreshData();
        },
        child: ListView.separated(
        	controller: _controller,
        	...
      	)
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    也可以使用NotificationListener监听

    late ScrollController _controller;
    
    void initState() {
      // TODO: implement initState
      super.initState();
      future = IndexDao.getProjectList(cid: 0, start: 1);
      _controller = ScrollController();
    }
    
    Widget build(BuildContext context) {
      return NotificationListener<ScrollEndNotification>(
        onNotification: (ScrollEndNotification notification) {
          if (_controller.position.extentAfter == 0) {
          	//滚动到底部
          	//加载更多数据
          }
          return false;
        },
        child: RefreshIndicator(
          onRefresh: () {
            return _refreshData();
          },
        	child: ListView.separated(
        		controller: _controller,
        		...
      		)
      	)
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    加载更多数据,分别对应四种加载状态,more:有更多数据,loading: 加载中,noMore: 没有更多数据了,error: 请求网络出错了

    enum LoadMoreStatus { more, loading, error, noMore }
    
    • 1

    我们需要根据这四种加载状态,显示不同的footer,并且,ListView的itemCount需要在原有基础上加一,预留出一个位置,显示Footer

    ListView.separated(
      ...
      itemBuilder: (BuildContext context, int index) {
        if(index == datas.length) {
          if(loadMoreStatus == LoadMoreStatus.more) {
            return const SizedBox(
              height: 40,
              child: Center(
                child: Text("上拉显示更多"),
              ),
            );
          } else if(loadMoreStatus == LoadMoreStatus.loading) {
            return const SizedBox(
              height: 40,
              child: Center(
                child: Text("正在加载..."),
              ),
            );
          } else if(loadMoreStatus == LoadMoreStatus.noMore) {
            return const SizedBox(
              height: 40,
              child: Center(
                child: Text("没有更多数据了"),
              ),
            );
          } else {
            return const SizedBox(
              height: 40,
              child: Center(
                child: Text("出错了-_-,上拉重新加载"),
              ),
            );
          }
        } else {
          ...
        }
      },
      itemCount: datas.length + 1
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    实现上拉加载更多

    void _loadMoreData() {
      if(loadMoreStatus == LoadMoreStatus.noMore) {
        return;
      }
    
      if(loadMoreStatus == LoadMoreStatus.loading) {
        return;
      }
    
      int page = start;
      if(loadMoreStatus != LoadMoreStatus.error) {
        page += 1;
      }
    
      setState(() {
        loadMoreStatus = LoadMoreStatus.loading;
      });
    
      IndexDao.getProjectList(cid: 0, start: page).then((value) {
        start = page;
    
        setState(() {
          if(value.hasNextPage) {
            loadMoreStatus = LoadMoreStatus.more;
          } else {
            loadMoreStatus = LoadMoreStatus.noMore;
          }
          datas.addAll(value.records);
        });
      }).onError((error, stackTrace) {
        setState(() {
          loadMoreStatus = LoadMoreStatus.error;
        });
        return Future.error(error!, stackTrace);
      });
    }
    
    _controller.addListener(() {
      if(_controller.position.extentAfter == 0) {
        //划动到底部了,加载更多数据
        _loadMoreData();
      }
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    在这里插入图片描述

    六.Fixed:滑动到最后一页,下拉刷新数据,没有将加载状态重置为more

    在这里插入图片描述

    Future<void> _refreshData() {
      start = 1;
      setState(() {
        loadMoreStatus = LoadMoreStatus.more;
      });
      
      return IndexDao.getProjectList(cid: 0, start: start).then((value) {
    
        setState(() {
          datas.clear();
          datas.addAll(value.records);
          hasMore = value?.hasNextPage ?? false;
          if(hasMore) {
            loadMoreStatus = LoadMoreStatus.more;
          } else {
            loadMoreStatus = LoadMoreStatus.noMore;
          }
        });
      });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    七.Fixed:第一页数据不足一屏时,不能触发下拉刷新和加载更多

    这种情况属于极端情况,可根据实际情况考虑是否需要修复,可以使用CustomScrollView结合SliverList、SliverFillRemaining修复

    Widget build(BuildContext context) {
      return RefreshIndicator(
        onRefresh: () {
          return _refreshData();
        },
        child: CustomScrollView(
          controller: _controller,
          slivers: [
            SliverPadding(
              padding: EdgeInsets.all(10),
              sliver: SliverList.separated(
                itemCount: datas.length,
                itemBuilder: (BuildContext context, int index) {
                  return Container(
                    padding: const EdgeInsets.all(10),
                    decoration: const BoxDecoration(
                      borderRadius: BorderRadius.all(Radius.circular(5)),
                      color: Colors.white,
                    ),
                    child: IntrinsicHeight(
                      child: Row(
                        mainAxisSize: MainAxisSize.max,
                        mainAxisAlignment: MainAxisAlignment.center,
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: [
                          SizedBox(
                            width: 120,
                            height: 1,
                            child: Image.network(datas[index].envelopePic ?? "", fit: BoxFit.cover),
                          ),
                          SizedBox(width: 10,),
                          Expanded(
                            flex: 1,
                            child: Column(
                              mainAxisSize: MainAxisSize.min,
                              mainAxisAlignment: MainAxisAlignment.center,
                              crossAxisAlignment: CrossAxisAlignment.stretch,
    
                              children: [
                                Text(
                                  "${datas[index]?.title}",
                                  maxLines: 2,
                                  style: const TextStyle(
                                    overflow: TextOverflow.ellipsis,
                                    fontSize: 16
                                  ),
                                ),
    
                                const SizedBox(
                                  height: 10,
                                ),
    
                                Text(
                                  "${datas[index]?.desc}",
                                  maxLines: 2,
                                  style: const TextStyle(
                                    overflow: TextOverflow.ellipsis,
                                    fontSize: 14
                                  )
                                ),
                              ],
                            )
                          )
                        ],
                      ),
                    ),
                  );
                },
                separatorBuilder: (BuildContext context, int index) {
                  return const Divider(color: Colors.transparent, height: 10,);
                },
              ),
            ),
    
            //填充剩余空间
            SliverFillRemaining(
              hasScrollBody: false,
              fillOverscroll: false,
              child: Container(),
            ),
    
            SliverToBoxAdapter(
              child: Container(
                padding: const EdgeInsets.only(bottom: 10),
                height: 40,
                child: Center(
                  child: Text(tips),
                ),
              ),
            )
          ],
        )
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
  • 相关阅读:
    STM32F1网络编程-HTTP服务器(基于W5500网卡)
    学物理的计算机不错是什么体验
    java通过调打印机实现打印图片
    小恐龙快跑,不要被逮到拉~ python制作小恐龙游戏
    云服务器部署LNMP Web环境教程合集(多版linux系统安装方法)
    Java中「Future」接口详解
    【前端2】jquary(选择器),bootstrap(布局容器),vue(创建实例)
    MongoDB 存储引擎
    HDFS、MapReduce原理--学习笔记
    Linux Shell脚本:从文件中读取文件路径并以相同目录结构复制到当前目录
  • 原文地址:https://blog.csdn.net/gzx110304/article/details/132682811