Flutter 網路請求 Dio 封裝

小林rush發表於2021-09-01

一般在dart中網路請求庫採用的是dio,在dio之上我們需要對其進行一層封裝,以適應業務的需求。

dio是一個強大的Dart Http請求庫,支援Restful API、FormData、攔截器、請求取消、Cookie管理、檔案上傳/下載、超時等...

1. 首先建一個網路管理工具類DioManager

class DioManager {
  factory DioManager() => getInstance();

  static DioManager get instance => getInstance();
  static DioManager? _instance;

  static DioManager getInstance() {
    if (_instance == null) {
      _instance = DioManager._init();
    }
    return _instance!;
  }

  Dio? _dio;
複製程式碼

2. 初始化dio

DioManager._init() {
    if (_dio == null) {
      // 設定 Dio 預設配置
      _dio = Dio(BaseOptions(
          // 請求基地址
          baseUrl: GlobalData.baseUrl,
          // 連線伺服器超時時間,單位是毫秒
          connectTimeout: 60 * 1000,
          // 接收資料的最長時限
          receiveTimeout: 60 * 1000));
      String proxy = GlobalData.proxy;
      if (TextUtil.isNotEmpty(proxy)) {
        (_dio!.httpClientAdapter as DefaultHttpClientAdapter)
            .onHttpClientCreate = (client) {
          // config the http client
          client.findProxy = (uri) {
            return "PROXY $proxy";
          };
          //抓Https包設定,這裡是預設都通過
          client.badCertificateCallback =
              (X509Certificate cert, String host, int port) => true;
        };
      }

      CookieJar cookieJar = CookieJar();
      // 攔截器,按順序攔截
      _dio!.interceptors.add(CookieManager(cookieJar));
      _dio!.interceptors.add(OnReqResInterceptors());
      _dio!.interceptors.add(OnErrorInterceptors());
    }
  }

複製程式碼

3. JSON 序列化資料

在實戰中,後臺介面往往會返回一些結構化資料,如JSON。我們可以先將JSON格式轉為Dart物件,json.decode() 可以根據JSON字串具體內容將其轉為List或Map,但是這會讓我們在執行時才知道具體的型別,這就失去了型別檢查的好處了,所以我們還需要將Map或者List轉為dart物件,也就是'model'。

首先我們從BaseModel說起,這是專案中後端返回的內容格式,我的專案中BaseModel主要是3個值:

  • responseCode 狀態碼
  • responseMsg 返回的提示訊息
  • responseData 資料

重點是responseData,這是個泛型,先上程式碼:

class BaseModel<T> {
  int? responseCode;
  String? responseMsg;
  T? responseData;

  BaseModel({this.responseCode, this.responseMsg, this.responseData});

  BaseModel.copy(BaseModel baseModel) {
    this.responseCode = baseModel.responseCode;
    this.responseMsg = baseModel.responseMsg;
  }

  bool get isSuccess {
    return this.responseCode == ErrCode.SUCCESS;
  }

  BaseModel.fromJson(Map<String, dynamic> json, [FromJson<T>? fromJson]) {
    int? responseCode = json['ResponseCode'];
    String? responseMsg = json['ResponseMsg'];
    var data = json['ResponseData'];
    T? responseData;
    try {
      if (data != null) {
        if (fromJson != null) {
          responseData = fromJson(data);
        } else {
          responseData = data;
        }
      }
    } catch (e, stack) {
      throw "responseData解析異常: $e\n$stack";
    }
    this.responseData = responseData;
    this.responseMsg = responseMsg;
    this.responseCode = responseCode;
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['ResponseMsg'] = this.responseMsg;
    data['ResponseCode'] = this.responseCode;
    data['ResponseData'] = this.responseData;
    return data;
  }
}

複製程式碼

可以看到fromJson的構造方法裡有個FromJson的可選引數,這個是用來將從json轉為的Map再轉為Model的方法。

以下是基礎型別的轉化:


typedef FromJson<T> = T Function(dynamic json);

FromJson<String> fromStringJson = (dynamic json) => json;
FromJson<int> fromIntJson = (dynamic json) => json;
FromJson<double> fromDoubleJson = (dynamic json) => json;
FromJson<bool> fromBoolJson = (dynamic json) => json;
FromJson<Map<String, dynamic>> fromMapJson = (dynamic json) => json;
FromJson<List<String>> fromListStringJson = fromListJson(fromStringJson);
FromJson<List<int>> fromListIntJson = fromListJson(fromIntJson);
FromJson<List<bool>> fromListBoolJson = fromListJson(fromBoolJson);

FromJson<List<T>> fromListJson<T>(FromJson<T> fromJson) => (dynamic json) {
      List<T> list = [];
      if (json != null && json is List) {
        list = json.map((e) => fromJson(e)).toList();
      }
      return list;
    };

複製程式碼

如果你的responseData是個複雜結構,不能用基礎型別表示,那麼就需要你自己提供一個'FromJson'.

比如結構是這樣子:

{
  "squadName" : "Super hero squad",
  "homeTown" : "Metro City",
  "members" : [
    {
      "name" : "Molecule Man",
      "age" : 29,
      "secretIdentity" : "Dan Jukes",
      "powers" : [
        "Radiation resistance",
        "Turning tiny",
        "Radiation blast"
      ]
    }
  ]
}
複製程式碼

那你就需要建立一個dart物件,表示這個資料結構,並且提供一個從Map轉為這個物件的建構函式。

官方文件提供了兩種JSON 序列化資料的方法,一個是手動序列化資料,另一個是利用程式碼生成進行自動序列化資料。

我都不喜歡,手動太繁瑣,利用程式碼生成雖然有一定的智慧但是還需要你手寫出所有的成員以及新增註釋,我更喜歡直接從json文字直接生成dart物件。

把後端返回的json文字資料拿去一些json2dart的網站,比如:

4. 請求封裝

get、post等封裝,其實還有put、delete以及下載檔案用到的dio.download,在此不列舉。 請求引數除了基礎的url params和formData以外,還有[isShowErrorToast] 表示出現錯誤的情況是否自動彈窗提示錯誤,預設true;[isAddTokenInHeader] 請求頭是否新增token; [fromJson]是將json轉為model的方法。

約定所有的成功、錯誤、異常等各種情況都會返回一個BaseModel,請求後的業務程式碼需要呼叫BaseModel例項的isSuccess方法來判斷是否請求成功。

/// Dio 請求方法

enum DioMethod {
  get,
  post,
  put,
  delete,
}

/// get請求
/// [isShowErrorToast] 出現錯誤的情況是否自動彈窗提示錯誤,預設true
/// [isAddTokenInHeader] 請求頭是否新增token
/// [fromJson]是將json轉為model的方法
Future get<T>(
  {required String url,
  bool isShowErrorToast = true,
  bool isAddTokenInHeader = true,
  Map<String, dynamic>? params,
  FromJson<T>? fromJson}) async {
return await requestHttp(url,
    method: DioMethod.get,
    isShowErrorToast: isShowErrorToast,
    params: params,
    fromJson: fromJson);
}

/// post 請求
Future post<T>(
  {required String url,
  Map<String, dynamic>? params,
  bool isAddTokenInHeader = true,
  bool isShowErrorToast = true,
  FormData? formData,
  FromJson<T>? fromJson,
  CancelToken? cancelToken,
  ProgressCallback? onSendProgress,
  ProgressCallback? onReceiveProgress}) async {
return await requestHttp<T>(url,
    method: DioMethod.post,
    isShowErrorToast: isShowErrorToast,
    params: params,
    formData: formData,
    fromJson: fromJson,
    cancelToken: cancelToken,
    onSendProgress: onSendProgress,
    onReceiveProgress: onReceiveProgress);
}

/// Dio request 方法
Future requestHttp<T>(String url,
  {DioMethod method = DioMethod.get,
  Map<String, dynamic>? params,
  bool isShowErrorToast = true,
  bool isAddTokenInHeader = true,
  FormData? formData,
  FromJson<T>? fromJson,
  CancelToken? cancelToken,
  ProgressCallback? onSendProgress,
  ProgressCallback? onReceiveProgress}) async {
const methodValues = {
  DioMethod.get: 'get',
  DioMethod.post: 'post',
  DioMethod.delete: 'delete',
  DioMethod.put: 'put'
};

try {
  Response response;

  /// 不同請求方法,不同的請求引數,按實際專案需求分.
  /// 這裡 get 是 queryParameters,其它用 data. FormData 也是 data
  /// 注意: 只有 post 方法支援傳送 FormData.
  switch (method) {
    case DioMethod.get:
      response = await _dio!.request(url,
          queryParameters: params,
          options: Options(method: methodValues[method], extra: {
            'isAddTokenInHeader': isAddTokenInHeader,
            'isShowErrorToast': isShowErrorToast
          }));
      break;
    default:
      // 如果有formData引數,說明是傳檔案,忽略params的引數
      if (formData != null) {
        response = await _dio!.post(url,
            data: formData,
            cancelToken: cancelToken,
            onSendProgress: onSendProgress,
            onReceiveProgress: onReceiveProgress);
      } else {
        response = await _dio!.request(url,
            data: params,
            cancelToken: cancelToken,
            options: Options(method: methodValues[method], extra: {
              'isAddToken': isAddTokenInHeader,
              'isShowErrorToast': isShowErrorToast
            }));
      }
  }
  // json轉model
  String jsonStr = json.encode(response.data);
  Map<String, dynamic> responseMap = json.decode(jsonStr);
  BaseModel<T> baseModel = BaseModel.fromJson(responseMap, fromJson);

  // 處理使用者被踢出的情況
  if (baseModel.responseCode == ErrCode.USER_OUT) {
    // TODO ...
    return baseModel;
  }

  if (baseModel.responseCode != ErrCode.SUCCESS) {
    if (isShowErrorToast) {
      EasyLoading.showToast(baseModel.responseMsg ?? '請求出錯');
    }
  }
  return baseModel;
} on DioError catch (e) {
  // DioError是指返回值不為200的情況
  logger.shout('DioError報錯${e.type}:${e.error.toString()}');
  // 對錯誤進行判斷
  onErrorInterceptor(e);
  // 判斷是否斷網了
  String? errorMsg = isNetworkConnected
      ? e.requestOptions.extra["errorMsg"]
      : "網路連線斷開,請檢查網路設定";
  return BaseModel<T>(
      responseCode: ErrCode.NETWORK_ERR, responseMsg: errorMsg);
} catch (e) {
  // 其他一些意外的報錯
  logger.shout('Dio報錯:${e.toString()}');
  return BaseModel<T>(responseCode: ErrCode.OTHER_ERR, responseMsg: "其他異常");
}
}

// 錯誤判斷
void onErrorInterceptor(DioError err) {
  // 異常分類
  switch (err.type) {
    // 4xx 5xx response
    case DioErrorType.response:
      err.requestOptions.extra["errorMsg"] = err.response?.data ?? "連線異常";
      break;
    case DioErrorType.connectTimeout:
      err.requestOptions.extra["errorMsg"] = "連線超時";
      break;
    case DioErrorType.sendTimeout:
      err.requestOptions.extra["errorMsg"] = "傳送超時";
      break;
    case DioErrorType.receiveTimeout:
      err.requestOptions.extra["errorMsg"] = "接收超時";
      break;
    case DioErrorType.cancel:
      err.requestOptions.extra["errorMsg"] =
          err.message.isNotEmpty ? err.message : "取消連線";
      break;
    case DioErrorType.other:
    default:
      err.requestOptions.extra["errorMsg"] = "連線異常";
      break;
  }
}
複製程式碼
  1. 攔截器

主要是在請求之前給header新增一些指定的引數。 如果header的值是比較固定的,比如不需要時間戳引數進行運算的,那麼其實可以在初始化dio的時候傳入BaseOptions。

/// 請求攔截
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
String token = getToken();
if (token.isNotEmpty && options.extra['isAddTokenInHeader'] != false) {
  options.headers['Token'] = token;
  int t = DateTime.now().millisecondsSinceEpoch;
  options.headers['key'] =/* 加密 */;
  options.headers['timestamp'] = t;
}

options.headers['deviceId'] = getDeviceId();

return super.onRequest(options, handler);

}

複製程式碼

最後

如果有紕漏、錯誤,歡迎拍磚交流~

相關文章