有时我们不需要复杂的应用程序,只需要显示一个数据列表的简单功能,我们可以通过一个简单的方法来实现:
Future<Response> fetchItems() {
return Dio().("https://some-website.com/listing");
}
没有任何错误,也不用用log打印任何响应信息。
但是事实是,几乎没有这么简单的应用程序,有时我们需要做更多用于从服务器获取响应或调试我们的应用程序,比如:
- 将动态的Header发送给服务器,比如存储在SharedPreferences中的值。
- 检查每个响应头并保存他的值。
- 验证服务器返回的错误并将他们映射到我们应用程序中能够处理的错误类中。
- 另外,我们希望为我们的应用添加简单的缓存功能,以便在连接超时或用户无法访问网络时,可以显示该请求的缓存响应;
- 我们也可以添加log日志来打印我们的请求和响应信息。
Interceptors拦截器将通过为我们提供错误,请求和响应的特定回调来帮助我们处理这些问题。
在深入之前,我们先来看一下如何配置Dio。
Dio 配置
可以通过BaseOption来配置Dio,该对象允许我们设置一些参数来初始化Dio实例: connectTimeout,receiveTimeout和baseUrl,他们将用于每个Api的调用。
Dio createDio() {
return Dio(
BaseOptions(
connectTimeout: 5000,
receiveTimeout: 5000,
baseUrl: "https://some-website.com"
)
);
}
但是在基本配置中无法添加拦截器,我们需要在创建dio实例的时候在拦截器集合中添加我们需要的拦截器:
Dio addInterceptors(Dio dio) {
return dio..interceptors.add(InterceptorsWrapper(
onRequest: (RequestOptions options) => requestInterceptor(options),
onResponse: (Response response) => responseInterceptor(response),
onError: (DioError dioError) => errorInterceptor(dioError)));
}
我们已经设置了一个dio实例,可以用于任何一个Api调用。
添加动态Headers
假如我们需要将SharedPreferences中存储的一个值作为header传给服务器,我们不能使用BaseOption中的extra字段,该字段用于访问在创建请求时设置静态数据。
InterceptorsWrapper给我们提供了一个RequestOptions对象,有如下一些属性:
- Request
dynamic data - Url
String path - Query Parameters
Map<String, dynamic> queryParameters
有了这些信息,我们就可以实现我们自己的requestInterceptor方法了。
此方法返回一个动态类型,可以是:
RequestOptions对象,如果我们想继续请求的话Response对象,如果应用程序想自己处理请求DioError或dio.reject对象,将会抛出一个异常
这将使我们可以灵活的验证每个请求,添加数据以及在必要时抛出错误,我们使用起来只需要添加一些数据,然后处理返回的数据就可以了。
dynamic requestInterceptor(RequestOptions options) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var token = prefs.get("token");
options.headers.addAll({"Token": "$token${DateTime.now()}"});
return options;
}
我们还可以在创建endpoints时,为不同的请求添加是否需要token的header:
Future<Response> getListOfTodos() {
return dio.get("/todos/1", options: Options(headers: {"requiresToken" : true}));
}
然后在拦截器中进行统一处理:
dynamic requestInterceptor(RequestOptions options) async {
if (options.headers.containsKey("requiresToken")) {
//remove the auxiliary header
options.headers.remove("requiresToken");
SharedPreferences prefs = await SharedPreferences.getInstance();
var header = prefs.get("Header");
options.headers.addAll({"Header": "$header${DateTime.now()}"});
return options;
}
}
验证 Response
Response中有Headers,还有Status Code以及响应数据。我们将响应数据类型设置为动态类型。他可以是:
- 如果我们想继续请求就是
Response - 如果我们在验证数据响应之后抛出错误,就是
DioError
意味着,如果Header中的isUserActive值为false,那么就返回一个DioError.
dynamic responseInterceptor(Response options) async {
if (options.headers.value("verifyToken") != null) {
//if the header is present, then compare it with the Shared Prefs key
SharedPreferences prefs = await SharedPreferences.getInstance();
var verifyToken = prefs.get("VerifyToken");
// if the value is the same as the header, continue with the request
if (options.headers.value("verifyToken") == verifyToken) {
return options;
}
}
return DioError(request: options.request, message: "User is no longer active");
}
验证服务器错误
假设服务器中有删除用户账户的机制,那么一旦账户删除了之后,就会收到一个错误信息,从而界面用户界面将会跳转到创建用户的界面,服务器将返回该{"error":"ERROR_001"}错误信息,我们需要创建一个拦截器用来统一处理这些错误信息。
错误拦截器的类型也是动态的:
- 如果我们想继续发送错误请求,我们将返回
DioError对象 - 如果我们要解析请求并返回
Response对象,这种情况下,应用程序并不知道是服务器错误,将会继续正常请求。
dynamic errorInterceptor(DioError dioError) {
if (dioError.message.contains("ERROR_001")) {
// this will push a new route and remove all the routes that were present
navigatorKey.currentState.pushNamedAndRemoveUntil(
"/login", (Route<dynamic> route) => false);
}
return dioError;
}
扩展拦截器类
我们可以创建一个拦截器类来覆盖onRequest、onError、onResponse方法。
class AppInterceptors extends Interceptor {
@override
FutureOr<dynamic> onRequest(RequestOptions options) async {
if (options.headers.containsKey("requiresToken")) {
//remove the auxiliary header
options.headers.remove("requiresToken");
SharedPreferences prefs = await SharedPreferences.getInstance();
var header = prefs.get("Header");
options.headers.addAll({"Header": "$header${DateTime.now()}"});
return options;
}
}
@override
FutureOr<dynamic> onError(DioError dioError) {
if (dioError.message.contains("ERROR_001")) {
// this will push a new route and remove all the routes that were present
navigatorKey.currentState.pushNamedAndRemoveUntil(
"/login", (Route<dynamic> route) => false);
}
return dioError;
}
@override
FutureOr<dynamic> onResponse(Response options) async {
if (options.headers.value("verifyToken") != null) {
//if the header is present, then compare it with the Shared Prefs key
SharedPreferences prefs = await SharedPreferences.getInstance();
var verifyToken = prefs.get("VerifyToken");
// if the value is the same as the header, continue with the request
if (options.headers.value("verifyToken") == verifyToken) {
return options;
}
}
return DioError(request: options.request, message: "User is no longer active");
}
}
该类可以被轻松的添加到dio对象的拦截器集合中:
Dio addInterceptors(Dio dio) {
dio.interceptors.add(AppInterceptors());
}
创建一个简单的缓存
import 'package:dio/dio.dart';
class CacheInterceptor extends Interceptor {
CacheInterceptor();
var _cache = new Map<Uri, Response>();
@override
onRequest(RequestOptions options) {
return options;
}
@override
onResponse(Response response) {
_cache[response.request.uri] = response;
}
@override
onError(DioError e) {
print('onError: $e');
if (e.type == DioErrorType.CONNECT_TIMEOUT || e.type == DioErrorType.DEFAULT) {
var cachedResponse = _cache[e.request.uri];
if (cachedResponse != null) {
return cachedResponse;
}
}
return e;
}
}
之后可以通过添加数据库缓存当方式来改进该缓存拦截器。
记录所有Dio相关的内容
在请求中,我们想通过控制台打印所有的请求信息以及相应信息,以方便我们调试请求中的问题。
和以前一样,我们创建一个新的Interceptor类,实现所有必要的方法,并从请求中记录我们想要的所有信息,此外,我们希望我们的人日志打印的是格式化好的信息:
# REQUEST:
--> GET https://jsonplaceholder.typicode.com/todos/1
...
--> END GET
# RESPONSE:
<-- 200 https://jsonplaceholder.typicode.com/todos/1
...
<-- END HTTP
- 对于Request,我们希望打印请求参数、请求体、headers、URL
- 对于Response,我们要打印出URL、Headers、body和状态码
- 对于错误,我们需要打印状态码和错误本身
class LoggingInterceptors extends Interceptor {
@override
FutureOr<dynamic> onRequest(RequestOptions options) {
print(
"--> ${options.method != null ? options.method.toUpperCase() : 'METHOD'} ${"" + (options.baseUrl ?? "") + (options.path ?? "")}");
print("Headers:");
options.headers.forEach((k, v) => print('$k: $v'));
if (options.queryParameters != null) {
print("queryParameters:");
options.queryParameters.forEach((k, v) => print('$k: $v'));
}
if (options.data != null) {
print("Body: ${options.data}");
}
print(
"--> END ${options.method != null ? options.method.toUpperCase() : 'METHOD'}");
return options;
}
@override
FutureOr<dynamic> onError(DioError dioError) {
print(
"<-- ${dioError.message} ${(dioError.response?.request != null ? (dioError.response.request.baseUrl + dioError.response.request.path) : 'URL')}");
print(
"${dioError.response != null ? dioError.response.data : 'Unknown Error'}");
print("<-- End error");
}
@override
FutureOr<dynamic> onResponse(Response response) {
print(
"<-- ${response.statusCode} ${(response.request != null ? (response.request.baseUrl + response.request.path) : 'URL')}");
print("Headers:");
response.headers?.forEach((k, v) => print('$k: $v'));
print("Response: ${response.data}");
print("<-- END HTTP");
}
}
使用JsonPlaceHolder测试得到如下输出:
—> GET https:///jsonplaceholder.typicode.com/todos/1/
Headers:
requiresToken: true
queryParameters:
—> END GET
<— 200 https:///jsonplaceholder.typicode.com/todos/1/
Headers:
connection: [keep-alive]
set-cookie: [__cfduid=dd3fb888c5f062dd954e06e2e4c1166241567679659; expires=Fri, 04-Sep-20 10:34:19 GMT; path=/; domain=.typicode.com; HttpOnly]
cache-control: [public, max-age=14400]
transfer-encoding: [chunked]
date: [Thu, 05 Sep 2019 10:34:19 GMT]
content-encoding: [gzip]
vary: [Origin, Accept-Encoding]
age: [4045]
cf-cache-status: [HIT]
expect-ct: [max-age=604800, report-uri=“https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct”]
content-type: [application/json; charset=utf-8]
pragma: [no-cache]
server: [cloudflare]
x-powered-by: [Express]
access-control-allow-credentials: [true]
cf-ray: [51178cd29f9b724b-AMS]
etag: [W/“53-hfEnumeNh6YirfjyjaujcOPPT+s”]
via: [1.1 vegur]
x-content-type-options: [nosniff]
expires: [Thu, 05 Sep 2019 14:34:19 GMT]
Response: {userId: 1, id: 1, title: delectus aut autem, completed: false}
<— END HTTP
我们目前使用的是print方法打印的日志,这样如果我们在生产环境中使用就不太合适,任何使用该App的用户只要用手机连接电脑,然后通过flutter log命令就可以看到这些所有的输出,我们可以结合debugPrint和Product Flavors一起使用,请参考debugPrint and the power of hiding and customizing your logs in Dart。