Article series
A review of Flutter Dio
HttpClient, Http, Dio
Analysis of Flutter Dio source code (iii)- In-depth analysis
Analysis of Flutter Dio source code (iv)– Encapsulation
Video series
A review of The source code for Flutter Dio
A review of the source code for Flutter Dio
Analysis of Flutter Dio source code (iii)- In-depth analysis of the video tutorial
Analysis of The Source code of Flutter Dio (iv)– Encapsulating video tutorials
Source repository address
Github repository address
preface
This article will teach you how to encapsulate a class library by hand. Usually, we use the wheel made by others in our work. This article will show you how to build the wheel by yourself, and provide ideas and methods when we need to encapsulate other class libraries.
Why do YOU need to package Dio?
In the previous article, we have the basic use of Dio, request library comparison, source code analysis, we know that the use of Dio is very simple, so why do we need to package? There are two points:
1. Code migration
When a component library method changes significantly and needs to be migrated in multiple places, it needs to modify every file used, which can be tedious and problematic.
2. Request library switch
When the Dio library is not needed, we can easily switch to another network request library at any time, of course, Dio currently built-in support for the use of third-party library adapters.
3. Unified configuration
Because an application is almost uniformly configured, we can manage configurations for interceptors, converters, caches, unified error handling, proxy configuration, certificate verification, and more.
Use singleton mode for Dio encapsulation
Why use singleton pattern?
Since our application uses network requests on every page, instantiating a Dio every time we request it will simply add unnecessary overhead to the system, whereas using a singleton object will create the same object every time we access it, so we don’t need to instantiate the object again.
Create a singleton class
This is a singleton pattern created with a private constructor for static variables
class DioUtil {
factory DioUtil() => _getInstance();
static DioUtil get instance => _getInstance();
static DioUtil _instance;
DioUtil._init() {
/ / initialization
}
static DioUtil _getInstance() {
if (_instance == null) {
_instance = DioUtil._init();
}
return_instance; }}Copy the code
Initialize the Dio request
We set the timeout time, response time and BaseUrl uniformly
/// Connection timeout
static const int CONNECT_TIMEOUT = 60*1000;
/// Response timeout
static const int RECEIVE_TIMEOUT = 60*1000;
/// Declare the Dio variable
Dio _dio;
DioUtil._init() {
if (_dio == null) {
/// Initialize the base options
BaseOptions options = BaseOptions(
baseUrl: "http://localhost:8080",
connectTimeout: CONNECT_TIMEOUT,
receiveTimeout: RECEIVE_TIMEOUT
);
/// Initialize the dio_dio = Dio(options); }}Copy the code
Encapsulate Restful APi styles in a unified manner
Since both get() and POST () requests end up calling the Request method internally, just with a different method passed in, we define an enumerated type to handle in a method
enum DioMethod {
get,
post,
put,
delete,
patch,
head,
}
/// Request class
Future<T> request<T>(String path, {
DioMethod method = DioMethod.get.Map<String.dynamic> params,
data,
CancelToken cancelToken,
Options options,
ProgressCallback onSendProgress,
ProgressCallback onReceiveProgress,
}) async {
const _methodValues = {
DioMethod.get: 'get',
DioMethod.post: 'post',
DioMethod.put: 'put',
DioMethod.delete: 'delete',
DioMethod.patch: 'patch',
DioMethod.head: 'head'}; options ?? = Options(method: _methodValues[method]);try {
Response response;
response = await _dio.request(path,
data: data,
queryParameters: params,
cancelToken: cancelToken,
options: options,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress
);
return response.data;
} on DioError catch (e) {
throwe; }}Copy the code
The interceptor
introduce
We’ve simplified the Restful API style to a method that identifies different requests through DioMethod. In our normal development process, we need to do special processing on some interfaces before request, before response, when error, then we need to use interceptors. Dio provides us with a custom interceptor function, which is easy to implement request, response, error interception
Unified error handling
We found that although the Dio framework already packages a DioError class library, we can only customize it if we need to do uniform popover handling of returned errors or route jumps, etc
Unified processing before request
When we send a request, we will encounter several situations, such as the need to automatically add some specific parameters to the non-open interface, obtaining the request header to add a unified token
Unified processing before response
Before we request the interface, we can do some basic processing on the response data, such as custom encapsulation of the result of the response, and special processing for individual urls.
Custom interceptor implementation
import 'package:dio/dio.dart';
import 'package:flutter_dio/dio_util/dio_response.dart';
class DioInterceptors extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// Add userId to all request parameters of non-open interfaces
if(! options.path.contains("open")) {
options.queryParameters["userId"] = "xxx";
}
// Add the token to the header
options.headers["token"] = "xxx";
// More business requirements
handler.next(options);
// super.onRequest(options, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
// Request success is basic processing of data
if (response.statusCode == 200) {
response.data = DioResponse(code: 0, message: "Request successful.", data: response);
} else {
response.data = DioResponse(code: 1, message: "Request failed", data: response);
}
// Do special processing for some individual URL return data
if (response.requestOptions.baseUrl.contains("?????????")) {
//....
}
// Customized according to the company's business needs
/ / the key
handler.next(response);
}
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
switch(err.type) {
// The connection to the server timed out
case DioErrorType.connectTimeout:
{
// Set the operation according to your own service requirements, can be a pop-up box prompt/or do some route redirection processing
}
break;
// Response timed out
case DioErrorType.receiveTimeout:
{
// Set the operation according to your own service requirements, can be a pop-up box prompt/or do some route redirection processing
}
break;
// Send timed out
case DioErrorType.sendTimeout:
{
// Set the operation according to your own service requirements, can be a pop-up box prompt/or do some route redirection processing
}
break;
// Request cancellation
case DioErrorType.cancel:
{
// Set the operation according to your own service requirements, can be a pop-up box prompt/or do some route redirection processing
}
break;
/ / 404/503 error
case DioErrorType.response:
{
// Set the operation according to your own service requirements, can be a pop-up box prompt/or do some route redirection processing
}
break;
// other Other error type
case DioErrorType.other:
{
}
break;
}
super.onError(err, handler); }}class DioResponse<T> {
/// Messages (such as success message text/error message text)
final String message;
/// Custom code(according to internal definition)
final int code;
/// The data returned by the interface
final T data;
/// Need to add more
/// .
DioResponse({
this.message,
this.data,
this.code,
});
@override
String toString() {
StringBuffer sb = StringBuffer('{');
sb.write("\"message\":\"$message\ "");
sb.write(",\"errorMsg\":\"$code\ "");
sb.write(",\"data\":\"$data\ "");
sb.write('} ');
returnsb.toString(); }}class DioResponseCode {
/// successful
static const int SUCCESS = 0;
/// error
static const int ERROR = 1;
/// More and more
}
Copy the code
converter
introduce
Transformer is used to encode and decode request and response data. Dio implements a DefaultTransformer DefaultTransformer as the DefaultTransformer. If you want custom codec processing of request/response data, you can provide custom converters
Why do you need a converter?
We read the introduction of converters and found that the function of interceptors is similar to that of interceptors, so why there are converters, there are two points:
- Decoupled from interceptors
- Do not modify the original request data
Execution flow: Request Interceptor >> Request Converter >> Initiate a request >> Response Converter >> Response Interceptor >> End Result.
Request converter
Will only be used for ‘PUT’, ‘POST’, ‘PATCH’ methods because only these can carry the request body.
Response converter
Is used for the return data of all request methods.
Custom converter implementation
import 'dart:async';
import 'package:dio/dio.dart';
class DioTransformer extends DefaultTransformer {
@override
Future<String> transformRequest(RequestOptions options) async {
// If the requested data interface is List
then we throw an exception
if (options.data is List<String>) {
throw DioError(
error: "You can't send List data directly to the server.",
requestOptions: options,
);
} else {
return super.transformRequest(options); }}@override
Future transformResponse(RequestOptions options, ResponseBody response) async {
For example, if we don't customize some header data in the response options, we can add it ourselves
options.extra['myHeader'] = 'abc';
return super.transformResponse(options, response); }}Copy the code
The refresh Token
In the development process, clients and servers often use a token for verification. Each company has a different logic for refreshing the token. Here is a simple example
We need to add a refreshToken to all the request headers. If the refreshToken does not exist, we first request the refreshToken, and then launch the subsequent requests after obtaining the refreshToken. Since the process of requesting refreshToken is asynchronous, we need to lock subsequent requests during the request (because they require refreshToken) until the refreshToken request is successful
import 'package:dio/dio.dart';
import 'package:flutter_dio/dio_util/dio_util.dart';
class DioTokenInterceptors extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (options.headers['refreshToken'] = =null) {
DioUtil.instance.dio.lock();
Dio _tokenDio = Dio();
_tokenDio..get("http://localhost:8080/getRefreshToken").then((d) {
options.headers['refreshToken'] = d.data['data'] ['token'];
handler.next(options);
}).catchError((error, stackTrace) {
handler.reject(error, true);
}) .whenComplete(() {
DioUtil.instance.dio.unlock();
}); // unlock the dio
} else {
options.headers['refreshToken'] = options.headers['refreshToken']; handler.next(options); }}@override
void onResponse(Response response, ResponseInterceptorHandler handler) async {
// The token must be refreshed before the response
super.onResponse(response, handler);
}
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
super.onError(err, handler); }}Copy the code
Cancel the request
Why do we need to have the function of canceling the request? If when our page sends the request, the user actively exits the current interface or the data does not respond when the APP exits, we need to cancel the network request to prevent unnecessary errors.
/// Cancel request token
CancelToken _cancelToken = CancelToken();
/// Canceling a network request
voidcancelRequests({CancelToken token}) { token ?? _cancelToken? .cancel("cancelled");
}
Copy the code
Cookie management
A cookie is introduced
A short piece of text information generated by the server is sent to the browser, and the browser saves the cookie in the form of KV in a text file under a local directory. The cookie will be sent to the server when the same website is requested next time.
The principle of
- The client sends a request (HTTP request + user authentication information) to the server
- After authentication is successful, the server sends an HttpResponse response to the client containing the set-cookie header
- The client extracts and saves the cookie to memory or disk
- When requested again, the HttpRequest request contains an authenticated Cookie header
- The server parses cookies to obtain information about clients in cookies
- The server returns the response data
use
The use of cookies requires two third-party components, dio_cookie_Manager and cookie_JAR
- Cookie_jar:
Dart
中http
The request ofcookie
Manager, through which you can easily handle complexcookie
Policy and persistencecookie
- Dio_cookie_manager: The CookieManager interceptor helps us automatically manage request/response cookies. CookieManager relies on the cookieJar package
The import file
dio_cookie_manager: ^2.0. 0
cookie_jar: ^3.01.
Copy the code
/// cookie
CookieJar cookieJar = CookieJar();
/// Add a cookie manager
_dio.interceptors.add(CookieManager(cookieJar));
List<Cookie> cookies = [
Cookie("xxx", xxx),
/ /...
];
//Save cookies
DioUtil.instance.cookieJar.saveFromResponse(Uri.parse(BaseUrl.url), cookies);
//Get cookies
List<Cookie> cookies = DioUtil.instance.cookieJar.loadForRequest(Uri.parse(BaseUrl.url));
Copy the code
Network interface cache
Why cache?
In our normal development process, we would encounter a situation that when making a network request, we hope to access the last data normally, which is better for users’ experience, rather than displaying a blank page. The cache is mainly for reference of the network interface cache of Flutter.
Persistence using shared_preferences
The memory cache will disappear after the program exits, so we use shared_preferences for disk cache data.
import 'dart:collection';
import 'package:dio/dio.dart';
import 'package:flutter_dio/dio_util/dio_util.dart';
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 DioCacheInterceptors extends Interceptor {
// To ensure that the iterator order is consistent with the object insertion time order, we use LinkedHashMap
var cache = LinkedHashMap<String, CacheObject>();
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if(! DioUtil.CACHE_ENABLE)return super.onRequest(options, handler);
// Use the refresh field to determine whether to refresh the cache
bool refresh = options.extra["refresh"] = =true;
if (refresh) {
// Delete the local cache
delete(options.uri.toString());
}
// Caching is enabled only for GET requests
if (options.extra["noCache"] != true &&
options.method.toLowerCase() == 'get') {
String key = options.extra["cacheKey"]???? options.uri.toString();var ob = cache[key];
if(ob ! =null) {
// If the cache is not expired, the cache contents are returned
if ((DateTime.now().millisecondsSinceEpoch - ob.timeStamp) / 1000 <
DioUtil.MAX_CACHE_AGE) {
return handler.resolve(cache[key].response);
} else {
// If it has expired, delete the cache and continue to request the servercache.remove(key); }}}super.onRequest(options, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
// Save the response data to the cache
if (DioUtil.CACHE_ENABLE) {
_saveCache(response);
}
super.onResponse(response, handler);
}
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
// TODO: implement onError
super.onError(err, handler);
}
_saveCache(Response object) {
RequestOptions options = object.requestOptions;
if (options.extra["noCache"] != true &&
options.method.toLowerCase() == "get") {
// If the number of caches exceeds the maximum number, the earliest record is removed first
if (cache.length == DioUtil.MAX_CACHE_COUNT) {
cache.remove(cache[cache.keys.first]);
}
String key = options.extra["cacheKey"] ?? options.uri.toString();
cache[key] = CacheObject(object);
}
}
void delete(Stringkey) { cache.remove(key); }}Copy the code
Proxy configuration
Dio proxy needs to be configured when we use Flutter to capture packets. DefaultHttpClientAdapter provides an onHttpClientCreate callback to set up the proxy for the underlying HttpClient.
/// Setting the Http proxy (enable when set)
void setProxy({
String proxyAddress,
bool enable = false{})if (enable) {
(_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(HttpClient client) {
client.findProxy = (uri) {
return proxyAddress;
};
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true; }; }}Copy the code
Certificate of calibration
Used to verify that the site being visited is authentic. Security is provided because the certificate is bound to the domain name and is signed by the root certificate Authority.
/// Example Set HTTPS certificate verification
void setHttpsCertificateVerification({
String pem,
bool enable = false{})if (enable) {
(_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.badCertificateCallback=(X509Certificate cert, String host, int port){
if(cert.pem==pem){ // Verify the certificate
return true;
}
return false; }; }; }}Copy the code
Unified Log Printing
Log printing is mainly to help us develop auxiliary troubleshooting
Void openLog() {_dio.interceptors.add(LogInterceptor(responseBody: true)); } DioUtil().openLog();Copy the code