Many nuggets friends in the last message, said to encapsulate the latest version, so this package ideas write down, we can encapsulate their own. If you have a good idea, you can also send a request to Github. Thanks to WingCH for his contribution
Analyze requirements
Why encapsulate?
-
Global Token Authentication
-
Custom interceptors
-
Cache handling
-
Unified encapsulation of business error logic
-
Proxy configuration
-
Retry mechanism
-
The log output
-
Custom parsing, data shinning
Which configurations are to be initialized?
- The domain name
- Address of the agent
- Cookie Local cache address
- timeout
- Custom interceptors
Define a configuration information class to initialize these configurations:
// Dio configuration item 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>(); }Copy the code
What are the configurations for request differentiation?
-
Resolution strategy
Many corporate interface specifications have undergone changes to have multiple return types, requiring different parsing for different data types.
Like the old version:
/ / the old version {" code ": 1," data ": {}," state ": true} {/ / new version" code ": 1," data ": {" data" : {}, "hasmore" : false}, "message" : "Success"}Copy the code
To unshell and get parsed data, you need two parsing strategies. Therefore, you need to dynamically configure resolution policies based on different interfaces.
-
path
-
parameter
-
cancelToken
-
Common dio parameters
Dio request parameters already fully include the analyzed configuration parameters, just need to add another parsing policy.
Define an abstract parsing strategy that follows the SOLID principle:
Abstract class HttpTransformer {HttpResponse parse(Response Response); }Copy the code
Default implementation based on actual requirements:
Class DefaultHttpTransformer extends HttpTransformer {// Suppose the interface return type // {/ "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(); DefaultHttpTransformer._internal(); / / / factory construction method, used here named constructor declaration in the form of factory DefaultHttpTransformer. GetInstance () = > _instance. }Copy the code
The singleton pattern is designed to avoid creating instances multiple times. Convenient for next use.
Exception handling
Anomalies are generally classified as follows:
- Network anomalies
- The client request is abnormal. Procedure
- Server Exception
Client exceptions can be classified into two common exceptions: request parameters or paths are incorrect, authentication fails, and token is invalid
Create exceptions after exception archiving:
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); } 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(); }Copy the code
Return data type
The returned data type needs to have a success or failure identifier, as well as the unshucked data, if the failure, also need to fail information, define several factory methods to facilitate the creation of instance:
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; }}Copy the code
Began to encapsulate
Configuration Dio
Dio configuration assembly requires us to define an initialization class to add the requested initialization configuration to. In general, we can define a singleton class to initialize Dio in init method, or we can implement 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();
};
}
}
Copy the code
A Restful request
Use Restful standards to create the following request methods:
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; }}}Copy the code
The response parsing
After the request data is obtained, it is parsed into the defined general return data type. It is necessary to first determine whether the return value is obtained, and then determine whether the network request is successful. After the network request is successful, it is necessary to determine whether the interface returns the expected data, or whether the request parameter is wrong or the server returns an error message. If there is an error, format the error message as a defined exception:
HttpResponse handleResponse(Response? response, {HttpTransformer? httpTransformer}) { httpTransformer ?? = DefaultHttpTransformer.getInstance(); / / the return value if abnormal (response = = null) {return an HttpResponse. FailureFromError (); } / / token failure if (_isTokenTimeout (response. StatusCode)) {return an HttpResponse. FailureFromError ( UnauthorisedException(message: "no permission ", code: response.statuscode); If (_isRequestSuccess(response.statusCode)) {return httpTransformer. Parse (response); } else {return httpresponse.failure (errorMsg: response.statusMessage, errorCode: response.statusCode); } } HttpResponse handleException(Exception exception) { var parseException = _parseException(exception); return HttpResponse.failureFromError(parseException); } /// Authentication failed 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: "error syntax ", code: errCode); Case 401: return UnauthorisedException(message: "no permission ", code: errCode); Case 403: return BadRequestException(message: "Server refuses to execute ", code: errCode); Case 404: return BadRequestException(message: "Failed to connect to server ", code: errCode); Case 405: return BadRequestException(message: "request method forbidden ", code: errCode); Case 500: return BadServiceException(message: "Internal server error ", code: errCode); Case 502: return BadServiceException(message: "invalid request ", code: errCode) Case 503: return BadServiceException(message: "The server is down ", code: errCode); Case 505: return UnauthorisedException(message: "HTTP request not supported ", code: errCode); case 505: Return UnauthorisedException(message:" HTTP request not supported ", 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()); }}Copy the code
Cache, retry, 401 intercept
The default generic interceptor is defined directly in AppDio, and if additional interceptors are needed, they are passed in from HttpConfig.
The creation of these interceptors, which you can refer to in the previous article on powerful DIO encapsulation, may have everything you need, but I won’t go over it here.
use
Step 1, global configuration and initialization:
HttpConfig dioConfig =
HttpConfig(baseUrl: "https://gank.io/", proxy: "192.168.2.249:8888");
HttpClient client = HttpClient(dioConfig: dioConfig);
Get.put<HttpClient>(client);
Copy the code
Request:
void get() async { HttpResponse appResponse = await dio.get("api/v2/banners"); if (appResponse.ok) { debugPrint("====" + appResponse.data.toString()); } else { debugPrint("====" + appResponse.error.toString()); }}Copy the code
Attached development environment:
✓] Flutter (Channel stable, 2.0.5, on Mac OS X 10.15.7 19H15 Darwin-x64, locale zh-hans-cn)Copy the code
The source address