Flutter 網絡封裝 2022-10-12 周三

網絡選擇

  • Flutter自帶httpClient,這個也是很好用的;

  • Http庫,有個三方庫的名字就叫這個;

  • Dio,這是目前最熱門的,相當于iOS中AFNetworking。隨大流,就選這個進行封裝。

Dio引入

  • Dio是一個第三方庫,所以需要先下載。使用一行命令就可以引入flutter pub add dio
    dio: ^4.0.6

  • 日志是需要的,最簡單的就是用系統提供debugPrint,基本上也夠用了。為Dio專門寫的插件也有,比如dio_logger。也有比較流行的插件,比如logger

  • loading一方面是等待,另一方是防止用戶誤操作。一般這個也是用第三方插件的居多。
    這方面有一個比較突出的第三方插件,那就用吧。
    flutter_easyloading: ^3.0.5
    另外,toast一般和loading都在一個插件中,這兩種都是需要的,類似的插件也可以考慮
    bot_toast
    感覺上flutter_easyloading要好一點。

  • 網絡狀態檢測,主要是判斷有網還是沒網,功能和ping類似,這個也一般需要第三方插件。如果沒有這個,只有等Dio連接斷了再提示。這方面也有一個比較流行的第三方插件。
    connectivity_plus: ^2.3.6
    當然,監聽網絡狀態也是可以的,不過這個不是強需求,可以再需要的時候添加。

  • 網絡緩存,失敗重傳,cookie管理這些,不是必須的需求,可以延后考慮。目前這些也有相應的Dio配套三方庫可以選擇。這些都是攔截器模式的,可以隨時添加。

  • 抓包,代理,證書驗證這些也暫時不加,等以后有需要的時候再考慮。

連接常數

用一個類來保存網絡連接需要常數,目前主要是超時時間,baseURL,頭部信息等。常見的需求切換后臺環境,主要的就是更換這個baseURL

class HttpOptions {
  /// 超時時間;單位是ms
  static const int connectTimeout = 30000;
  static const int receiveTimeout = 30000;

  /// 地址前綴
  static const String baseUrl = 'https://baidu.com';

  /// header
  static const Map<String, dynamic> headers = {
    'Accept': 'application/json,*/*',
    'Content-Type': 'application/json',
    'currency': 'CNY',
    'lang': 'en',
    'device': 'app',
  };
}

異常處理

  • 錯誤提示是重要的基礎功能,繞不過的,所以自定義一個類,實現Exception協議,主要包括錯誤碼和錯誤信息兩部分內容。只需要一次封裝就好了,分太細反而麻煩。

  • DioError是Dio提供的一個錯誤信息結構,我們可以基于這個,添加一些自定義的信息。

  • DioError中還包含服務響應Response,里面有代表服務強響應的狀態碼和錯誤信息,可以利用這點,對服務器端的錯誤狀態進行一次封裝。

import 'package:dio/dio.dart';

class HttpException implements Exception {
  final int code;
  final String msg;

  HttpException({
    this.code = -1,
    this.msg = 'unknow error',
  });

  @override
  String toString() {
    return 'Http Error [$code]: $msg';
  }

  factory HttpException.create(DioError error) {
    /// dio異常
    switch (error.type) {
      case DioErrorType.cancel:
        {
          return HttpException(code: -1, msg: 'request cancel');
        }
      case DioErrorType.connectTimeout:
        {
          return HttpException(code: -1, msg: 'connect timeout');
        }
      case DioErrorType.sendTimeout:
        {
          return HttpException(code: -1, msg: 'send timeout');
        }
      case DioErrorType.receiveTimeout:
        {
          return HttpException(code: -1, msg: 'receive timeout');
        }
      case DioErrorType.response:
        {
          try {
            int statusCode = error.response?.statusCode ?? 0;
            // String errMsg = error.response.statusMessage;
            // return ErrorEntity(code: errCode, message: errMsg);
            switch (statusCode) {
              case 400:
                {
                  return HttpException(code: statusCode, msg: 'Request syntax error');
                }
              case 401:
                {
                  return HttpException(code: statusCode, msg: 'Without permission');
                }
              case 403:
                {
                  return HttpException(code: statusCode, msg: 'Server rejects execution');
                }
              case 404:
                {
                  return HttpException(code: statusCode, msg: 'Unable to connect to server');
                }
              case 405:
                {
                  return HttpException(code: statusCode, msg: 'The request method is disabled');
                }
              case 500:
                {
                  return HttpException(code: statusCode, msg: 'Server internal error');
                }
              case 502:
                {
                  return HttpException(code: statusCode, msg: 'Invalid request');
                }
              case 503:
                {
                  return HttpException(code: statusCode, msg: 'The server is down.');
                }
              case 505:
                {
                  return HttpException(code: statusCode, msg: 'HTTP requests are not supported');
                }
              default:
                {
                  return HttpException(
                      code: statusCode, msg: error.response?.statusMessage ?? 'unknow error');
                }
            }
          } on Exception catch (_) {
            return HttpException(code: -1, msg: 'unknow error');
          }
        }
      default:
        {
          return HttpException(code: -1, msg: error.message);
        }
    }
  }
}

異常攔截器

  • 把錯誤處理做一個攔截器,在onError方法中將自定義的異常類型放入DioError的error字段(dynamic的)

  • 這個確實有點繞。顯示通過DioError的error type來創建自定義的異常類型HttpException;然后又把自定義的異常類型通過攔截器放入DioError的error字段,重新回歸到Dio的框架處理過程之中。

  • 如果沒有網絡,DioError的type字段為DioErrorType.other,這部分自定義的異常類型HttpException并沒有處理。所以這個時候,還需要調用一下檢測網絡狀態第三方插件connectivity_plus,看看是不是斷網了。

class ErrorInterceptor extends Interceptor {
  @override
  void onError(DioError err, ErrorInterceptorHandler handler) async {
    /// 根據DioError創建HttpException
    HttpException httpException = HttpException.create(err);

    /// dio默認的錯誤實例,如果是沒有網絡,只能得到一個未知錯誤,無法精準的得知是否是無網絡的情況
    /// 這里對于斷網的情況,給一個特殊的code和msg
    if (err.type == DioErrorType.other) {
      var connectivityResult = await (Connectivity().checkConnectivity());
      if (connectivityResult == ConnectivityResult.none) {
        httpException = HttpException(code: -100, msg: 'None Network.');
      }
    }

    /// 將自定義的HttpException
    err.error = httpException;

    /// 調用父類,回到dio框架
    super.onError(err, handler);
  }
}

封裝

  • 一般都會把Dio封裝為一個單例。一個APP中,一個Dio實例足夠了。

  • 雖然Dio提供了get,post等各種方法,但是需要加入一些自定義的參數,所以一般會直接封裝更底層的request方法。

  • http的method是字符串,可以考慮用一個枚舉,在調用request方法的時候統一轉換。

  • loading,錯誤信息,可以在這個request方法上統一用try catch結構在這里處理

  • 錯誤信息,log,自定義頭部(比如token)等可以通過攔截器的形式加入。有很多配合Dio的攔截器第三方插件,比如pretty_dio_logger

  • 公共頭部信息,超時時間,baseUrl等信息可以通過BaseOption的形式在Dio單例創建的時候給出。

  • 切換環境可以通過修改baseUrl的方式實現。直接代碼注釋是最方便的,高級一點的話,可以通過本地緩存的方式來實現。

class HttpRequest {
  // 單例模式使用Http類,
  static final HttpRequest _instance = HttpRequest._internal();
  factory HttpRequest() => _instance;

  static late final Dio dio;

  /// 內部構造方法
  HttpRequest._internal() {
    /// 初始化dio
    BaseOptions options = BaseOptions(
      connectTimeout: HttpOptions.connectTimeout,
      receiveTimeout: HttpOptions.receiveTimeout,
      sendTimeout: HttpOptions.sendTimeout,
      baseUrl: HttpOptions.baseUrl,
      headers: HttpOptions.headers,
    );
    dio = Dio(options);

    /// 添加各種攔截器
    dio.interceptors.add(ErrorInterceptor());
    dio.interceptors.add(dioLoggerInterceptor);
  }

  /// 封裝request方法
  Future request({
    required String path,
    required HttpMethod method,
    dynamic data,
    Map<String, dynamic>? queryParameters,
    bool showLoading = true,
    bool showErrorMessage = true,
  }) async {
    const Map methodValues = {
      HttpMethod.get: 'get',
      HttpMethod.post: 'post',
      HttpMethod.put: 'put',
      HttpMethod.delete: 'delete',
      HttpMethod.patch: 'patch',
      HttpMethod.head: 'head'
    };
    Options options = Options(
      method: methodValues[method],
    );

    try {
      if (showLoading) {
        EasyLoading.show(status: 'loading...');
      }
      Response response = await HttpRequest.dio.request(
        path,
        data: data,
        queryParameters: queryParameters,
        options: options,
      );
      return response.data;
    } on DioError catch (error) {
      HttpException httpException = error.error;
      if (showErrorMessage) {
        EasyLoading.showToast(httpException.msg);
      }
    } finally {
      if (showLoading) {
        EasyLoading.dismiss();
      }
    }
  }
}

enum HttpMethod {
  get,
  post,
  delete,
  put,
  patch,
  head,
}

工具方法

  • 直接使用request方法不是很方便,所以,再封裝一層,對外仍然提供get,post等方便方法

  • 一般直接包裝成靜態方法,用起來最方便。

/// 調用底層的request,重新提供get,post等方便方法
class HttpUtil {
  static HttpRequest httpRequest = HttpRequest();

  /// get
  static Future get({
    required String path,
    Map<String, dynamic>? queryParameters,
    bool showLoading = true,
    bool showErrorMessage = true,
  }) {
    return httpRequest.request(
      path: path,
      method: HttpMethod.get,
      queryParameters: queryParameters,
      showLoading: showLoading,
      showErrorMessage: showErrorMessage,
    );
  }

  /// post
  static Future post({
    required String path,
    required HttpMethod method,
    dynamic data,
    bool showLoading = true,
    bool showErrorMessage = true,
  }) {
    return httpRequest.request(
      path: path,
      method: HttpMethod.post,
      data: data,
      showLoading: showLoading,
      showErrorMessage: showErrorMessage,
    );
  }

邏輯模塊

  • 一般后臺會按照邏輯模塊分類,分類一般以path中的字符來區分。比如用戶模塊一般以/user/開頭。

  • 對應于后臺的習慣,可以考慮以模塊名為文件名,將類似的接口放在同一個文件中。比如用戶模塊都放在user_api.dart文件中。

  • 具體到每一個接口,path,參數等都可以確定,所以在用static方法包一層是可行的。

import 'package:panda_buy/apis/http/http_util.dart';

class UserApi {
  /// path定義
  static const String pathPrefix = '/gateway/user/';

  /// 獲取公鑰
  static Future getPubKey() {
    return HttpUtil.get(
      path: '${pathPrefix}pubkey',
      showLoading: false,
      showErrorMessage: false,
    );
  }
  • 文件結果如下
企業微信截圖_d8012040-f6af-4d98-a46c-1eabb2a05d95.png

log

  • dio_logger 是為Dio定制的,只要一行代碼就可以了dio.interceptors.add(dioLoggerInterceptor);

  • log內容按照request和response分開,基本能用。

  • 顏色區分沒有,不知道什么原因。

  • 以攔截器的形式作為Dio的配件引入,感覺還是不錯的。

企業微信截圖_1eb5734a-3e11-4ed1-8bc0-ef41b183451d.png

參考文章

Flutter Dio 親媽級別封裝教程

Flutter應用框架搭建(四) 網絡請求封裝

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容