前沿
页面通用Loading组件是一个App必不可少的基础功能,之前只开发过Android原生的页面Loading,这次就按原生的逻辑再开发一个Flutter的Widget,对其进行封装复用。
我们先看下效果:
原理
状态
一个通用的页面加载Loading组件应该具备以下几种状态:
IDLE 初始化
Idle状态,此时的组件还只是初始化
LOADING 加载中
Loading状态,一般在网络请求或者耗时加载数据时调用,通用显示的是一个progress或者自定义的帧动画
LOADING_SUCCESS
LoadingSuccess加载成功,一般在网络请求成功后调用,并将需要展示的页面展示出来
LOADING_SUCCESS_BUT_EMPTY
页面加载成功但是没有数据,这种情况一般是发起列表数据请求但是没有数据,通常我们会展示一个空数据的页面来提醒用户
NETWORK_BLOCKED
网络错误,一般是由于网络异常、网络请求连接超时导致。此时我们需要展示一个网络错误的页面,并且带有重试按钮,让用户重新发起请求
ERROR
通常是接口错误,这种情况下我们会根据接口返回的错误码或者错误文本提示用户,并且也有重试按钮
/// 状态枚举
enum LoadingStatus {
idle, // 初始化
loading, // 加载中
loading_suc, // 加载成功
loading_suc_but_empty, // 加载成功但是数据为空
network_blocked, // 网络加载错误
error, // 加载错误
}
点击事件回调
当网络异常或者接口报错时,会显示错误页面,并且提供重试按钮,让用户点击重新请求。基于这个需求,我们还需要提供点击重试后的事件回调让业务可以处理重新请求。
/// 定义点击事件
typedef OnTapCallback = Function(LoadingView widget);
提示文案
提供提示文案的自定义,方便业务根据自己的需求展示特定的提示文案
代码实现
根据上面的原理来实现对应的代码
- 构造方法
/// 构造方法
LoadingView({
Key key,
@required this.child, // 需要加载的Widget
@required this.todoAfterError, // 错误点击重试
@required this.todoAfterNetworkBlocked, // 网络错误点击重试
this.networkBlockedDesc = "网络连接超时,请检查你的网络环境",
this.errorDesc = "加载失败",
this.loadingStatus = LoadingStatus.idle,
}) : super(key: key);
- 根据不同的Loading状态展示对应的Widget
- 其中idle、success状态直接展示需要加载的Widget(这里也可以使用渐变动画进行切换过度)
///根据不同状态展示不同Widget
Widget _buildBody() {
switch (widget.loadingStatus) {
case LoadingStatus.idle:
return widget.child;
case LoadingStatus.loading:
return _buildLoadingView();
case LoadingStatus.loading_suc:
return widget.child;
case LoadingStatus.loading_suc_but_empty:
return _buildLoadingSucButEmptyView();
case LoadingStatus.error:
return _buildErrorView();
case LoadingStatus.network_blocked:
return _buildNetworkBlockedView();
}
return widget.child;
}
- buildLoadingView,这里简单用了系统的CircularProgressIndicator,也可以自己显示帧动画
/// 加载中 View
Widget _buildLoadingView() {
return Container(
width: double.maxFinite,
height: double.maxFinite,
child: Center(
child: SizedBox(
height: 22.w,
width: 22.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryBgBlue),
),
),
),
);
}
- 其他提示页面,这里做了一个统一的封装
/// 编译通用页面
Container _buildGeneralTapView({
String url = "images/icon_network_blocked.png",
String desc,
@required Function onTap,
}) {
return Container(
color: AppColors.primaryBgWhite,
width: double.maxFinite,
height: double.maxFinite,
child: Center(
child: SizedBox(
height: 250.h,
child: Column(
children: [
Image.asset(url,
width: 140.w, height: 99.h),
SizedBox(
height: 40.h,
),
Text(
desc,
style: AppText.gray50Text12,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(
height: 30.h,
),
if (onTap != null)
BorderRedBtnWidget(
content: "重新加载",
onClick: onTap,
padding: 40.w,
),
],
),
),
),
);
}
/// 加载成功但数据为空 View
Widget _buildLoadingSucButEmptyView() {
return _buildGeneralTapView(
url: "images/icon_empty.png",
desc: "暂无数据",
onTap: null,
);
}
/// 网络加载错误页面
Widget _buildNetworkBlockedView() {
return _buildGeneralTapView(
url: "images/icon_network_blocked.png",
desc: widget.networkBlockedDesc,
onTap: () {
widget.todoAfterNetworkBlocked(widget);
});
}
/// 加载错误页面
Widget _buildErrorView() {
return _buildGeneralTapView(
url: "images/icon_error.png",
desc: widget.errorDesc,
onTap: () {
widget.todoAfterError(widget);
});
}
使用
Widget _buildBody() {
var loadingView = LoadingView(
loadingStatus: LoadingStatus.loading,
child: _buildContent(),
todoAfterNetworkBlocked: (LoadingView widget) {
// 网络错误,点击重试
widget.updateStatus(LoadingStatus.loading);
Future.delayed(Duration(milliseconds: 1000), () {
widget.updateStatus(LoadingStatus.error);
});
},
todoAfterError: (LoadingView widget) {
// 接口错误,点击重试
widget.updateStatus(LoadingStatus.loading);
Future.delayed(Duration(milliseconds: 1000), () {
// widget.updateStatus(LoadingStatus.loading_suc);
widget.updateStatus(LoadingStatus.loading_suc_but_empty);
});
},
);
Future.delayed(Duration(milliseconds: 1000), (){
loadingView.updateStatus(LoadingStatus.network_blocked);
});
return loadingView;
}
总结
至此已经完成了对整个Loading组件的封装,代码已上传Github