強大的dio封裝,可能滿足你的一切需要

艾維碼發表於2020-06-14

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);
}
複製程式碼

程式碼地址

相關文章