Flutter Dio 親媽級別封裝教程

油糕 發表於 2021-10-20
Flutter
前不久看到 艾維碼 大佬的dio封裝,經過摸索,改吧改吧,使用的不錯。對於之前 艾維碼 大佬文章中一些已經失效的做了修正

為什麼一定要封裝一手?

token攔截,錯誤攔截,統一錯誤處理,統一快取,統一資訊封裝(錯誤,正確)

Cookie???滾犢子

不管cookie,再見

全域性初始化,傳入引數

dio初始化,傳入baseUrl, connectTimeout, receiveTimeout,options,header 攔截器等。dio初始化的時候允許我們傳入的一些配置

dio初始化的配置

這裡說下,之前 艾維碼 大佬的帖子中的options,最新版的dio已經使用requestOptions, 之前的merge,現在使用copyWith。詳情向下看

如果要白嫖完整的方案

可以參考使用這套方案開發的 flutter + getx 仿開眼視訊app,有star的大佬可以賞點star。

未標題-2.jpg

未標題-3.jpg

專案地址 github地址
apk下載1 百度雲 提取碼:2mpq
apk下載2 藍奏雲 提取碼:fpqc

初始化

這裡說下攔截器,可以在初始化的時候傳入,也可以手寫傳入,例如我這裡定義了四個攔截器,第一個用於全域性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);
    }
}

未標題-4.jpg

這裡的提示就是自定義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);
    }
  }

如果要白嫖完整的方案

可以參考使用這套方案開發的 flutter + getx 仿開眼視訊app,有star的大佬可以賞點star。

專案地址 github地址
apk下載 panbaidu 提取碼:3ev2