dio是一個強大的Dart Http請求庫,支援Restful API、FormData、攔截器、請求取消、Cookie管理、檔案上傳/下載、超時、自定義介面卡等..
基本使用
新增依賴
dependencies:
dio: ^3.x.x // 請使用pub上3.0.0分支的最新版本
複製程式碼
發起一個 GET 請求 :
Response response;
Dio dio = Dio();
response = await dio.get("/test?id=12&name=wendu")
print(response.data.toString());
// 請求引數也可以通過物件傳遞,上面的程式碼等同於:
response = await dio.get("/test", queryParameters: {"id": 12, "name": "wendu"});
print(response.data.toString());
複製程式碼
發起一個 POST 請求:
response = await dio.post("/test", data: {"id": 12, "name": "wendu"});
複製程式碼
發起多個併發請求:
response = await Future.wait([dio.post("/info"), dio.get("/token")]);
複製程式碼
下載檔案:
response = await dio.download("https://www.google.com/", "./xx.html");
複製程式碼
傳送 FormData:
FormData formData = FormData.from({
"name": "wendux",
"age": 25,
});
response = await dio.post("/info", data: formData);
複製程式碼
通過FormData上傳多個檔案:
FormData.fromMap({
"name": "wendux",
"age": 25,
"file": await MultipartFile.fromFile("./text.txt",filename: "upload.txt"),
"files": [
await MultipartFile.fromFile("./text1.txt", filename: "text1.txt"),
await MultipartFile.fromFile("./text2.txt", filename: "text2.txt"),
]
});
response = await dio.post("/info", data: formData);
複製程式碼
封裝Dio
為什麼要封裝 dio
上面看了dio的api,非常靈活和簡單,那麼為什麼還要封裝呢?因為我們開發需要統一的配置場景。比如:
- 全域性token驗證
- 自定義攔截器
- 快取處理
- 統一封裝業務錯誤邏輯
- 代理配置
- 重試機制
- log輸出
代理配置:
flutter抓包需要配置dio代理,所以我們實現一個代理配置,proxy.dart:
// 是否啟用代理
const PROXY_ENABLE = false;
/// 代理服務IP
// const PROXY_IP = '192.168.1.105';
const PROXY_IP = '172.16.43.74';
/// 代理服務埠
const PROXY_PORT = 8866;
複製程式碼
錯誤處理:
一般錯誤分為網路錯誤、請求錯誤、認證錯誤、伺服器錯誤,所以實現統一的錯誤處理,認證錯誤需要登入等認證,所以單獨一個型別,請求錯誤也單獨設定一個型別,方便我們定位錯誤,app_exceptions.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";
}
factory AppException.create(DioError error) {
switch (error.type) {
case DioErrorType.CANCEL:
{
return BadRequestException(-1, "請求取消");
}
break;
case DioErrorType.CONNECT_TIMEOUT:
{
return BadRequestException(-1, "連線超時");
}
break;
case DioErrorType.SEND_TIMEOUT:
{
return BadRequestException(-1, "請求超時");
}
break;
case DioErrorType.RECEIVE_TIMEOUT:
{
return BadRequestException(-1, "響應超時");
}
break;
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, "請求語法錯誤");
}
break;
case 401:
{
return UnauthorisedException(errCode, "沒有許可權");
}
break;
case 403:
{
return UnauthorisedException(errCode, "伺服器拒絕執行");
}
break;
case 404:
{
return UnauthorisedException(errCode, "無法連線伺服器");
}
break;
case 405:
{
return UnauthorisedException(errCode, "請求方法被禁止");
}
break;
case 500:
{
return UnauthorisedException(errCode, "伺服器內部錯誤");
}
break;
case 502:
{
return UnauthorisedException(errCode, "無效的請求");
}
break;
case 503:
{
return UnauthorisedException(errCode, "伺服器掛了");
}
break;
case 505:
{
return UnauthorisedException(errCode, "不支援HTTP協議請求");
}
break;
default:
{
// return ErrorEntity(code: errCode, message: "未知錯誤");
return AppException(errCode, error.response.statusMessage);
}
}
} on Exception catch (_) {
return AppException(-1, "未知錯誤");
}
}
break;
default:
{
return AppException(-1, 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);
}
複製程式碼
Error攔截器:
有了上面的異常型別,我們要把DioError變成自己定義的異常:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'app_exceptions.dart';
/// 錯誤處理攔截器
class ErrorInterceptor extends Interceptor {
@override
Future onError(DioError err) {
// error統一處理
AppException appException = AppException.create(err);
// 錯誤提示
debugPrint('DioError===: ${appException.toString()}');
err.error = appException;
return super.onError(err);
}
}
複製程式碼
http單例操作類:
利用單利和配置,實現一個dio的封裝。
class Http {
///超時時間
static const int CONNECT_TIMEOUT = 30000;
static const int RECEIVE_TIMEOUT = 30000;
static Http _instance = Http._internal();
factory Http() => _instance;
Dio dio;
CancelToken _cancelToken = new CancelToken();
Http._internal() {
if (dio == null) {
// BaseOptions、Options、RequestOptions 都可以配置引數,優先順序別依次遞增,且可以根據優先順序別覆蓋引數
BaseOptions options = new BaseOptions(
connectTimeout: CONNECT_TIMEOUT,
// 響應流上前後兩次接受到資料的間隔,單位為毫秒。
receiveTimeout: RECEIVE_TIMEOUT,
// Http請求頭.
headers: {},
);
dio = new Dio(options);
// 新增error攔截器
dio.interceptors
.add(ErrorInterceptor());
// 在除錯模式下需要抓包除錯,所以我們使用代理,並禁用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,
int receiveTimeout,
List<Interceptor> interceptors}) {
dio.options = dio.options.merge(
baseUrl: baseUrl,
connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout,
);
if (interceptors != null && interceptors.isNotEmpty) {
dio.interceptors..addAll(interceptors);
}
}
}
複製程式碼
增加取消功能:
/*
* 取消請求
*
* 同一個cancel token 可以用於多個請求,當一個cancel token取消時,所有使用該cancel token的請求都會被取消。
* 所以引數可選
*/
void cancelRequests({CancelToken token}) {
token ?? _cancelToken.cancel("cancelled");
}
複製程式碼
增加認證header:
/// 讀取本地配置
Map<String, dynamic> getAuthorizationHeader() {
var headers;
String accessToken = Global.accessToken;
if (accessToken != null) {
headers = {
'Authorization': 'Bearer $accessToken',
};
}
return headers;
}
複製程式碼
新增cookie和cache:
新增cookie管理:
cookie_jar: ^1.0.1
dio_cookie_manager: ^1.0.0
複製程式碼
// Cookie管理
CookieJar cookieJar = CookieJar();
dio.interceptors.add(CookieManager(cookieJar));
// 加記憶體快取
dio.interceptors.add(NetCache());
複製程式碼
利用sp做磁碟快取:
shared_preferences: ^0.5.6+3
複製程式碼
編寫cache類,net_cache.dart:
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
onRequest(RequestOptions options) async {
if (!CACHE_ENABLE) return options;
// 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 options;
}
// 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 cache[key].response;
} else {
//若已過期則刪除快取,繼續向伺服器請求
cache.remove(key);
}
}
// 2 磁碟快取
if (cacheDisk) {
var cacheData = SpUtil().getJSON(key);
if (cacheData != null) {
return Response(
statusCode: 200,
data: cacheData,
);
}
}
}
}
@override
onError(DioError err) async {
// 錯誤狀態不快取
}
@override
onResponse(Response response) async {
// 如果啟用快取,將返回結果儲存到快取
if (CACHE_ENABLE) {
await _saveCache(response);
}
}
Future<void> _saveCache(Response object) async {
RequestOptions options = object.request;
// 只快取 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);
}
}
複製程式碼
dio加入快取:
// 加記憶體快取
dio.interceptors.add(NetCacheInterceptor());
複製程式碼
重試攔截器:
在網路斷開的時候,監聽網路,等重連的時候重試:
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'connectivity_request_retrier.dart';
class RetryOnConnectionChangeInterceptor extends Interceptor {
final DioConnectivityRequestRetrier requestRetrier;
RetryOnConnectionChangeInterceptor({
@required this.requestRetrier,
});
@override
Future onError(DioError err) async {
if (_shouldRetry(err)) {
try {
return requestRetrier.scheduleRequestRetry(err.request);
} catch (e) {
return e;
}
}
return err;
}
bool _shouldRetry(DioError err) {
return err.type == DioErrorType.DEFAULT &&
err.error != null &&
err.error is SocketException;
}
}
複製程式碼
import 'dart:async';
import 'package:connectivity/connectivity.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
class DioConnectivityRequestRetrier {
final Dio dio;
final Connectivity connectivity;
DioConnectivityRequestRetrier({
@required this.dio,
@required this.connectivity,
});
Future<Response> scheduleRequestRetry(RequestOptions requestOptions) async {
StreamSubscription streamSubscription;
final responseCompleter = Completer<Response>();
streamSubscription = connectivity.onConnectivityChanged.listen(
(connectivityResult) {
if (connectivityResult != ConnectivityResult.none) {
streamSubscription.cancel();
responseCompleter.complete(
dio.request(
requestOptions.path,
cancelToken: requestOptions.cancelToken,
data: requestOptions.data,
onReceiveProgress: requestOptions.onReceiveProgress,
onSendProgress: requestOptions.onSendProgress,
queryParameters: requestOptions.queryParameters,
options: requestOptions,
),
);
}
},
);
return responseCompleter.future;
}
}
複製程式碼
新增重試攔截器:
if (Global.retryEnable) {
dio.interceptors.add(
RetryOnConnectionChangeInterceptor(
requestRetrier: DioConnectivityRequestRetrier(
dio: Dio(),
connectivity: Connectivity(),
),
),
);
}
複製程式碼
restful請求:
/// restful get 操作
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.merge(extra: {
"refresh": refresh,
"noCache": noCache,
"cacheKey": cacheKey,
"cacheDisk": cacheDisk,
});
Map<String, dynamic> _authorization = getAuthorizationHeader();
if (_authorization != null) {
requestOptions = requestOptions.merge(headers: _authorization);
}
Response response;
response = await dio.get(path,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken);
return response.data;
}
/// restful post 操作
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.merge(headers: _authorization);
}
var response = await dio.post(path,
data: data,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken);
return response.data;
}
/// restful put 操作
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.merge(headers: _authorization);
}
var response = await dio.put(path,
data: data,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken);
return response.data;
}
/// restful patch 操作
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.merge(headers: _authorization);
}
var response = await dio.patch(path,
data: data,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken);
return response.data;
}
/// restful delete 操作
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.merge(headers: _authorization);
}
var response = await dio.delete(path,
data: data,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken);
return response.data;
}
/// restful post form 表單提交操作
Future postForm(
String path, {
Map<String, dynamic> params,
Options options,
CancelToken cancelToken,
}) async {
Options requestOptions = options ?? Options();
Map<String, dynamic> _authorization = getAuthorizationHeader();
if (_authorization != null) {
requestOptions = requestOptions.merge(headers: _authorization);
}
var response = await dio.post(path,
data: FormData.fromMap(params),
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken);
return response.data;
}
複製程式碼
通常我們喜歡靜態方法呼叫,所以新建一個類:
import 'package:dio/dio.dart';
import 'package:flutter_dio/http/http.dart';
import 'app_exceptions.dart';
import 'cache.dart';
class HttpUtils {
static void init(
{String baseUrl,
int connectTimeout,
int receiveTimeout,
List<Interceptor> interceptors}) {
Http().init(
baseUrl: baseUrl,
connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout,
interceptors: interceptors);
}
static void setHeaders(Map<String, dynamic> map) {
Http().setHeaders(map);
}
static void cancelRequests({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,
);
}
static Future postForm(
String path, {
Map<String, dynamic> params,
Options options,
CancelToken cancelToken,
}) async {
return await Http().postForm(
path,
params: params,
options: options,
cancelToken: cancelToken,
);
}
}
複製程式碼
使用:
初始化:
void main() {
HttpUtils.init(
baseUrl: "https://gan.io/",
);
runApp(MyApp());
}
複製程式碼
在model裡寫介面請求:
static Future<ApiResponse<CategoryEntity>> getCategories() async {
try {
final response = await HttpUtils.get(categories);
var data = CategoryEntity.fromJson(response);
return ApiResponse.completed(data);
} on DioError catch (e) {
return ApiResponse.error(e.error);
}
}
複製程式碼
呼叫:
void getCategories() async {
ApiResponse<CategoryEntity> entity = await GanRepository.getCategories();
print(entity.data.data.length);
}
複製程式碼