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

一.创建页面

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

dart 复制代码
class ProjectListPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _ProjectListPageState();
}

class _ProjectListPageState extends State<ProjectListPage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("项目列表")
      ),
      body: Container()
    );
  }

}

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

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

dart 复制代码
class ProjectListPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _ProjectListPageState();
}

class _ProjectListPageState extends State<ProjectListPage> {

  late Future<PageModel<ProjectModel>> future;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    future = IndexDao.getProjectList(cid: 0, start: 1);
  }

  @override
  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}");
            }
          }
        },
      )
    );
  }

}

三.渲染列表

dart 复制代码
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
  );
}

四.实现下拉刷新

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

dart 复制代码
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);
    });
  });
}

五.上拉加载更多

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

dart 复制代码
late ScrollController _controller;

@override
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,
    	...
  	)
  );
}

也可以使用NotificationListener监听

dart 复制代码
late ScrollController _controller;
@override
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,
    		...
  		)
  	)
  )
}

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

dart 复制代码
enum LoadMoreStatus { more, loading, error, noMore }

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

dart 复制代码
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
)

实现上拉加载更多

dart 复制代码
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();
  }
});

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

dart 复制代码
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;
      }
    });
  });
}

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

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

dart 复制代码
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),
            ),
          ),
        )
      ],
    )
  );
}
相关推荐
y先森21 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy22 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891125 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿1 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡2 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐3 小时前
前端图像处理(一)
前端