接著上一篇文章:基於MVVM架構封裝Flutter基礎庫
一、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()物件配置的方法);