前不久看到 艾維碼 大佬的dio封裝,經過摸索,改吧改吧,使用的不錯。對於之前 艾維碼 大佬文章中一些已經失效的做了修正
為什麼一定要封裝一手?
token攔截,錯誤攔截,統一錯誤處理,統一快取,統一資訊封裝(錯誤,正確)
Cookie???滾犢子
不管cookie,再見
全域性初始化,傳入引數
dio初始化,傳入baseUrl, connectTimeout, receiveTimeout,options,header 攔截器等。dio初始化的時候允許我們傳入的一些配置
dio初始化的配置
這裡說下,之前 艾維碼 大佬的帖子中的options,最新版的dio已經使用requestOptions, 之前的merge,現在使用copyWith。詳情向下看
如果要白嫖完整的方案
可以參考使用這套方案開發的 flutter + getx 仿開眼視訊app,有star的大佬可以賞點star。
專案地址 github地址
百度雲 提取碼:xi6b
藍奏雲 提取碼:7agj
初始化
這裡說下攔截器,可以在初始化的時候傳入,也可以手寫傳入,例如我這裡定義了四個攔截器,第一個用於全域性request時候給請求投加上context-type:json。第二個是全域性錯誤處理攔截器,下面的內容會介紹攔截器部分。
cache攔截器,全域性處理介面快取資料,retry重試攔截器(我暫時沒怎麼用)
class Http {
static final Http _instance = Http._internal();
// 單例模式使用Http類,
factory Http() => _instance;
static late final Dio dio;
CancelToken _cancelToken = new CancelToken();
Http._internal() {
// BaseOptions、Options、RequestOptions 都可以配置引數,優先順序別依次遞增,且可以根據優先順序別覆蓋引數
BaseOptions options = new BaseOptions();
dio = Dio(options);
// 新增request攔截器
dio.interceptors.add(RequestInterceptor());
// 新增error攔截器
dio.interceptors.add(ErrorInterceptor());
// // 新增cache攔截器
dio.interceptors.add(NetCacheInterceptor());
// // 新增retry攔截器
dio.interceptors.add(
RetryOnConnectionChangeInterceptor(
requestRetrier: DioConnectivityRequestRetrier(
dio: dio,
connectivity: Connectivity(),
),
),
);
// 在除錯模式下需要抓包除錯,所以我們使用代理,並禁用HTTPS證書校驗
// if (PROXY_ENABLE) {
// (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
// (client) {
// client.findProxy = (uri) {
// return "PROXY $PROXY_IP:$PROXY_PORT";
// };
// //代理工具會提供一個抓包的自簽名證書,會通不過證書校驗,所以我們禁用證書校驗
// client.badCertificateCallback =
// (X509Certificate cert, String host, int port) => true;
// };
// }
}
///初始化公共屬性
///
/// [baseUrl] 地址字首
/// [connectTimeout] 連線超時趕時間
/// [receiveTimeout] 接收超時趕時間
/// [interceptors] 基礎攔截器
void init({
String? baseUrl,
int connectTimeout = 1500,
int receiveTimeout = 1500,
Map<String, String>? headers,
List<Interceptor>? interceptors,
}) {
dio.options = dio.options.copyWith(
baseUrl: baseUrl,
connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout,
headers: headers ?? const {},
);
// 在初始化http類的時候,可以傳入攔截器
if (interceptors != null && interceptors.isNotEmpty) {
dio.interceptors..addAll(interceptors);
}
}
// 關閉dio
void cancelRequests({required CancelToken token}) {
_cancelToken.cancel("cancelled");
}
// 新增認證
// 讀取本地配置
Map<String, dynamic>? getAuthorizationHeader() {
Map<String, dynamic>? headers;
// 從getx或者sputils中獲取
// String accessToken = Global.accessToken;
String accessToken = "";
if (accessToken != null) {
headers = {
'Authorization': 'Bearer $accessToken',
};
}
return headers;
}
Future get(
String path, {
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
bool refresh = false,
bool noCache = !CACHE_ENABLE,
String? cacheKey,
bool cacheDisk = false,
}) async {
Options requestOptions = options ?? Options();
requestOptions = requestOptions.copyWith(
extra: {
"refresh": refresh,
"noCache": noCache,
"cacheKey": cacheKey,
"cacheDisk": cacheDisk,
},
);
Map<String, dynamic>? _authorization = getAuthorizationHeader();
if (_authorization != null) {
requestOptions = requestOptions.copyWith(headers: _authorization);
}
Response response;
response = await dio.get(
path,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken,
);
return response.data;
}
Future post(
String path, {
Map<String, dynamic>? params,
data,
Options? options,
CancelToken? cancelToken,
}) async {
Options requestOptions = options ?? Options();
Map<String, dynamic>? _authorization = getAuthorizationHeader();
if (_authorization != null) {
requestOptions = requestOptions.copyWith(headers: _authorization);
}
var response = await dio.post(
path,
data: data,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken,
);
return response.data;
}
Future put(
String path, {
data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
Options requestOptions = options ?? Options();
Map<String, dynamic>? _authorization = getAuthorizationHeader();
if (_authorization != null) {
requestOptions = requestOptions.copyWith(headers: _authorization);
}
var response = await dio.put(
path,
data: data,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken,
);
return response.data;
}
Future patch(
String path, {
data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
Options requestOptions = options ?? Options();
Map<String, dynamic>? _authorization = getAuthorizationHeader();
if (_authorization != null) {
requestOptions = requestOptions.copyWith(headers: _authorization);
}
var response = await dio.patch(
path,
data: data,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken,
);
return response.data;
}
Future delete(
String path, {
data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
Options requestOptions = options ?? Options();
Map<String, dynamic>? _authorization = getAuthorizationHeader();
if (_authorization != null) {
requestOptions = requestOptions.copyWith(headers: _authorization);
}
var response = await dio.delete(
path,
data: data,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken,
);
return response.data;
}
}
dio攔截器
下面我們來看下攔截器,下面是一個處理處理攔截器案例
// 這裡是一個我單獨寫得soket錯誤例項,因為dio預設生成的是不允許修改message內容的,我只能自定義一個使用
class MyDioSocketException extends SocketException {
late String message;
MyDioSocketException(
message, {
osError,
address,
port,
}) : super(
message,
osError: osError,
address: address,
port: port,
);
}
/// 錯誤處理攔截器
class ErrorInterceptor extends Interceptor {
// 是否有網
Future<bool> isConnected() async {
var connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}
@override
Future<void> onError(DioError err, ErrorInterceptorHandler errCb) async {
// 自定義一個socket例項,因為dio原生的例項,message屬於是隻讀的
// 這裡是我單獨加的,因為預設的dio err例項,的幾種型別,缺少無網路情況下的錯誤提示資訊
// 這裡我手動做處理,來加工一手,效果,看下面的圖片,你就知道
if (err.error is SocketException) {
err.error = MyDioSocketException(
err.message,
osError: err.error?.osError,
address: err.error?.address,
port: err.error?.port,
);
}
// dio預設的錯誤例項,如果是沒有網路,只能得到一個未知錯誤,無法精準的得知是否是無網路的情況
if (err.type == DioErrorType.other) {
bool isConnectNetWork = await isConnected();
if (!isConnectNetWork && err.error is MyDioSocketException) {
err.error.message = "當前網路不可用,請檢查您的網路";
}
}
// error統一處理
AppException appException = AppException.create(err);
// 錯誤提示
debugPrint('DioError===: ${appException.toString()}');
err.error = appException;
return super.onError(err, errCb);
}
}
以上的程式碼可以看到,ErrorInterceptor類繼承自Interceptor,可以重新onRequest 、onResponse、onError,三個狀態,最後return super.onError將err例項傳遞給超類。
統一的錯誤資訊包裝處理
試想一下,如果你的專案,有十幾種狀態碼,每種也也都需要吧code碼轉換成文字資訊,因為有時候你需要給使用者提示。例如: 連線超時,請求失敗,網路錯誤,等等。下面是統一的錯誤處理
AppException.dart
import 'package:dio/dio.dart';
/// 自定義異常
class AppException implements Exception {
final String _message;
final int _code;
AppException(
this._code,
this._message,
);
String toString() {
return "$_code$_message";
}
String getMessage() {
return _message;
}
factory AppException.create(DioError error) {
switch (error.type) {
case DioErrorType.cancel:
{
return BadRequestException(-1, "請求取消");
}
case DioErrorType.connectTimeout:
{
return BadRequestException(-1, "連線超時");
}
case DioErrorType.sendTimeout:
{
return BadRequestException(-1, "請求超時");
}
case DioErrorType.receiveTimeout:
{
return BadRequestException(-1, "響應超時");
}
case DioErrorType.response:
{
try {
int? errCode = error.response!.statusCode;
// String errMsg = error.response.statusMessage;
// return ErrorEntity(code: errCode, message: errMsg);
switch (errCode) {
case 400:
{
return BadRequestException(errCode!, "請求語法錯誤");
}
case 401:
{
return UnauthorisedException(errCode!, "沒有許可權");
}
case 403:
{
return UnauthorisedException(errCode!, "伺服器拒絕執行");
}
case 404:
{
return UnauthorisedException(errCode!, "無法連線伺服器");
}
case 405:
{
return UnauthorisedException(errCode!, "請求方法被禁止");
}
case 500:
{
return UnauthorisedException(errCode!, "伺服器內部錯誤");
}
case 502:
{
return UnauthorisedException(errCode!, "無效的請求");
}
case 503:
{
return UnauthorisedException(errCode!, "伺服器掛了");
}
case 505:
{
return UnauthorisedException(errCode!, "不支援HTTP協議請求");
}
default:
{
// return ErrorEntity(code: errCode, message: "未知錯誤");
return AppException(errCode!, error.response!.statusMessage!);
}
}
} on Exception catch (_) {
return AppException(-1, "未知錯誤");
}
}
default:
{
return AppException(-1, error.error.message);
}
}
}
}
/// 請求錯誤
class BadRequestException extends AppException {
BadRequestException(int code, String message) : super(code, message);
}
/// 未認證異常
class UnauthorisedException extends AppException {
UnauthorisedException(int code, String message) : super(code, message);
}
使用的時候這樣使用,
Future<ApiResponse<Feed>> getFeedData(url) async {
try {
dynamic response = await HttpUtils.get(url);
// print(response);
Feed data = Feed.fromJson(response);
return ApiResponse.completed(data);
} on DioError catch (e) {
print(e);
// 這裡看這裡,如果是有錯誤的請求下,使用AppException對錯誤物件進行處理
// 處理過後,你就可以比如彈個toast,提示給使用者等,
// 彈窗toast等在下面的方法中呼叫
return ApiResponse.error(e.error);
}
}
Future<void> _refresh() async {
ApiResponse<Feed> swiperResponse = await getFeedData(initPageUrl);
// 加工過後,我們可以獲得兩個狀態,Status.COMPLETED 和 Status.ERROR
// 看這裡
if (swiperResponse.status == Status.COMPLETED) {
// 成功的程式碼,想幹嘛幹嘛
}else if (swiperResponse.status == Status.ERROR) {
// 失敗的程式碼,可以給個toast,提示給使用者
// 例如我在這裡提示使用者
// 使用 exception!.getMessage(); 獲得錯誤物件的文字資訊,是我們攔截器處理過後的提示文字,非英文,拿到這,提示給使用者不香嗎???看下面的圖片效果
String errMsg = swiperResponse.exception!.getMessage();
publicToast(errMsg);
}
}
這裡的提示就是自定義err攔截器中增加的程式碼,對於dio不能夠得到是否無網路的補充
磁碟快取資料,攔截器
磁碟快取介面資料,首先我們要封裝一個SpUtil類,
sputils.dart
class SpUtil {
SpUtil._internal();
static final SpUtil _instance = SpUtil._internal();
factory SpUtil() {
return _instance;
}
SharedPreferences? prefs;
Future<void> init() async {
prefs = await SharedPreferences.getInstance();
}
Future<bool> setJSON(String key, dynamic jsonVal) {
String jsonString = jsonEncode(jsonVal);
return prefs!.setString(key, jsonString);
}
dynamic getJSON(String key) {
String? jsonString = prefs?.getString(key);
return jsonString == null ? null : jsonDecode(jsonString);
}
Future<bool> setBool(String key, bool val) {
return prefs!.setBool(key, val);
}
bool? getBool(String key) {
return prefs!.getBool(key);
}
Future<bool> remove(String key) {
return prefs!.remove(key);
}
}
快取攔截器
const int CACHE_MAXAGE = 86400000;
const int CACHE_MAXCOUNT = 1000;
const bool CACHE_ENABLE = false;
class CacheObject {
CacheObject(this.response)
: timeStamp = DateTime.now().millisecondsSinceEpoch;
Response response;
int timeStamp;
@override
bool operator ==(other) {
return response.hashCode == other.hashCode;
}
@override
int get hashCode => response.realUri.hashCode;
}
class NetCacheInterceptor extends Interceptor {
// 為確保迭代器順序和物件插入時間一致順序一致,我們使用LinkedHashMap
var cache = LinkedHashMap<String, CacheObject>();
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler requestCb,
) async {
if (!CACHE_ENABLE) {
return super.onRequest(options, requestCb);
}
// refresh標記是否是重新整理快取
bool refresh = options.extra["refresh"] == true;
// 是否磁碟快取
bool cacheDisk = options.extra["cacheDisk"] == true;
// 如果重新整理,先刪除相關快取
if (refresh) {
// 刪除uri相同的記憶體快取
delete(options.uri.toString());
// 刪除磁碟快取
if (cacheDisk) {
await SpUtil().remove(options.uri.toString());
}
return;
}
// get 請求,開啟快取
if (options.extra["noCache"] != true &&
options.method.toLowerCase() == 'get') {
String key = options.extra["cacheKey"] ?? options.uri.toString();
// 策略 1 記憶體快取優先,2 然後才是磁碟快取
// 1 記憶體快取
var ob = cache[key];
if (ob != null) {
//若快取未過期,則返回快取內容
if ((DateTime.now().millisecondsSinceEpoch - ob.timeStamp) / 1000 <
CACHE_MAXAGE) {
return;
} else {
//若已過期則刪除快取,繼續向伺服器請求
cache.remove(key);
}
}
// 2 磁碟快取
if (cacheDisk) {
var cacheData = SpUtil().getJSON(key);
if (cacheData != null) {
return;
}
}
}
return super.onRequest(options, requestCb);
}
@override
void onResponse(
Response response, ResponseInterceptorHandler responseCb) async {
// 如果啟用快取,將返回結果儲存到快取
if (CACHE_ENABLE) {
await _saveCache(response);
}
return super.onResponse(response, responseCb);
}
Future<void> _saveCache(Response object) async {
RequestOptions options = object.requestOptions;
// 只快取 get 的請求
if (options.extra["noCache"] != true &&
options.method.toLowerCase() == "get") {
// 策略:記憶體、磁碟都寫快取
// 快取key
String key = options.extra["cacheKey"] ?? options.uri.toString();
// 磁碟快取
if (options.extra["cacheDisk"] == true) {
await SpUtil().setJSON(key, object.data);
}
// 記憶體快取
// 如果快取數量超過最大數量限制,則先移除最早的一條記錄
if (cache.length == CACHE_MAXCOUNT) {
cache.remove(cache[cache.keys.first]);
}
cache[key] = CacheObject(object);
}
}
void delete(String key) {
cache.remove(key);
}
}
開始封裝
class HttpUtils {
static void init({
required String baseUrl,
int connectTimeout = 1500,
int receiveTimeout = 1500,
List<Interceptor>? interceptors,
}) {
Http().init(
baseUrl: baseUrl,
connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout,
interceptors: interceptors,
);
}
static void cancelRequests({required CancelToken token}) {
Http().cancelRequests(token: token);
}
static Future get(
String path, {
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
bool refresh = false,
bool noCache = !CACHE_ENABLE,
String? cacheKey,
bool cacheDisk = false,
}) async {
return await Http().get(
path,
params: params,
options: options,
cancelToken: cancelToken,
refresh: refresh,
noCache: noCache,
cacheKey: cacheKey,
);
}
static Future post(
String path, {
data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
return await Http().post(
path,
data: data,
params: params,
options: options,
cancelToken: cancelToken,
);
}
static Future put(
String path, {
data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
return await Http().put(
path,
data: data,
params: params,
options: options,
cancelToken: cancelToken,
);
}
static Future patch(
String path, {
data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
return await Http().patch(
path,
data: data,
params: params,
options: options,
cancelToken: cancelToken,
);
}
static Future delete(
String path, {
data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
return await Http().delete(
path,
data: data,
params: params,
options: options,
cancelToken: cancelToken,
);
}
}
注入,初始化
main。dart。這裡參考我個人的使用例子
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// debugPaintSizeEnabled = true;
await initStore();
runApp(MyApp());
}
Future<void> initStore() async {
// 初始化本地儲存類
await SpUtil().init();
// 初始化request類
HttpUtils.init(
baseUrl: Api.baseUrl,
);
// 歷史記錄,全域性 getx全域性注入,
await Get.putAsync(() => HistoryService().init());
print("全域性注入");
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: PageRoutes.INIT_ROUTER,
getPages: PageRoutes.routes,
);
}
}
使用封裝好得例子
// 這裡定義一個函式,返回的是future 《apiResponse》,可以得到status的狀態
Future<ApiResponse<Feed>> getFeedData(url) async {
try {
dynamic response = await HttpUtils.get(url);
// print(response);
Feed data = Feed.fromJson(response);
return ApiResponse.completed(data);
} on DioError catch (e) {
print(e);
return ApiResponse.error(e.error);
}
}
Future<void> _refresh() async {
ApiResponse<Feed> swiperResponse = await getFeedData(initPageUrl);
if (!mounted) {
return;
}
// 使用 status.COMPLETED 判斷是否成功
if (swiperResponse.status == Status.COMPLETED) {
setState(() {
nextPageUrl = swiperResponse.data!.nextPageUrl;
_swiperList = [];
_swiperList.addAll(swiperResponse.data!.issueList![0]!.itemList!);
_itemList = [];
});
// 拉取新的,列表
await _loading();
// 使用 status.ERROR 判斷是否失敗
} else if (swiperResponse.status == Status.ERROR) {
setState(() {
stateCode = 2;
});
// 錯誤的話,我們可以呼叫 getMessage() 獲取錯誤資訊。提示給使用者(漢化後的友好提示語)
String errMsg = swiperResponse.exception!.getMessage();
publicToast(errMsg);
print("發生錯誤,位置home bottomBar1 swiper, url: ${initPageUrl}");
print(swiperResponse.exception);
}
}