一步一步教你封裝最新版Dio

許多掘金朋友在上一篇留言,說要封裝下最新版倔监,所以這篇把封裝思路寫下,大家可以自己封裝菌仁。有好的想法也可以去github提request浩习,也感謝WingCH的貢獻(xiàn)

分析需求

為什么要封裝?

  • 全局token驗(yàn)證

  • 自定義攔截器

  • 緩存處理

  • 統(tǒng)一封裝業(yè)務(wù)錯誤邏輯

  • 代理配置

  • 重試機(jī)制

  • log輸出

  • 自定義解析济丘,數(shù)據(jù)脫殼

要初始化哪些配置谱秽?

  • 域名
  • 代理地址
  • cookie本地緩存地址
  • 超時時間
  • 自定義攔截器

定義一個配置信息類去初始化這些配置:

// dio 配置項(xiàng)
class HttpConfig {
  final String? baseUrl;
  final String? proxy;
  final String? cookiesPath;
  final List<Interceptor>? interceptors;
  final int connectTimeout;
  final int sendTimeout;
  final int receiveTimeout;

  HttpConfig({
    this.baseUrl,
    this.proxy,
    this.cookiesPath,
    this.interceptors,
    this.connectTimeout = Duration.millisecondsPerMinute,
    this.sendTimeout = Duration.millisecondsPerMinute,
    this.receiveTimeout = Duration.millisecondsPerMinute,
  });

  // static DioConfig of() => Get.find<DioConfig>();
}

請求差異化有哪些配置?

  • 解析策略

    許多公司接口規(guī)范經(jīng)歷過變更摹迷,有多個返回類型疟赊,那么就需要針對不同的數(shù)據(jù)類型,做不同的解析峡碉。

    比如舊版本:

    // 舊版本
    {
        "code": 1,
        "data": {},
        "state": true
    }
    // 新版本
    {
        "code": 1,
        "data": {
          "data": {},
          "hasmore":false
        },
        "message": “success”
    }
    

    要做到脫殼近哟,拿到解析后的data,就需要兩種解析策略鲫寄。所以需要根據(jù)不同接口動態(tài)配置解析策略吉执。

  • path

  • 參數(shù)

  • cancelToken

  • dio 的常用參數(shù)

    Dio 的請求參數(shù)已經(jīng)很全面的包括了分析出的配置參數(shù),只需要另添加一個解析策略即可地来。

    遵守 SOLID 原則定義一個抽象解析策略:

    /// Response 解析
    abstract class HttpTransformer {
      HttpResponse parse(Response response);
    }
    
    

    根據(jù)實(shí)際需求默認(rèn)實(shí)現(xiàn):

    class DefaultHttpTransformer extends HttpTransformer {
    // 假設(shè)接口返回類型
    //   {
    //     "code": 100,
    //     "data": {},
    //     "message": "success"
    // }
      @override
      HttpResponse parse(Response response) {
        // if (response.data["code"] == 100) {
        //   return HttpResponse.success(response.data["data"]);
        // } else {
        // return HttpResponse.failure(errorMsg:response.data["message"],errorCode: response.data["code"]);
        // }
        return HttpResponse.success(response.data["data"]);
      }
    
      /// 單例對象
      static DefaultHttpTransformer _instance = DefaultHttpTransformer._internal();
    
      /// 內(nèi)部構(gòu)造方法戳玫,可避免外部暴露構(gòu)造函數(shù),進(jìn)行實(shí)例化
      DefaultHttpTransformer._internal();
    
      /// 工廠構(gòu)造方法未斑,這里使用命名構(gòu)造函數(shù)方式進(jìn)行聲明
      factory DefaultHttpTransformer.getInstance() => _instance;
    }
    

    單例模式是為了避免多次創(chuàng)建實(shí)例咕宿。方便下一步使用。

異常處理

異常大體分為以下幾種:

  • 網(wǎng)絡(luò)異常
  • 客戶端請求異常
  • 服務(wù)端異常

客戶端異常又可拆分兩種常見的異常:請求參數(shù)或路徑錯誤,鑒權(quán)失敗/token失效

異常歸檔后創(chuàng)建異常:

class HttpException implements Exception {
  final String? _message;

  String get message => _message ?? this.runtimeType.toString();

  final int? _code;

  int get code => _code ?? -1;

  HttpException([this._message, this._code]);

  String toString() {
    return "code:$code--message=$message";
  }
}

/// 客戶端請求錯誤
class BadRequestException extends HttpException {
  BadRequestException({String? message, int? code}) : super(message, code);
}
/// 服務(wù)端響應(yīng)錯誤
class BadServiceException extends HttpException {
  BadServiceException({String? message, int? code}) : super(message, code);
}



class UnknownException extends HttpException {
  UnknownException([String? message]) : super(message);
}

class CancelException extends HttpException {
  CancelException([String? message]) : super(message);
}

class NetworkException extends HttpException {
  NetworkException({String? message, int? code}) : super(message, code);
}

/// 401
class UnauthorisedException extends HttpException {
  UnauthorisedException({String? message, int? code = 401}) : super(message);
}

class BadResponseException extends HttpException {
  dynamic? data;

  BadResponseException([this.data]) : super();
}

返回數(shù)據(jù)類型

返回的數(shù)據(jù)類型府阀,需要有成功或是失敗的標(biāo)識缆镣,還需要脫殼后的數(shù)據(jù),如果失敗了试浙,也需要失敗的信息费就,定義幾個工廠方法方便創(chuàng)建實(shí)例:

class HttpResponse {
  late bool ok;
  dynamic? data;
  HttpException? error;

  HttpResponse._internal({this.ok = false});

  HttpResponse.success(this.data) {
    this.ok = true;
  }

  HttpResponse.failure({String? errorMsg, int? errorCode}) {
    this.error = BadRequestException(message: errorMsg, code: errorCode);
    this.ok = false;
  }

  HttpResponse.failureFormResponse({dynamic? data}) {
    this.error = BadResponseException(data);
    this.ok = false;
  }

  HttpResponse.failureFromError([HttpException? error]) {
    this.error = error ?? UnknownException();
    this.ok = false;
  }
}

開始封裝

配置 Dio

Dio 配置組裝,需要我們定義一個初始化類川队,用于把請求的初始化配置添加進(jìn)去力细。一般可以定義一個單例類,init方法里去初始化一個 Dio 固额,也可以采用實(shí)現(xiàn) Dio 的方式:

class AppDio with DioMixin implements Dio {
  AppDio({BaseOptions? options, HttpConfig? dioConfig}) {
    options ??= BaseOptions(
      baseUrl: dioConfig?.baseUrl ?? "",
      contentType: 'application/json',
      connectTimeout: dioConfig?.connectTimeout,
      sendTimeout: dioConfig?.sendTimeout,
      receiveTimeout: dioConfig?.receiveTimeout,
    );
    this.options = options;

    // DioCacheManager
    final cacheOptions = CacheOptions(
      // A default store is required for interceptor.
      store: MemCacheStore(),
      // Optional. Returns a cached response on error but for statuses 401 & 403.
      hitCacheOnErrorExcept: [401, 403],
      // Optional. Overrides any HTTP directive to delete entry past this duration.
      maxStale: const Duration(days: 7),
    );
    interceptors.add(DioCacheInterceptor(options: cacheOptions));
    // Cookie管理
    if (dioConfig?.cookiesPath?.isNotEmpty ?? false) {
      interceptors.add(CookieManager(
          PersistCookieJar(storage: FileStorage(dioConfig!.cookiesPath))));
    }

    if (kDebugMode) {
      interceptors.add(LogInterceptor(
          responseBody: true,
          error: true,
          requestHeader: false,
          responseHeader: false,
          request: false,
          requestBody: true));
    }
    if (dioConfig?.interceptors?.isNotEmpty ?? false) {
      interceptors.addAll(interceptors);
    }
    httpClientAdapter = DefaultHttpClientAdapter();
    if (dioConfig?.proxy?.isNotEmpty ?? false) {
      setProxy(dioConfig!.proxy!);
    }
  }

  setProxy(String proxy) {
    (httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
        (client) {
      // config the http client
      client.findProxy = (uri) {
        // proxy all request to localhost:8888
        return "PROXY $proxy";
      };
      // you can also create a HttpClient to dio
      // return HttpClient();
    };
  }
}


Restful請求

采用 Restful 標(biāo)準(zhǔn)眠蚂,創(chuàng)建對應(yīng)的請求方法:

class HttpClient {
  late AppDio _dio;

  HttpClient({BaseOptions? options, HttpConfig? dioConfig})
      : _dio = AppDio(options: options, dioConfig: dioConfig);

  Future<HttpResponse> get(String uri,
      {Map<String, dynamic>? queryParameters,
      Options? options,
      CancelToken? cancelToken,
      ProgressCallback? onReceiveProgress,
      HttpTransformer? httpTransformer}) async {
    try {
      var response = await _dio.get(
        uri,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
        onReceiveProgress: onReceiveProgress,
      );
      return handleResponse(response, httpTransformer: httpTransformer);
    } on Exception catch (e) {
      return handleException(e);
    }
  }

  Future<HttpResponse> post(String uri,
      {data,
      Map<String, dynamic>? queryParameters,
      Options? options,
      CancelToken? cancelToken,
      ProgressCallback? onSendProgress,
      ProgressCallback? onReceiveProgress,
      HttpTransformer? httpTransformer}) async {
    try {
      var response = await _dio.post(
        uri,
        data: data,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
        onSendProgress: onSendProgress,
        onReceiveProgress: onReceiveProgress,
      );
      return handleResponse(response, httpTransformer: httpTransformer);
    } on Exception catch (e) {
      return handleException(e);
    }
  }

  Future<HttpResponse> patch(String uri,
      {data,
      Map<String, dynamic>? queryParameters,
      Options? options,
      CancelToken? cancelToken,
      ProgressCallback? onSendProgress,
      ProgressCallback? onReceiveProgress,
      HttpTransformer? httpTransformer}) async {
    try {
      var response = await _dio.patch(
        uri,
        data: data,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
        onSendProgress: onSendProgress,
        onReceiveProgress: onReceiveProgress,
      );
      return handleResponse(response, httpTransformer: httpTransformer);
    } on Exception catch (e) {
      return handleException(e);
    }
  }

  Future<HttpResponse> delete(String uri,
      {data,
      Map<String, dynamic>? queryParameters,
      Options? options,
      CancelToken? cancelToken,
      HttpTransformer? httpTransformer}) async {
    try {
      var response = await _dio.delete(
        uri,
        data: data,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
      );
      return handleResponse(response, httpTransformer: httpTransformer);
    } on Exception catch (e) {
      return handleException(e);
    }
  }

  Future<HttpResponse> put(String uri,
      {data,
      Map<String, dynamic>? queryParameters,
      Options? options,
      CancelToken? cancelToken,
      HttpTransformer? httpTransformer}) async {
    try {
      var response = await _dio.put(
        uri,
        data: data,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
      );
      return handleResponse(response, httpTransformer: httpTransformer);
    } on Exception catch (e) {
      return handleException(e);
    }
  }

  Future<Response> download(String urlPath, savePath,
      {ProgressCallback? onReceiveProgress,
      Map<String, dynamic>? queryParameters,
      CancelToken? cancelToken,
      bool deleteOnError = true,
      String lengthHeader = Headers.contentLengthHeader,
      data,
      Options? options,
      HttpTransformer? httpTransformer}) async {
    try {
      var response = await _dio.download(
        urlPath,
        savePath,
        onReceiveProgress: onReceiveProgress,
        queryParameters: queryParameters,
        cancelToken: cancelToken,
        deleteOnError: deleteOnError,
        lengthHeader: lengthHeader,
        data: data,
        options: data,
      );
      return response;
    } catch (e) {
      throw e;
    }
  }
}

響應(yīng)解析

得到請求數(shù)據(jù)后,解析為定義的通用返回數(shù)據(jù)類型斗躏,需要首先判斷是否取得返回值逝慧,然后判斷網(wǎng)絡(luò)請求成功,網(wǎng)絡(luò)請求成功之后啄糙,采取判斷是否接口返回期望的數(shù)據(jù)笛臣,還是因?yàn)檎埱髤?shù)錯誤或者服務(wù)器錯誤返回了錯誤信息。如果錯誤了隧饼,把錯誤信息格式化為定義的異常:

HttpResponse handleResponse(Response? response,
    {HttpTransformer? httpTransformer}) {
  httpTransformer ??= DefaultHttpTransformer.getInstance();

  // 返回值異常
  if (response == null) {
    return HttpResponse.failureFromError();
  }

  // token失效
  if (_isTokenTimeout(response.statusCode)) {
    return HttpResponse.failureFromError(
        UnauthorisedException(message: "沒有權(quán)限", code: response.statusCode));
  }
  // 接口調(diào)用成功
  if (_isRequestSuccess(response.statusCode)) {
    return httpTransformer.parse(response);
  } else {
    // 接口調(diào)用失敗
    return HttpResponse.failure(
        errorMsg: response.statusMessage, errorCode: response.statusCode);
  }
}

HttpResponse handleException(Exception exception) {
  var parseException = _parseException(exception);
  return HttpResponse.failureFromError(parseException);
}

/// 鑒權(quán)失敗
bool _isTokenTimeout(int? code) {
  return code == 401;
}

/// 請求成功
bool _isRequestSuccess(int? statusCode) {
  return (statusCode != null && statusCode >= 200 && statusCode < 300);
}

HttpException _parseException(Exception error) {
  if (error is DioError) {
    switch (error.type) {
      case DioErrorType.connectTimeout:
      case DioErrorType.receiveTimeout:
      case DioErrorType.sendTimeout:
        return NetworkException(message: error.error.message);
      case DioErrorType.cancel:
        return CancelException(error.error.message);
      case DioErrorType.response:
        try {
          int? errCode = error.response?.statusCode;
          switch (errCode) {
            case 400:
              return BadRequestException(message: "請求語法錯誤", code: errCode);
            case 401:
              return UnauthorisedException(message: "沒有權(quán)限", code: errCode);
            case 403:
              return BadRequestException(message: "服務(wù)器拒絕執(zhí)行", code: errCode);
            case 404:
              return BadRequestException(message: "無法連接服務(wù)器", code: errCode);
            case 405:
              return BadRequestException(message: "請求方法被禁止", code: errCode);
            case 500:
              return BadServiceException(message: "服務(wù)器內(nèi)部錯誤", code: errCode);
            case 502:
              return BadServiceException(message: "無效的請求", code: errCode);
            case 503:
              return BadServiceException(message: "服務(wù)器掛了", code: errCode);
            case 505:
              return UnauthorisedException(
                  message: "不支持HTTP協(xié)議請求", code: errCode);
            default:
              return UnknownException(error.error.message);
          }
        } on Exception catch (_) {
          return UnknownException(error.error.message);
        }

      case DioErrorType.other:
        if (error.error is SocketException) {
          return NetworkException(message: error.message);
        } else {
          return UnknownException(error.message);
}
      default:
        return UnknownException(error.message);
    }
  } else {
    return UnknownException(error.toString());
  }
}

緩存沈堡、重試、401攔截

默認(rèn)的通用攔截器在 AppDio里直接定義燕雁,如果需要額外配置的攔截器诞丽,從HttpConfig里傳入。

這些攔截器的創(chuàng)建拐格,可以參考上一篇強(qiáng)大的dio封裝僧免,可能滿足你的一切需要,這里就不再贅述。

使用

第一步捏浊,全局配置并初始化:

  HttpConfig dioConfig =
      HttpConfig(baseUrl: "https://gank.io/", proxy: "192.168.2.249:8888");
  HttpClient client = HttpClient(dioConfig: dioConfig);
  Get.put<HttpClient>(client);

請求:

  void get() async {
    HttpResponse appResponse = await dio.get("api/v2/banners");
    if (appResponse.ok) {
      debugPrint("====" + appResponse.data.toString());
    } else {
      debugPrint("====" + appResponse.error.toString());
    }
  }

附上開發(fā)環(huán)境:

[?] Flutter (Channel stable, 2.0.5, on Mac OS X 10.15.7 19H15 darwin-x64, locale zh-Hans-CN)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末懂衩,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子金踪,更是在濱河造成了極大的恐慌浊洞,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件热康,死亡現(xiàn)場離奇詭異沛申,居然都是意外死亡劣领,警方通過查閱死者的電腦和手機(jī)姐军,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人奕锌,你說我怎么就攤上這事著觉。” “怎么了惊暴?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵饼丘,是天一觀的道長。 經(jīng)常有香客問我辽话,道長肄鸽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任油啤,我火速辦了婚禮典徘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘益咬。我一直安慰自己逮诲,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布幽告。 她就那樣靜靜地躺著梅鹦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪冗锁。 梳的紋絲不亂的頭發(fā)上齐唆,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機(jī)與錄音冻河,去河邊找鬼蝶念。 笑死,一個胖子當(dāng)著我的面吹牛芋绸,可吹牛的內(nèi)容都是我干的媒殉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼摔敛,長吁一口氣:“原來是場噩夢啊……” “哼廷蓉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起马昙,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤桃犬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后行楞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體攒暇,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年子房,在試婚紗的時候發(fā)現(xiàn)自己被綠了形用。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片就轧。...
    茶點(diǎn)故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖田度,靈堂內(nèi)的尸體忽然破棺而出妒御,到底是詐尸還是另有隱情,我是刑警寧澤镇饺,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布乎莉,位于F島的核電站,受9級特大地震影響奸笤,放射性物質(zhì)發(fā)生泄漏惋啃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一监右、第九天 我趴在偏房一處隱蔽的房頂上張望肥橙。 院中可真熱鬧,春花似錦秸侣、人聲如沸存筏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽椭坚。三九已至,卻和暖如春搏色,著一層夾襖步出監(jiān)牢的瞬間善茎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工频轿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留垂涯,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓航邢,卻偏偏與公主長得像耕赘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子膳殷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容