組件選擇
下拉刷新,直接使用
RefreshIndicator
的概率不是很大。一方面視覺效果不好,另一方面還缺少上拉加載功能。第三方插件easy_refresh;比較好用,我們一直用這個。如果不是在NestedScrollView中使用上拉加載指示器不能消失,我們也不會嘗試其他的刷新組件。
第三方插件pull_to_refresh;這就是我們找的替代刷新組件。整體感覺還是easy_refresh好點,不過有幾個優點還是值得嘗試:
(1)能解決NestedScrollView不會自動消失的問題;
(2)雨滴組件WaterDropMaterialHeader()效果不錯;
(3)上拉加載有開關,這個在無數據的時候比較有用。
(4)使用上,比easy_refresh要簡單一些。
組件封裝
原來我們是直接使用easy_refresh的,不過經歷過這次換組件的事件之后,考慮在外面封裝一層,這樣的話到時候再換第三方組件的時候會方便一點。
RefreshController一般都要的,這里作為一個必要屬性。
header和footer視圖可以提供一個默認的,所以給了個可選屬性。
onRefresh和onLoad一般也是必須的,不過這里給了個可選屬性。少部分情況,上拉和下拉功能會關閉。
這里的child其實也是必須的,一般是ListView等可滑動組件。不過為了方便,我們直接固定成CustomScrollView,然后給出參數slivers,這樣可以避免SmartRefresher和child跨組件封裝的錯誤。
加載過程的視圖如果和SmartRefresher進行替換,會導致CustomScrollView的位置丟失,每次load之后都回到頭部。所以這里采用了一個stack,覆蓋在CustomScrollView上面的方式來避免這個問題。
加載視圖提供一個默認的,并且提供一個開關變量isLoading來控制。如果業務方不想要加載視圖,只要保持isLoading參數為false就好了。
網絡錯誤和空視圖也給一個默認的,同時提供變量進行控制。這個采用替換的形式,刷新回到頂部是合理的。
class RefreshWidget extends StatelessWidget {
const RefreshWidget(
{super.key,
required this.controller,
this.header,
this.footer,
this.onRefresh,
this.onLoad,
this.enableLoad = true,
this.enableRefresh = true,
this.isLoading = false,
this.loadingWidget,
this.isEmpty = false,
this.emptyWidget,
this.isNetworkError = false,
this.networkErrorWidget,
required this.slivers});
final pull.RefreshController controller;
final Widget? header;
final Widget? footer;
final void Function()? onRefresh;
final void Function()? onLoad;
final bool enableLoad;
final bool enableRefresh;
final bool isLoading;
final Widget? loadingWidget;
final bool isEmpty;
final Widget? emptyWidget;
final bool isNetworkError;
final Widget? networkErrorWidget;
final List<Widget> slivers;
@override
Widget build(BuildContext context) {
/// 網絡錯誤,空數據
List<Widget> actualSlivers;
if (isNetworkError) {
actualSlivers = [SliverToBoxAdapter(child: networkErrorWidget ?? defaultNetworkErrorWidget())];
} else if (isEmpty) {
actualSlivers = [SliverToBoxAdapter(child: emptyWidget ?? defaultEmptyWidget())];
} else {
actualSlivers = slivers;
}
return Stack(
children: [
pull.RefreshConfiguration(
child: pull.SmartRefresher(
enablePullUp: enableLoad,
enablePullDown: enableRefresh,
header: header ?? classicHeader(),
footer: footer ?? classicFooter(),
controller: controller,
onRefresh: onRefresh,
onLoading: onLoad,
child: CustomScrollView(
slivers: actualSlivers,
),
),
),
/// 加載中視圖
Visibility(
visible: isLoading,
child: Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: Container(
child: loadingWidget ?? defaultLoadingWidget(),
),
),
),
],
);
}
}
刷新邏輯封裝
“下拉刷新”和“上拉加載”功能存在較多的重復性,所以有必要進行封裝。
本來想封裝成mixin的方式,但是更新界面很麻煩,所以還是回到了基類的方式。
后端接口要求的分頁參數可以寫在這里,比如page,size等;
數據的話就用一個數組dataList表示,我們直接用Map,所以可以不考慮類型,如果用Model,估計得用上泛型,稍微麻煩一些。
網絡訪問用一個Future表示。
“下拉刷新”和“上拉加載”,也需要一個bool類型的isRefresh表示。
“上拉加載”的無更多數據,也需要一個bool類型的hasMore表示。如何判斷是否有更多數據?可以有兩種方法。一種是后端返回增加字段,比如total表示總數。還有一個方式就是判斷本次網絡訪問的數量,與page進行比較。在這里,我們就用了后面這種簡單方式。
refreshController,這里默認給一個,方便復用。這個是必須的,用來控制刷新控件狀態。
如果是刷新,不論是否成功,都清數組。這樣可以避免tab切換時的異常情況。
TabBar可以用,TabView盡量不要用。如果多個實例,就需要多個refreshController切換,比較碼放
/// 刷新相關方法
class BaseRefreshLogic extends GetxController {
int page = 1;
int size = 20;
List dataList = [];
bool isRefresh = true;
bool hasMore = true;
pull.RefreshController refreshController = pull.RefreshController(initialRefresh: false);
Future<BaseResponse>? loadFuture;
bool isLoading = false;
bool isNetworkError = false;
bool enableLoad = true;
bool enableRefresh = true;
/// 是否為空
bool get isEmpty => dataList.isEmpty;
/// 接口的loading控制
bool showLoading = false;
/// 重寫這個方法,構造網絡請求
void buildFuture() {}
/// 用于刷新組件
void onRefresh({bool showLoading = false}) async {
this.showLoading = showLoading;
page = 1;
isRefresh = true;
isNetworkError = false;
hasMore = true;
refreshController.resetNoData();
fetchData();
}
/// 用于刷新組件
void onLoad({bool showLoading = false}) async {
this.showLoading = showLoading;
page++;
isRefresh = false;
isNetworkError = false;
fetchData();
}
/// 重寫這個方法,業務方自行處理刷新邏輯
void fetchData() async {
/// 參數檢查
buildFuture();
if (loadFuture == null) {
LogUtil.e("loadFuture為空");
return;
}
isLoading = true;
update();
LogUtil.e("開始網絡請求");
BaseResponse baseResponse = await loadFuture!;
isLoading = false;
update();
LogUtil.e("網絡請求結束");
/// 不論成功失敗,刷新都要清空數據
if (isRefresh) {
dataList.clear();
}
if (baseResponse.isSuccess) {
List list = baseResponse.data ?? baseResponse.list ?? [];
if (list.isNotEmpty) {
dataList.addAll(list);
}
if (list.length < size) {
hasMore = false;
} else {
hasMore = true;
}
}
onFinish(baseResponse.isSuccess);
}
void onFinish(bool isSuccess) {
isNetworkError = !isSuccess;
if (isSuccess) {
if (isRefresh) {
refreshController.refreshCompleted();
} else {
if (hasMore) {
refreshController.loadComplete();
} else {
refreshController.loadNoData();
}
}
} else {
if (isRefresh) {
refreshController.refreshFailed();
} else {
refreshController.loadFailed();
}
}
update();
LogUtil.e("onFinish數據更新;isSuccess:$isSuccess");
}
}
如何使用?
- 用基類需要手動修改GetxController為自己寫的基類。
class TradeRecordLogic extends BaseRefreshLogic
- 重寫網絡Future,進行具體的網絡訪問
/// 下拉
@override
void buildFuture() {
Map params = {
"pageNum": page,
"pageSize": size,
};
int? type = currentBusinessType["type"];
if (type != null) {
if (type != balanceLogic.allItem["type"]) {
params["businessType"] = type;
}
}
loadFuture = PayApis.transactionRecordList(params, showLoading: false);
}
- 在界面文件中,由于基類中中已經提供默認的空視圖和網絡訪問視圖,變量也設置好了,基本上都差不多。
body: Container(
color: Colors.transparent,
margin: EdgeInsets.symmetric(horizontal: 15.r, vertical: 15.r),
child: RefreshWidget(
controller: logic.refreshController,
onRefresh: logic.onRefresh,
onLoad: logic.onLoad,
enableLoad: logic.enableLoad,
isLoading: logic.isLoading,
isEmpty: logic.isEmpty,
isNetworkError: logic.isNetworkError,
child: CustomScrollView(
slivers: [
listWidget(),
],
),
),
),
- 在TabView中使用,需要多個refreshController切換,比較麻煩,金陵避免這樣用:
final RefreshController allController = RefreshController(initialRefresh: false);
final RefreshController payController = RefreshController(initialRefresh: false);
final RefreshController extraController = RefreshController(initialRefresh: false);
final RefreshController shippedController = RefreshController(initialRefresh: false);
final RefreshController receivedController = RefreshController(initialRefresh: false);
void getRefreshController() {
switch (tabType) {
case ParcelTabType.all:
refreshController = allController;
break;
case ParcelTabType.pay:
refreshController = payController;
break;
case ParcelTabType.extra:
refreshController = extraController;
break;
case ParcelTabType.shipped:
refreshController = shippedController;
break;
case ParcelTabType.received:
refreshController = receivedController;
break;
}
}
在NestedScrollView中使用
NestedScrollView需要在外層,NestedScrollView不能作為SmartRefresher的child,不然的話,上拉和下拉功能都失效。
封裝后的RefreshWidget,可以當做是一個CustomScrollView,就自然而然地放到了NestedScrollView的body參數中,可以正常進行上拉下拉。這樣能夠解決easy refresh不能復原的問題。
@override
Widget build(BuildContext context) {
return GetBuilder<PackageLogic>(
builder: (_) {
return GBScaffold(
title: "my_package".tr,
actions: [
filterWidget(),
],
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
searchWidget(),
tabWidget(),
];
},
body: contentWidget(),
),
);
},
);
}
Widget contentWidget() {
return TabBarView(
physics: NeverScrollableScrollPhysics(),
controller: logic.tabController,
children: [
refreshWidget(logic.allController),
refreshWidget(logic.payController),
refreshWidget(logic.extraController),
refreshWidget(logic.shippedController),
refreshWidget(logic.receivedController),
],
);
}
Widget refreshWidget(RefreshController controller) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 15.r),
child: RefreshWidget(
controller: controller,
onRefresh: logic.onRefresh,
onLoad: logic.onLoad,
isEmpty: logic.isEmpty,
emptyWidget: emptyWidget(),
slivers: [
goListWidget(),
],
),
);
}
- 重新代碼不多,就像下面那樣
/// 上拉下拉
@override
void buildFuture() {
getRefreshController();
Map parameters = {
"pageNum": page,
"pageSize": size,
};
/// tab切換
if (tabType.parcelStatusValue != null) {
parameters["parcelStatus"] = tabType.parcelStatusValue;
}
if (tabType.abnormalStatusValue != null) {
parameters["abnormalStatus"] = tabType.abnormalStatusValue;
}
loadFuture = StorageApis.queryParcelList(parameters, showLoading: showLoading);
}
- 這樣一套格式代碼下來,只要關注網絡訪問的Future就可以了。