Flutter Dio封装实践(3行代码实现http请求)
一、http组件XApi包含的特性
1、http请求时传递泛型直接解析为对应对象或List;
2、配合MVVM组件(BaseViewModel和BaseView)使用,实现了通用加载中、加载错误页,空白页以及正常显示页UI和逻辑;
3、http加载时dialog自动显示隐藏逻辑,dialog UI可自定义;
4、http加载时手动关闭dialog会自动取消请求,也可以禁止在请求过程中关闭弹窗;
5、请求失败时自动显示错误Toast,可以自定义样式或不显示;
6、token失效时回调指定方法;
7、可以自定义加载提示文本;
二、初始化http组件
在应用启动时调用BaseLibPlugin.init()初始化httpConfig:
BaseLibPlugin.init(
...省略其它代码
///http配置
httpConfig: HttpConfigImpl(),
...
);
参考下面代码,根据业务实现IHttpConfig接口:
///@date: 2021/2/26 14:01
///@author: lixu
///@description: 全局http相关配置
///当前配置在[XApi]类中被调用
class HttpConfigImpl implements IHttpConfig {
String _tag = 'HttpConfigImpl';
///配置默认值:http请求时是否显示加载dialog
@override
bool isShowLoading() {
return true;
}
///配置默认值:http加载提示文本
@override
String configLoadingText() {
return 'loading...';
}
///配置默认值:加载中能否通过关闭加载弹窗取消请求
@override
bool isCancelableDialog() {
return false;
}
///配置默认值:请求失败时是否自动显示toast提示错误
@override
bool isShowFailToast() {
return true;
}
///配置默认值:请求前是否校验网络连接
///true:如果无网络,直接返回错误
@override
bool isCheckNetwork() {
return true;
}
///配置通用的http请求选项[BaseOptions]
///优先级最低,优先取[XApi]#[request]方法中配置的method和option
@override
BaseOptions configBaseOptions() {
BaseOptions options = BaseOptions(
baseUrl: HttpUrls.httpHost,
connectTimeout: HttpConst.httpTimeOut,
receiveTimeout: HttpConst.httpTimeOut,
sendTimeout: HttpConst.httpTimeOut,
contentType: XApi.contentTypeJson,
method: XApi.methodPost,
responseType: ResponseType.json,
);
return options;
}
///返回http成功的响应码
@override
String configHttpResultSuccessCode() {
return HttpConst.httpResultSuccess.toString();
}
///配置https
@override
bool configHttps(X509Certificate cert, String host, int port) {
///TODO 根据业务做校验
///true:忽略证书校验
return true;
}
///添加http拦截器
///拦截器队列的执行顺序是FIFO,先添加的拦截器先执行
@override
List<Interceptor> configInterceptors() {
List<Interceptor> interceptors = [];
interceptors.add(HeaderInterceptor());
///TODO 可以添加拦截器实现http缓存逻辑,或其它功能
return interceptors;
}
///是否自动添加[LogInterceptors]默认日志拦截器,打印http请求响应相关的日志
@override
bool configLogEnable() {
return true;
}
///每个http请求前回调该方法获取baseUrl
///优先级高于[IHttpConfig]#[configBaseOptions]方法配置的baseUrl
///[url] 当前正在请求的接口url
///return: 返回null使用[IHttpConfig]#[configBaseOptions]方法配置的baseUrl
@override
String getHttpHost(String url) {
return HttpUrls.httpHost;
}
///http请求失败时会回调该方法,判断是否是token失效导致的错误
///[errorBean] 请求失败对象
@override
bool isHttpRespTokenError(HttpErrorBean errorBean) {
///通过code判断是否是token失效了
return HttpConst.sysTokenError.toString() == errorBean.code
|| HttpConst.sysTokenExpired.toString() == errorBean.code;
}
///token失效回调该方法
///[errorBean] 请求失败对象
@override
void onTokenErrorCallback(HttpErrorBean errorBean) {
ToastUtils.show("Token 失效:${errorBean.toString()}", isShowLong: true);
///TODO 实现token失效的业务逻辑
}
///将http响应的json解析成对象
///[url] 当前请求url
///[jsonData] http响应完整json
///[isRespListData] 响应数据是否是List格式
@override
HttpResultBean<T> parseJsonToObject<T>(String url, Map<String, dynamic>
jsonData, bool isRespListData) {
///TODO 通过传递的泛型,解析成对象
return HttpJsonUtils.parseJsonToObject<T>(url, jsonData, isRespListData);
}
///http请求显示加载框
///[XApi]#[request]方法isShowLoading字段为true时,会回调该方法
///[url] 当前请求url
///[tag] 当前请求对应的tag,唯一
///[cancelToken] 用于加载框关闭时取消http请求
///[loadingText] 加载提示提示文本
///[isCancelableDialog] 请求过程中能否关闭加载框,默认false
@override
void showLoading(String url, int tag, CancelToken cancelToken, String loadingText,
bool isCancelableDialog) { LogUtils.i(_tag, 'showLoading tag:$tag loadingText:$loadingText');
///显示http加载dialog:isShowLoading为true时,会回调该方法
BaseLibPlugin.oneContext.showDialog(
barrierDismissible: false,
barrierColor: Colors.transparent,
isBackButtonDismissible: isCancelableDialog,
builder: (_) {
///TODO 可以参考HttpLoadingDialog类自定义dialog样式
return HttpLoadingDialog(loadingText);
},
onClickBackButtonDismissCallback: () {
///请求过程中关闭加载框时取消请求
if (isCancelableDialog) {
XApi().cancel(cancelToken);
}
},
);
}
///http请求完成,关闭加载框
///[XApi]#[request]方法isShowLoading字段为true时,会回调该方法
///[url] 当前请求url
///[tag]当前请求对应的tag,唯一
@override
void hideLoading(String url, int tag, bool isCancelled) {
if (!isCancelled && BaseLibPlugin.oneContext.hasDialogVisible) {
BaseLibPlugin.oneContext.popDialog();
}
}
三、XApi 主要方法说明
///@date: 2021/2/25 14:25
///@author: lixu
///@description: 网络请求基类(单例)
class XApi {
...省略其它代码
///单例
factory XApi() {
if (_instance == null) {
_instance = XApi._internal();
}
return _instance;
}
///私有构造方法
XApi._internal() {
///基于通用BaseOptions创建单例Dio对象
_dio = Dio(_httpConfig.configBaseOptions());
///拦截器队列的执行顺序是FIFO
///添加自定义拦截器
if (_httpConfig.configInterceptors() != null
&& _httpConfig.configInterceptors().isNotEmpty) {
_dio.interceptors.addAll(_httpConfig.configInterceptors());
}
///添加默认的日志拦截器
if (_httpConfig.configLogEnable()) {
_dio.interceptors.add(LogsInterceptors());
}
(_dio.httpClientAdapter as DefaultHttpClientAdapter).
onHttpClientCreate = (client) {
client.badCertificateCallback =
(X509Certificate cert, String host, int port) {
///true:忽略证书校验
return _httpConfig.configHttps(
cert,host,port) ?? true;
};
};
}
///发起请求,响应data为单个对象
Future request<T>(String url,
{Map<String, dynamic> params,
Map<String, dynamic> header,
String method,
RequestOptions option,
bool isShowLoading,
String loadingText,
bool isCancelableDialog,
bool isShowFailToast,
bool isCheckNetwork,
CancelToken cancelToken,
Function(T) onSuccess,
Function(HttpErrorBean) onError,
Function() onComplete}
) {
调用_commonRequest方法
}
///发起请求,响应data为List
Future requestList<T>(String url,
{Map<String, dynamic> params,
Map<String, dynamic> header,
String method,
RequestOptions option,
bool isShowLoading,
String loadingText,
bool isCancelableDialog,
bool isShowFailToast,
bool isCheckNetwork,
CancelToken cancelToken,
Function(List<T>) onSuccess,
Function(HttpErrorBean) onError,
Function() onComplete}
){
调用_commonRequest方法
}
///通用http请求
///[url] 请求url
///[params] 请求参数,可为空
///[isResultList] 返回的data是否是List类型
///[header] 请求头
///[method] 请求方法,优先级最高
///[option] 针对当前请求的配置选项,优先级次高
///[isShowLoading] 是否显示加载弹窗
///[loadingText] 加载提示
///[isCancelableDialog] 加载中能否关闭加载弹窗
///[isShowFailToast] 请求失败时是否自动显示toast提示错误
///[isCheckNetwork] 请求前是否校验网络连接
///[onSuccessListCallback] 请求List成功回调
///[onSuccessObjCallback] 请求单个对象成功回调
///[onErrorCallback] 请求失败回调
///[onComplete] 请求完成回调,在onSuccess或onError方法后面调用
Future _commonRequest<T>(String url, bool isResultList,
{Map<String, dynamic> params,
Map<String, dynamic> header,
String method,
RequestOptions option,
bool isShowLoading,
String loadingText,
bool isCancelableDialog,
bool isShowFailToast,
bool isCheckNetwork,
CancelToken cancelToken,
Function(List<T>) onSuccessListCallback,
Function(T) onSuccessObjCallback,
Function(HttpErrorBean) onErrorCallback,
Function() onCompleteCallback}) async {
///设置默认值
isShowLoading ??= _httpConfig.isShowLoading();
loadingText ??= _httpConfig.configLoadingText();
isCancelableDialog ??= _httpConfig.isCancelableDialog();
isShowFailToast ??= _httpConfig.isShowFailToast();
isCheckNetwork ??= _httpConfig.isCheckNetwork();
if (isCheckNetwork) {
///判断网络连接
ConnectivityResult connResult =
await Connectivity().checkConnectivity();
if (connResult != null && connResult == ConnectivityResult.none) {
return _onRespErrorCallback(
isShowFailToast,
onErrorCallback,
HttpErrorBean(code: HttpCode.networkError?.toString(),
message: '无网络连接,请检查网络设置'),
);
}
}
option ??= RequestOptions();
///添加baseUrl
///baseUrl优先级:形参option.baseUrl>_httpConfig.getBaseUrl>_httpConfig.
configBaseOptions
if (!url.startsWith(Constants.httpStartWith)
&& option.baseUrl == null) {
String baseUrl = _httpConfig.getBaseUrl(url);
if (baseUrl != null && baseUrl.isNotEmpty) {
option.baseUrl = baseUrl;
}
}
params ??= {};
///添加CancelToken,用于取消请求
cancelToken ??= CancelToken();
_cancelTokenList ??= [];
_cancelTokenList.add(cancelToken);
///处理请求头
if (header != null) {
option.headers ??= {};
option.headers.addAll(header);
}
///设置请求方法
if (method != null) {
option.method = method;
} else {
option.method ??= methodPost;
}
///显示加载框
if (isShowLoading) {
///只是封装了显示dialog的逻辑,具体的dialog UI实现交给调用者处理
_httpConfig.showLoading(url,
cancelToken.hashCode,
cancelToken,
loadingText,
isCancelableDialog
);
}
try {
Response response;
if (methodGet == option.method) {
///get请求
response = await _dio.get(url,
queryParameters: params,
options: option,
cancelToken: cancelToken
);
} else if (methodPost == option.method) {
///默认post请求
response = await _dio.post(url,
data: params,
options: option,
cancelToken: cancelToken
);
} else {
///其他请求方式
response = await _dio.request(url,
data: params,
options: option,
cancelToken: cancelToken
);
}
///json解析
HttpResultBean<T> resultBean = _parseJsonToObject<T>(url,
response,
isResultList
);
if (resultBean.isSuccess()) {
///请求成功
_onRespSuccessCallback(
resultBean,
onSuccessObjCallback,
onSuccessListCallback
);
} else {
///请求失败
_onRespErrorCallback(
isShowFailToast,
onErrorCallback,
resultBean.obtainErrorBean()
);
}
} on DioError catch (e) {
_onRespErrorCallback(
isShowFailToast,
onErrorCallback,
_createErrorEntity(e)
);
} catch (exception) {
LogUtils.e(_tag, ' 异常:${exception?.toString()}');
_onRespErrorCallback(
isShowFailToast,
onErrorCallback,
HttpErrorBean(
code: HttpCode.fail,
message: exception?.toString() ?? '网络异常,请稍后再试'
),
);
} finally {
///请求完成,隐藏加载框
if (isShowLoading) {
_httpConfig.hideLoading(url,
cancelToken.hashCode,
cancelToken.isCancelled
);
}
///请求完成移除cancelToken
if (cancelToken != null
&& _cancelTokenList != null
&& _cancelTokenList.contains(cancelToken)) {
_cancelTokenList.remove(cancelToken);
}
///请求完成回调
onCompleteCallback?.call();
}
}
///http响应json解析为对象
///[response] http 响应的对象
///[isRespListData] http响应的数据是否是List数据结构
HttpResultBean<T> _parseJsonToObject<T>(String url, Response response,
bool isRespListData) {
if (response == null || response.data == null) {
HttpResultBean<T> resultBean = HttpResultBean();
resultBean.isRespListData = isRespListData;
resultBean.code = HttpCode.unKnowError;
resultBean.message = 'response is null';
return resultBean;
}
//TODO 将json解析逻辑交给调用者处理
HttpResultBean<T> resultBean = _httpConfig.parseJsonToObject<T>(
url, response.data, isRespListData);
resultBean.json = response.data;
return resultBean;
}
...省略其它代码
}
四、http拦截器使用
http请求时经常需要添加通用的请求参数和请求头,或是对响应数据进行预处理,通过添加拦截器来实现:
1、定义拦截器
///@date: 2021/2/25 14:22
///@author: lixu
///@description: http拦截器,添加请求头和通用参数
class HeaderInterceptor extends InterceptorsWrapper {
String _tag = 'HeaderInterceptor';
@override
onRequest(RequestOptions options) async {
LogUtils.d(_tag, 'onRequest()');
///通用参数
var params = {
'lang': 'zhcn',
'centerId': loginInfo.getCenterId(),
};
///通过请求参数生成sign,添加到请求头
String sign;
if (XApi.methodGet == options.method) {
options.queryParameters =
(Map<String, dynamic>.from(
options.queryParameters ?? {}))..addAll(params);
sign = await HttpUtils.getSignEncode(
options.queryParameters,
HttpConst.serverKey
);
} else {
options.data =
(Map<String, dynamic>.from(
options.data ?? {}))..addAll(params);
sign = await HttpUtils.getSignEncode(
options.data,
HttpConst.serverKey
);
}
///添加请求头
Map<String, String> headerMap =
loginInfo.token != null ? {'token': loginInfo.token} : {};
headerMap.putIfAbsent('sign', () => sign);
options.headers ??= {};
options.headers.addAll(headerMap);
return options;
}
@override
Future onResponse(Response response) async {
///从登录接口中获取token和用户信息
///TODO 也可以直接在登录响应的对象中获取用户信息和token,此处只是演示http拦截器功能
if (response.request.path.contains(HttpUrls.loginUrl)) {
HttpResultBean<LoginResultBean> resultBean =
HttpJsonUtils.parseJsonToObject(HttpUrls.loginUrl,
response.data,
false
);
if (resultBean.isSuccess()) {
loginInfo.token = resultBean.data?.token;
loginInfo.userBean = resultBean.data?.user;
}
LogUtils.i(_tag, '登录获取的token:${loginInfo.token}');
}
return response;
}
}
2、添加拦截器
///@date: 2021/2/26 14:01
///@author: lixu
///@description: http相关配置
///当前配置在[XApi]类中被调用
class HttpConfigImpl implements IHttpConfig {
String _tag = 'HttpConfigImpl';
...省略其它代码
///添加http拦截器
///拦截器队列的执行顺序是FIFO,先添加的拦截器先执行
@override
List<Interceptor> configInterceptors() {
List<Interceptor> interceptors = [];
interceptors.add(HeaderInterceptor());
return interceptors;
}
...省略其它代码
}
五、http请求获取单个对象
参考LoginViewModel中onLogin()方法:
///调用登录接口,获取单个对象
Future<bool> onLogin() async {
Map<String, dynamic> map = {
'account': '15015001500',
'pass': '123qwe',
'appType': 'PATIENT',
'device': 'ANDROID',
'push': '13065ffa4e22e63efd2',
};
///http请求方法全部字段功能说明
RequestOptions option = RequestOptions();
option.method = XApi.methodPost;
option.baseUrl = HttpUrls.httpHost;
await XApi().request<LoginResultBean>(
HttpUrls.loginUrl,
params: map,
//优先级最高
method: XApi.methodPost,
//针对当前请求的配置选项,优先级次高
option: option,
cancelToken: loginCancelToken,
//请求前检测网络连接是否正常,如果连接异常,直接返回错误
isCheckNetwork: true,
//显示加载dialog
isShowLoading: true,
//加载dialog显示的提示文本
loadingText: '正在登录...',
//请求失败时显示toast提示
isShowFailToast: true,
//请求过程中可以关闭加载弹窗(请求过程中关闭dialog时自动取消请求)
isCancelableDialog: true,
onSuccess: (LoginResultBean bean) {
_loginResultBean = bean;
ToastUtils.show('登录成功');
},
onError: (HttpErrorBean errorBean) {
_loginResultBean = null;
LogUtils.e(getTag(), '登录失败');
},
onComplete: () {
LogUtils.i(getTag(), '登录完成');
},
);
return _loginResultBean?.token != null && _loginResultBean?.user != null;
}
六、http请求获取List对象
参考LoginViewModel中getUserList()方法
///获取用户列表List
Future<List<UserDetailBean>> getUserList() async {
var params = {
'userId': loginInfo.userBean?.userId,
'token': loginInfo.token,
};
List<UserDetailBean> userList;
await api.requestList<UserDetailBean>(HttpUrls.userListUrl,
params: params,
onSuccess: (List<UserDetailBean> list) {
userList = list;
},
onError: (HttpErrorBean errorBean) {
LogUtils.e(getTag(), '获取用户列表失败');
},
);
return userList;
}
七、最简单的http请求
参考LoginViewModel中simplestHttpDemo()方法:
Future simplestHttpDemo(BuildContext context) async {
var params = {
'userId': loginInfo.userBean?.userId,
'token': loginInfo.token,
};
///最简单的http请求,可以满足大多数场景
await api.requestList<UserDetailBean>(HttpUrls.userListUrl,
params: params,
onSuccess: (List<UserDetailBean> list) {
ToastUtils.show('获取用户列表成功,用户数:${list?.length}');
},
);
}
上面最简单的http请求包含的功能:
1、自动显示、隐藏加载dialog,请求过程中dialog不能关闭 ;
2、请求失败自动显示Toast提示错误信息 ;
3、使用默认请求方法(IHttpConfig#configBaseOptions()对象配置的方法);
仅3行代码就能满足大多数http请求的场景,Demo地址
转载自:https://juejin.cn/post/6939334755598991397