網絡選擇
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,
);
}
- 文件結果如下
log
dio_logger
是為Dio定制的,只要一行代碼就可以了dio.interceptors.add(dioLoggerInterceptor);
log內容按照request和response分開,基本能用。
顏色區分沒有,不知道什么原因。
以攔截器的形式作為Dio的配件引入,感覺還是不錯的。