简介
下拉刷新,上拉加载更多,是一个比较常见的需求。而这个一般是和表格之类的组件一起出现的。
说实话,和弹窗和Toast等类似,要自己写的话比较麻烦,所以一般采用插件比较方便。
easy_refresh
在这里,我们选取的插件是easy_refresh,总体感觉还是可以的
样式
- 分为头部和底部样式
- 可以设置成全局一致的
// 全局设置
EasyRefresh.defaultHeaderBuilder = () => RefreshUtils.header();
EasyRefresh.defaultFooterBuilder = () => RefreshUtils.footer();
- 除非特殊要求,一般用插件给的ClassicXXX就可以了,提醒文字
class RefreshUtils {
static header() {
return ClassicHeader(
textStyle: const TextStyle(color: Color(0xff666666)),
messageStyle: const TextStyle(color: Color(0xff666666)),
iconTheme: const IconThemeData(color: Color(0xff666666)),
dragText: '拉取刷新'.tr,
armedText: '准备就绪'.tr,
readyText: '刷新。。。'.tr,
processingText: '刷新。。。'.tr,
processedText: '刷新成功!'.tr,
noMoreText: '没数据了'.tr,
failedText: '失败'.tr,
messageText: '${'上次更新'.tr} %T',
mainAxisAlignment: MainAxisAlignment.end,
);
}
static footer() {
return ClassicFooter(
textStyle: const TextStyle(color: Color(0xff666666)),
messageStyle: const TextStyle(color: Color(0xff666666)),
iconTheme: const IconThemeData(color: Color(0xff666666)),
dragText: '拉到加载'.tr,
armedText: '准备就绪'.tr,
readyText: '加载中...'.tr,
processingText: '加载中...'.tr,
processedText: '加载成功!'.tr,
noMoreText: '没数据了'.tr,
failedText: '失败'.tr,
messageText: '${'上次更新'.tr} %T',
infiniteOffset: 70, // 这里给null就要主动上拉
);
}
}
基础组件
一般需要上拉下拉的都是表格之类的,性能问题比较突出。并且每个页面都要套一个EasyRefresh.builder也比较麻烦,所以干脆做一个通用组件比较合适。大家都用同一套东西。
@override
Widget build(BuildContext context) {
return GetBuilder<OtherHomeLogic>(builder: (_) {
return Scaffold(
body: EasyRefresh.builder(
header: RefreshUtils.header(),
footer: RefreshUtils.footer(),
onRefresh: logic.onRefresh,
onLoad: logic.onLoad,
childBuilder: (context, physics) {
return CustomScrollView(
physics: physics,
slivers: [
headWidget() ,
contentWidget(),
],
);
},
),
backgroundColor: StyleUtils.backgroundColor4,
);
});
}
Widget headWidget() {
return SliverVisibility(
visible: true,
sliver: SliverAppBar(),
);
}
Widget contentWidget() {
return SliverToBoxAdapter(
child: Column(
children: [],
),
);
}
这里只是一个封装思路:头部有可能不显示,所以包在一个Visibility组件中,用参数控制(头部组件都可以放出一些参数配置)。至于内容部分,放在一个Column组件中,比sliver家族用起来方便很多。
EasyRefreshController
- 这个就是上拉下拉的控制器。可以为nil,默认也是不控制。
EasyRefreshController({
this.controlFinishRefresh = false,
this.controlFinishLoad = false,
});
大多数情况下,默认情况就可以了,下拉和上拉的时候有一定的提示,一段时间之后,上拉和下拉的提示就结束,然后显示上拉或者下拉成功的状态。
假如需要上拉和下拉要反映接口的实际状态,那么就要引入EasyRefreshController,进行控制。简单讲就是在接口结束的时候调用finishRefresh或者finishLoad方法,结束加载的状态。
状态定义:成功和失败很好立即。这个noMore比较特殊,是上拉加载特有的。设定这个状态之后,load方法就不会再触发了。
/// The status returned after the task is completed.
enum IndicatorResult {
/// No state until the task is not triggered.
none,
/// Task succeeded.
success,
/// Task failed.
fail,
/// No more data.
noMore,
}
- 这个none状态也比较特殊,reset方法就是把noMore状态设置为none状态。猜测他的作用就是让上拉加载重新出发load方法的调用。
/// Reset Footer indicator state.
void resetFooter() {
_state?._footerNotifier._reset();
}
/// Reset partial state, e.g. no more.
void _reset() {
if (_result == IndicatorResult.noMore) {
_result = IndicatorResult.none;
}
}
分页的两种方式
size参数一般都是需要的,就是每次调用返回的数据量。
一种方式是page参数,也就是当前页码,从1开始往上增长。这种比较常见。
另外一种是lastId,就是上次数据的最后一个元素的Id。这种比较少见,不过我们这次的确是遇到了。
如何复用
不复用的话,至少size和page或者lastId,还有EasyRefreshController这几个参数都是要重复定义的。
EasyRefresh本身就是组件,里面嵌入一个List之类的滑动组件就有上拉和下拉的状态。
如果要实现上拉,下拉的状态控制,就要一个额外的EasyRefreshController参数,游离在组件之外,所以封装成Widget的方式比较困难。
由于我们使用GetX,每个页面都有一个GetxController。所以可以考虑采用继承的方式,将上拉下拉这些基础变量做在这个基类之中。
基类的设计
变量
我们要同时支持page和lastId两种方式,所以这3个变量是至少的。
由于一个页面中,有可能只用一种方式,也有可能同时用两种方式,也有可能一种都不用,所以引入两个变量usePage和useLastId来控制。
由于lastId是最后一个数据元素的id,所以需要一个变量来保存数据,就用List,数据类型就是字典,和约定好字段,比如id之类。我们直接取名叫dataList
上拉加载,是否有更多数据,这个需要一个变量来保存,我们就取名为hasMore
结束是成功还是失败,并且上拉和下拉分开,这样又引入两个变量refreshSuccess,loadSuccess
还要区分一下上拉还是下拉,所以再引入一个变量isRefresh
/// GetX中,与page配套使用的logic
class RefreshGet extends GetxController {
int page = 1;
int size = 20;
String lastId = '';
bool usePage = true;
bool useLastId = true;
List dataList = [];
bool hasMore = true;
bool refreshSuccess = true;
bool loadSuccess = true;
bool isRefresh = true;
/// 控制器
EasyRefreshController refreshController = EasyRefreshController(
controlFinishRefresh: true, controlFinishLoad: true);
@override
void onClose() {
refreshController.dispose();
super.onClose();
}
}
上拉下拉
EasyRefresh组件需要上拉和下拉函数,这个定义在基类中就可以了。
Future onRefresh() async {
isRefresh = true;
lastId = '';
dataList = [];
page = 1;
hasMore = true;
usePage = true;
useLastId = true;
refreshSuccess = true;
loadSuccess = true;
resetFooterAndHeader();
refreshFunc();
}
Future onLoad() async {
isRefresh = false;
if (useLastId && dataList.isNotEmpty) {
lastId = dataList.last['id'] ?? "";
}
if (usePage) {
page++;
}
resetFooter();
loadFunc();
}
void refreshFunc() {}
void loadFunc() {}
这里的refreshFunc和loadFunc是方便方法。在子类中,只要重写他们,直接把上拉和下拉的接口函数塞这里就可以了。
重置是为了取消noMore状态,让上拉加载重新调用onLoad函数。
/// 重置,将noMore状态改为none
void resetFooterAndHeader() {
resetFooter();
resetHeader();
}
void resetFooter() {
refreshController.resetFooter();
}
void resetHeader() {
refreshController.resetHeader();
}
状态设置
简单讲就是在接口函数结束之后,设置上拉和下拉的状态,提示数据的状态。
/// 手动更新refresh或者load状态;isAll表示两者都做
void onFinish({bool isAll = false}) {
if (isAll) {
updateHeader();
updateFooter();
} else {
if (isRefresh) {
updateHeader();
} else {
updateFooter();
}
}
}
/// 更新头部
void updateHeader() {
resetHeader();
if (refreshSuccess) {
refreshController.finishRefresh(IndicatorResult.success);
} else {
refreshController.finishRefresh(IndicatorResult.fail);
}
}
/// 更新底部
void updateFooter() {
resetFooter();
if (hasMore) {
if (loadSuccess) {
refreshController.finishLoad(IndicatorResult.success);
} else {
if (usePage && (page > 1)) {
page--;
}
refreshController.finishLoad(IndicatorResult.fail);
}
} else {
refreshController.finishLoad(IndicatorResult.noMore);
}
}
设置结果
接口调用是否成功,需要进行记录。一般上拉和下拉不同时进行,所以需要分别保存
/// 设置结果标志,上拉下拉分开保存。isAll表示两个标记存成一样的
void updateResult(bool isSuccess, {bool isAll = false}) {
if (isAll) {
refreshSuccess = isSuccess;
loadSuccess = isSuccess;
} else {
if (isRefresh) {
refreshSuccess = isSuccess;
} else {
loadSuccess = isSuccess;
}
}
}
如何使用
- 在界面文件中用上Controller,onRefresh和onLoad函数
body: SafeArea(
child: EasyRefresh.builder(
controller: logic.refreshController,
header: RefreshUtils.header(),
footer: RefreshUtils.footer(),
onRefresh: logic.onRefresh,
onLoad: logic.onLoad,
childBuilder: (context, physics) {
return CustomScrollView(
controller: _controller,
physics: physics,
slivers: [
mapModule(),
everyoneBrowsing(),
everyoneDiscuss(),
],
);
}),
),
- 对应的logic文件继承我们的基类RefreshGet
class HomeLogic extends RefreshGet { }
- 重写空方法refreshFunc和loadFunc,指定具体的上拉和下拉动作。由于基类中是空方法,可以省略super的调用。当然,写上也没问题。
@override
void refreshFunc() {
super.refreshFunc();
/// 这里用lastId的方式
usePage = false;
useLastId = true;
/// 刷新函数;只有getThemeListFeeds是分页函数;getHotGameList是页面的其他部分
getHotGameList();
getThemeListFeeds();
}
@override
void loadFunc() {
super.loadFunc();
/// 分页函数
getThemeListFeeds();
}
改写分页函数
需要分页的函数与普通函数不一样,需要改写相关的分页参数。
/// 获取主题列表feeds
void getThemeListFeeds() async {
if (!hasMore) {
LogUtil.log("getThemeListFeeds没有更多了,直接返回");
onFinish();
update();
return;
}
ApiResponse response = await HomeApi.getThemeListFeeds({'size': size, 'lastId': lastId});
if (response.code == 0) {
/// 标记成功
updateResult(true);
/// 追加本次分页数据
List data = response.data ?? [];
if (data.isNotEmpty) {
dataList.addAll(data);
}
/// 如果本次返回的数据少于size,说明没有更多了
if (data.length < size) {
hasMore = false;
} else {
hasMore = true;
}
} else {
/// 标记失败
updateResult(false);
}
/// 更新上拉下拉组件状态
onFinish();
/// 更新页面
update();
}
小结
将上拉下拉需要的一些变量和方法下沉到基类中,在实际使用的时候,相对还是比较简单的。如果偷懒一点usePage和useLastId这两个变量都不用设置,保持true就可以了。page不断自增,反正又不用,也没有关系。反之,lastId不用的话,一直设置为空字符串,放着也没关系。
如果id的字段不是“id”,其实关系也不大,只要在refreshFunc方法中重新设置一下lastId就可以了
如果想用默认的上拉下拉状态,只要在EasyRefresh不设置controller参数就可以了。基类中多一个refreshController变量的创建和销毁,无伤大雅。