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),
            ),
          ),
        )
      ],
    )
  );
}
相关推荐
bloxed4 分钟前
前端文件下载多方式集合
前端·filedownload
余生H10 分钟前
前端Python应用指南(三)Django vs Flask:哪种框架适合构建你的下一个Web应用?
前端·python·django
LUwantAC18 分钟前
CSS(四)display和float
前端·css
cwtlw22 分钟前
CSS学习记录20
前端·css·笔记·学习
界面开发小八哥27 分钟前
「Java EE开发指南」如何用MyEclipse构建一个Web项目?(一)
java·前端·ide·java-ee·myeclipse
米奇妙妙wuu42 分钟前
react使用sse流实现chat大模型问答,补充css样式
前端·css·react.js
傻小胖1 小时前
React 生命周期完整指南
前端·react.js
梦境之冢1 小时前
axios 常见的content-type、responseType有哪些?
前端·javascript·http
racerun1 小时前
vue VueResource & axios
前端·javascript·vue.js
m0_548514772 小时前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript