(十二)Dart網絡編程

一、Uri

Uri 類 提供了 編碼和解碼 URI(URL) 字符的功能。 這些函數處理 URI 特殊的字符,例如 &=。 Uri 類還可以解析和處理 URI 的每個部分,比如 host, port, scheme 等。

1.1.Encoding and decoding fully qualified URIs(編碼解碼URI)

要編碼和解碼除了 URI 中特殊意義(例如 /, :, &,#)的字符, 則可以使用 encodeFull()decodeFull() 函數。這兩個函數可以用來編碼和解碼整個 URI,并且保留 URI 特殊意義的字符不變。

var uri = 'http://example.org/api?foo=some message';

var encoded = Uri.encodeFull(uri);
assert(encoded ==
    'http://example.org/api?foo=some%20message');

var decoded = Uri.decodeFull(encoded);
assert(uri == decoded);

注意:上面 somemessage 之間的空格被編碼了。

1.2.Encoding and decoding URI components(編碼解碼URI組件)

使用encodeComponent()decodeComponent() 可以編碼 和解碼 URI 中的所有字符,特殊意義的字符(/, &, 和:等) 也會編碼,

var uri = 'http://example.org/api?foo=some message';

var encoded = Uri.encodeComponent(uri);
assert(encoded ==
    'http%3A%2F%2Fexample.org%2Fapi%3Ffoo%3Dsome%20message');

var decoded = Uri.decodeComponent(encoded);
assert(uri == decoded);

注意:上面特殊字符也被編碼了,比如 / 編碼為 %2F。

1.3.Parsing URIs

如果有個 Uri 對象或者 URI 字符串,使用 Uri 的屬性 可以獲取每個部分,比如 path。使用 parse() 靜態 函數可以從字符串中解析一個 Uri 對象:

var uri = Uri.parse('http://example.org:8080/foo/bar#frag');

assert(uri.scheme   == 'http');
assert(uri.host     == 'example.org');
assert(uri.path     == '/foo/bar');
assert(uri.fragment == 'frag');
assert(uri.origin   == 'http://example.org:8080');

1.4.Building URIs

使用 Uri() 構造函數可以從 URI 的 各個部分來構造一個 Uri 對象:

var uri = new Uri(scheme: 'http', host: 'example.org',
                  path: '/foo/bar', fragment: 'frag');
assert(uri.toString() ==
    'http://example.org/foo/bar#frag');

更多信息參考 Uri API 文檔 。

二、dart原生網絡請求API

原生API使用的是用dart:io中的HttpClient發起的請求,但HttpClient本身功能較弱,很多常用功能都不支持。

HTTP API 在返回值中使用了Dart Futures。 建議使用async/await語法來調用API。

網絡調用通常遵循如下步驟:

  • 1、創建 client.
  • 2、構造 Uri.
  • 3、發起請求, 等待請求,同時您也可以配置請求headers、 body。
  • 4、關閉請求, 等待響應.
  • 5、解碼響應的內容.

以下示例對HTTPS GET請求返回的JSON數據進行解碼:

import 'dart:async';
import 'dart:io';
import 'dart:convert'; // 使用dart:convert內置庫可以簡單解碼和編碼JSON



_getIPAddress() async {
    var url = 'https://httpbin.org/ip';
    // 1.創建 client.
    var httpClient = new HttpClient();
    // 2.構造 Uri.
    var uri = Uri.parse(url);

    String result;
    try {
      // 3.發起請求, 等待請求,同時您也可以配置請求headers、 body。
      var request = await httpClient.getUrl(uri);
      // 4.關閉請求, 等待響應.
      var response = await request.close();
      if (response.statusCode == HttpStatus.ok) {
       // 5.解碼響應的內容.
        var json = await response.transform(utf8.decoder).join();
        var data = jsonDecode(json);
        result = data['origin'];
      } else {
        result =
        'Error getting IP address:\nHttp status ${response.statusCode}';
      }
    } catch (exception) {
      result = 'Failed getting IP address';
    }
    print(result); // 14.147.104.242, 14.147.104.242

三、使用第三方庫http

http包含一組高級函數和類,可以方便地使用HTTP資源。它與平臺無關,可以在命令行和瀏覽器上使用。

3.1.庫的安裝

  • 1、在pubspec.yaml文件添加這個包的依賴:
dependencies:
  http: ^0.12.0+2
  • 2、通過命令行pub命令安裝包
pub get

如果你裝了Flutter插件也可以用以下令(也可通過用戶交互界面安裝,如Visual Studio Code、Android Studio裝了Flutter插件):

flutter packages get

3.2.庫的使用

使用這個庫最簡單的方法是通過頂級函數。它們允許你以最便捷的方式發出單獨的HTTP請求:

import 'package:http/http.dart' as http;

var url = 'http://example.com/whatsit/create';
var response = await http.post(url, body: {'name': 'doodle', 'color': 'blue'});
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');

print(await http.read('http://example.com/foobar.txt'));

如果向同一服務器發出多個請求,可以使用Client而不是一次性請求來保持打開持久連接。如果您這樣做,請確保在完成時關閉client。

var client = new http.Client();
try {
  var uriResponse = await client.post('http://example.com/whatsit/create',
      body: {'name': 'doodle', 'color': 'blue'});
  print(await client.get(uriResponse.bodyFields['uri']));
} finally {
  client.close();
}

您還可以通過自己創建RequestStreamedRequest 對象并將其傳遞給Client.send來對請求和響應施加更詳細的控制。

這個包被設計成可組合的。這使得外部庫可以很容易地相互協作,向其添加行為。希望添加行為的庫應該創建BaseClient的子類,該類包裝另一個Client并添加所需的行為:

class UserAgentClient extends http.BaseClient {
  final String userAgent;
  final http.Client _inner;

  UserAgentClient(this.userAgent, this._inner);

  Future<StreamedResponse> send(BaseRequest request) {
    request.headers['user-agent'] = userAgent;
    return _inner.send(request);
  }
}

四、使用第三方庫dio(推薦)

dio是一個強大的Dart Http請求庫,支持Restful API、FormData、攔截器、請求取消、Cookie管理、文件上傳/下載、超時、自定義適配器等...

4.1.添加依賴

  • 1、在pubspec.yaml文件添加這個包的依賴:
dependencies:
  dio: ^2.1.5  // 請使用pub上2.1分支的最新版本
  • 2、通過命令行pub命令安裝包
pub get

如果你裝了Flutter插件也可以用以下令(也可通過用戶交互界面安裝,如Visual Studio Code、Android Studio裝了Flutter插件):

flutter packages get

4.2.示例

  • 一個極簡的示例
import 'package:dio/dio.dart';
void getHttp() async {
  try {
    Response response = await Dio().get("http://www.baidu.com");
    print(response);
  } catch (e) {
    print(e);
  }
}
  • 發起一個 GET 請求 :
Response response;
Dio dio = new Dio();
response = await dio.get("/test?id=12&name=wendu")
print(response.data.toString());
// 請求參數也可以通過對象傳遞,上面的代碼等同于:
response = await dio.get("/test", queryParameters: {"id": 12, "name": "wendu"});
print(response.data.toString());
  • 發起一個 POST 請求:
response = await dio.post("/test", data: {"id": 12, "name": "wendu"});
  • 發起多個并發請求:
response = await Future.wait([dio.post("/info"), dio.get("/token")]);
  • 下載文件:
response = await dio.download("https://www.google.com/", "./xx.html");
  • 以流的方式接收響應數據:
Response<ResponseBody> rs = await Dio().get<ResponseBody>(url,
  options: Options(responseType: ResponseType.stream), //設置接收類型為stream
);
print(rs.data.stream); //響應流
  • 以二進制數組的方式接收響應數據:
Response<List<int>> rs = await Dio().get<List<int>>(url,
 options: Options(responseType: ResponseType.bytes), //設置接收類型為bytes
);
print(rs.data); //二進制數組
  • 發送 FormData:
FormData formData = new FormData.from({
    "name": "wendux",
    "age": 25,
  });
response = await dio.post("/info", data: formData);
  • 通過FormData上傳多個文件:
FormData formData = new FormData.from({
    "name": "wendux",
    "age": 25,
    "file1": new UploadFileInfo(new File("./upload.txt"), "upload1.txt"),
    //支持直接上傳字節數組 (List<int>) ,方便直接上傳內存中的內容
    "file2": new UploadFileInfo.fromBytes(
        utf8.encode("hello world"), "word.txt"),
    // 支持文件數組上傳
    "files": [
        new UploadFileInfo(new File("./example/upload.txt"), "upload.txt"),
        new UploadFileInfo(new File("./example/upload.txt"), "upload.txt")
    ]
});
response = await dio.post("/info", data: formData);
  • 監聽發送(上傳)數據進度:
response = await dio.post(
  "http://www.dtworkroom.com/doris/1/2.0.0/test",
  data: {"aa": "bb" * 22},
  onSendProgress: (int sent, int total) {
    print("$sent $total");
  },
);
  • 以流的形式提交二進制數據:
// 二進制數據
List<int> postData = <int>[...];
await dio.post(
  url,
  data: Stream.fromIterable(postData.map((e) => [e])), //創建一個Stream<List<int>>
  options: Options(
    headers: {
      HttpHeaders.contentLengthHeader: postData.length, // 設置content-length
    },
  ),
);

注意:如果要監聽提交進度,則必須設置content-length,反之則是可選的。

4.3.Dio APIs

建議:在項目中使用Dio單例,這樣便可對同一個dio實例發起的所有請求進行一些統一的配置,比如設置公共header、請求基地址、超時時間等;這里有一個在Flutter工程中使用Dio單例(定義為top level變量)的示例供開發者參考。

你可以使用默認配置或傳遞一個可選 BaseOptions參數來創建一個Dio實例 :

Dio dio = new Dio(); // 使用默認配置

// 配置dio實例
dio.options.baseUrl = "https://www.xx.com/api";
dio.options.connectTimeout = 5000; //5s
dio.options.receiveTimeout = 3000;

// 或者通過傳遞一個 `BaseOptions`來創建dio實例
BaseOptions options = new BaseOptions(
    baseUrl: "https://www.xx.com/api",
    connectTimeout: 5000,
    receiveTimeout: 3000,
);
Dio dio = new Dio(options);

Dio實例的核心API是 :

Future request(String path, {data,Map queryParameters, Options options,CancelToken cancelToken, ProgressCallback onSendProgress, ProgressCallback onReceiveProgress)

response = await request(
      "/test",
      data: {"id": 12, "name": "xx"},
      options: Options(method: "GET"),
  );

請求方法別名

為了方便使用,Dio提供了一些其它的Restful API, 這些API都是request的別名。

  • Future get(...)

  • Future post(...)

  • Future put(...)

  • Future delete(...)

  • Future head(...)

  • Future put(...)

  • Future path(...)

  • Future download(...)

4.4.請求配置

下面是所有的請求配置選項。 如果請求method沒有指定,則默認為GET :

{
  /// Http method.
  String method;

  /// 請求基地址,可以包含子路徑,如: "https://www.google.com/api/".
  String baseUrl;

  /// Http請求頭.
  Map<String, dynamic> headers;

  /// 連接服務器超時時間,單位是毫秒.
  int connectTimeout;
  /// 2.x中為接收數據的最長時限.
  int receiveTimeout;

  /// 請求路徑,如果 `path` 以 "http(s)"開始, 則 `baseURL` 會被忽略; 否則,
  /// 將會和baseUrl拼接出完整的的url.
  String path = "";

  /// 請求的Content-Type,默認值是[ContentType.JSON].
  /// 如果您想以"application/x-www-form-urlencoded"格式編碼請求數據,
  /// 可以設置此選項為 `ContentType.parse("application/x-www-form-urlencoded")`,  這樣[Dio]
  /// 就會自動編碼請求體.
  ContentType contentType;

  /// [responseType] 表示期望以那種格式(方式)接受響應數據。
  /// 目前 [ResponseType] 接受三種類型 `JSON`, `STREAM`, `PLAIN`.
  ///
  /// 默認值是 `JSON`, 當響應頭中content-type為"application/json"時,dio 會自動將響應內容轉化為json對象。
  /// 如果想以二進制方式接受響應數據,如下載一個二進制文件,那么可以使用 `STREAM`.
  ///
  /// 如果想以文本(字符串)格式接收響應數據,請使用 `PLAIN`.
  ResponseType responseType;

  /// `validateStatus` 決定http響應狀態碼是否被dio視為請求成功, 返回`validateStatus`
  ///  返回`true` , 請求結果就會按成功處理,否則會按失敗處理.
  ValidateStatus validateStatus;

  /// 用戶自定義字段,可以在 [Interceptor]、[Transformer] 和 [Response] 中取到.
  Map<String, dynamic> extra;

  /// 公共query參數
  Map<String, dynamic /*String|Iterable<String>*/ > queryParameters;
}

這里有一個完成的示例.

4.5.響應數據

當請求成功時會返回一個Response對象,它包含如下字段:

{
  /// 響應數據,可能已經被轉換了類型, 詳情請參考Options中的[ResponseType].
  var data;
  /// 響應頭
  HttpHeaders headers;
  /// 本次請求信息
  Options request;
  /// Http status code.
  int statusCode;
  /// 是否重定向
  bool isRedirect;  
  /// 重定向信息   
  List<RedirectInfo> redirects ;
  /// 最終真正的請求地址(因為可能會重定向)
  Uri realUri;   
  /// 響應對象的自定義字段(可以在攔截器中設置它),調用方可以在`then`中獲取.
  Map<String, dynamic> extra;
}

示例如下:

 Response response = await dio.get("https://www.google.com");
  print(response.data);
  print(response.headers);
  print(response.request);
  print(response.statusCode);
4.5.1.泛型支持

2.0.18版本后可以通過泛型來指定對響應數據

假如有一個url返回的是json數據,返回數據在默認情況下(options.responseType為json)會被自動轉為Json對象(Map或List)的:

Response response = await dio.get("/test");
print(response.data is Map); //true,自動轉為了map

上面的代碼在IDE里面輸入時,IDE是無法推斷出response.data的真實類型,所以對于Map的方法和屬性給不出提示,這時我們只需要指定Response的泛型參數為Map即可:

Response<Map<String,dynamic>> r = await dio.get("/test");
print(r.data.containsKey("errCode")); // IDE可以給出代碼提示

有時我們如果想以字符串方式接收json文本的話,我們可以通過制定responseTypeplain來禁止自動轉化:

Response response = await dio.get("/test", options: Options(responseType: ResponseType.plain));

現在,我們也可以通過指定泛型參數來做到這一點了:

Response response = await dio.get<String>("/test");

是不是很簡單,但是,上面的寫法有個瑕疵就是有些編輯器無法推斷出response.data的類型,所以當你對輸入response.data時,字符串的方法和屬性不會被推薦出來,要解決這個問題很簡單,我們只需要指定Response的泛型參數為String即可:

Response<String> response = await dio.get<String>("/test");

同理,如果我們在BaseOptions里設置了responseTypeResponseType.plain,那么我們需要對某一個接口返回的數據轉為Map的化,可以指定泛型參數為Map:

dio.options.responseType = ResponseType.plain;
Response<Map> r= await dio.get<Map>("/test");

注意:當responseType類型為plain或json時,泛型參數只能是String、Map和List三種類型,所有的請求內容都可以String形式接收,但只有Json數據可以轉為Map和List,所以如果泛型參數傳入Map或List時,則會強制將響應內容轉為Map或List,如果轉換失敗,則會拋出異常。

詳細示例請參見這里

4.6.攔截器

每個 Dio 實例都可以添加任意多個攔截器,通過攔截器你可以在請求之前或響應之后(但還沒有被 thencatchError處理)做一些統一的預處理操作。

dio.interceptors.add(InterceptorsWrapper(
    onRequest:(RequestOptions options){
     // 在請求被發送之前做一些事情
     return options; //continue
     // 如果你想完成請求并返回一些自定義數據,可以返回一個`Response`對象或返回`dio.resolve(data)`。
     // 這樣請求將會被終止,上層then會被調用,then中返回的數據將是你的自定義數據data.
     //
     // 如果你想終止請求并觸發一個錯誤,你可以返回一個`DioError`對象,或返回`dio.reject(errMsg)`,
     // 這樣請求將被中止并觸發異常,上層catchError會被調用。
    },
    onResponse:(Response response) {
     // 在返回響應數據之前做一些預處理
     return response; // continue
    },
    onError: (DioError e) {
      // 當請求失敗時做一些預處理
     return e;//continue
    }
));
4.6.1.完成和終止請求/響應

在所有攔截器中,你都可以改變請求執行流, 如果你想完成請求/響應并返回自定義數據,你可以返回一個 Response 對象或返回 dio.resolve(data)的結果。 如果你想終止(觸發一個錯誤,上層catchError會被調用)一個請求/響應,那么可以返回一個DioError 對象或返回 dio.reject(errMsg) 的結果.

dio.interceptors.add(InterceptorsWrapper(
  onRequest:(RequestOptions options){
   return dio.resolve("fake data")
  },
));
Response response = await dio.get("/test");
print(response.data);//"fake data"
4.6.2.攔截器中支持異步任務

攔截器中不僅支持同步任務,而且也支持異步任務, 下面是在請求攔截器中發起異步任務的一個實例:

dio.interceptors.add(InterceptorsWrapper(
    onRequest:(Options options) async{
        //...If no token, request token firstly.
        Response response = await dio.get("/token");
        //Set the token to headers
        options.headers["token"] = response.data["data"]["token"];
        return options; //continue
    }
));
4.6.3.Lock/unlock 攔截器

你可以通過調用攔截器的lock()/unlock 方法來鎖定/解鎖攔截器。一旦請求/響應攔截器被鎖定,接下來的請求/響應將會在進入請求/響應攔截器之前排隊等待,直到解鎖后,這些入隊的請求才會繼續執行(進入攔截器)。這在一些需要串行化請求/響應的場景中非常實用,后面我們將給出一個示例。

tokenDio = new Dio(); //Create a new instance to request the token.
tokenDio.options = dio;
dio.interceptors.add(InterceptorsWrapper(
    onRequest:(Options options) async {
        // If no token, request token firstly and lock this interceptor
        // to prevent other request enter this interceptor.
        dio.interceptors.requestLock.lock();
        // We use a new Dio(to avoid dead lock) instance to request token.
        Response response = await tokenDio.get("/token");
        //Set the token to headers
        options.headers["token"] = response.data["data"]["token"];
        dio.interceptors.requestLock.unlock();
        return options; //continue
    }
));

Clear()方法

你也可以調用攔截器的clear()方法來清空等待隊列。

4.6.4.別名

請求攔截器被鎖定時,接下來的請求將會暫停,這等價于鎖住了dio實例,因此,Dio示例上提供了請求攔截器lock/unlock的別名方法:

dio.lock() == dio.interceptors.requestLock.lock()

dio.unlock() == dio.interceptors.requestLock.unlock()

dio.clear() == dio.interceptors.requestLock.clear()

4.6.5.示例

假設這么一個場景:出于安全原因,我們需要給所有的請求頭中添加一個csrfToken,如果csrfToken不存在,我們先去請求csrfToken,獲取到csrfToken后,再發起后續請求。 由于請求csrfToken的過程是異步的,我們需要在請求過程中鎖定后續請求(因為它們需要csrfToken), 直到csrfToken請求成功后,再解鎖,代碼如下:

dio.interceptors.add(InterceptorsWrapper(
    onRequest: (Options options) {
        print('send request:path:${options.path},baseURL:${options.baseUrl}');
        if (csrfToken == null) {
            print("no token,request token firstly...");
            //lock the dio.
            dio.lock();
            return tokenDio.get("/token").then((d) {
                options.headers["csrfToken"] = csrfToken = d.data['data']['token'];
                print("request token succeed, value: " + d.data['data']['token']);
                print(
                    'continue to perform request:path:${options.path},baseURL:${options.path}');
                return options;
            }).whenComplete(() => dio.unlock()); // unlock the dio
        } else {
            options.headers["csrfToken"] = csrfToken;
            return options;
        }
    }
));

完整的示例代碼請點擊 這里.

4.6.6.日志

我們可以添加 LogInterceptor 攔截器來自動打印請求、響應日志, 如:

dio.interceptors.add(LogInterceptor(responseBody: false)); //開啟請求日志

由于攔截器隊列的執行順序是FIFO,如果把log攔截器添加到了最前面,則后面攔截器對options的更改就不會被打?。ǖ廊粫В?, 所以建議把log攔截添加到隊尾。

4.7.Cookie管理

我們可以通過添加CookieManager攔截器來自動管理請求/響應 cookie。CookieManager 依賴 cookieJar package

dio cookie 管理 API 是基于開源庫 cookie_jar.

你可以創建一個CookieJarPersistCookieJar 來幫您自動管理cookie, dio 默認使用 CookieJar , 它會將cookie保存在內存中。 如果您想對cookie進行持久化, 請使用 PersistCookieJar , 示例代碼如下:

var dio = new Dio();
dio.interceptors.add(CookieManager(CookieJar()))

PersistCookieJar實現了RFC中標準的cookie策略.PersistCookieJar 會將cookie保存在文件中,所以 cookies 會一直存在除非顯式調用 delete 刪除.

注意: 在Flutter中,傳給 PersistCookieJar 的路徑必須是有效的,必須是設備中存在的路徑并且路徑擁有寫權限,你可以通過 path_provider 包來獲取正確的路徑。

更多關于 cookie_jar 請參考 : https://github.com/flutterchina/cookie_jar .

4.7.1.自定義攔截器

開發者可以通過繼承Interceptor 類來實現自定義攔截器,這是一個簡單的緩存示例攔截器。

4.8.錯誤處理

當請求過程中發生錯誤時, Dio 會包裝 Error/Exception 為一個 DioError:

try {
    //404
    await dio.get("https://wendux.github.io/xsddddd");
  } on DioError catch (e) {
    // The request was made and the server responded with a status code
    // that falls out of the range of 2xx and is also not 304.
    if (e.response) {
      print(e.response.data);
      print(e.response.headers);
      print(e.response.request);
    } else {
      // Something happened in setting up or sending the request that triggered an Error
      print(e.request);
      print(e.message);
    }
  }
4.8.1.DioError 字段
{
  /// 響應信息, 如果錯誤發生在在服務器返回數據之前,它為 `null`
  Response response;

  /// 錯誤描述.
  String message;

  /// 錯誤類型,見下文
  DioErrorType type;

  ///原始的error或exception對象,通常type為DEFAULT時存在。
  dynamic error;

  /// 錯誤棧信息,可能為null
  StackTrace stackTrace;
}
4.8.2.DioErrorType
enum DioErrorType {
  /// When opening  url timeout, it occurs.
  CONNECT_TIMEOUT,

  ///  Whenever more than [receiveTimeout] (in milliseconds) passes between two events from response stream,
  ///  [Dio] will throw the [DioError] with [DioErrorType.RECEIVE_TIMEOUT].
  ///
  ///  Note: This is not the receiving time limitation.
  RECEIVE_TIMEOUT,

  /// When the server response, but with a incorrect status, such as 404, 503...
  RESPONSE,

  /// When the request is cancelled, dio will throw a error with this type.
  CANCEL,

  /// Default error type, Some other Error. In this case, you can
  /// read the DioError.error if it is not null.
  DEFAULT
}

4.9.使用application/x-www-form-urlencoded編碼

默認情況下, Dio 會將請求數據(除過String類型)序列化為 JSON. 如果想要以 application/x-www-form-urlencoded格式編碼, 你可以顯式設置contentType :

//Instance level
dio.options.contentType=ContentType.parse("application/x-www-form-urlencoded");
//or works once
dio.post("/info",data:{"id":5}, options: new Options(contentType:ContentType.parse("application/x-www-form-urlencoded")));

這里有一個示例.

4.10.FormData

Dio支持發送 FormData, 請求數據將會以 multipart/form-data方式編碼, FormData中可以一個或多個包含文件 .

FormData formData = new FormData.from({
    "name": "wendux",
    "age": 25,
    "file": new UploadFileInfo(new File("./example/upload.txt"), "upload.txt")
});
response = await dio.post("/info", data: formData);

注意: 只有 post 方法支持發送 FormData.

這里有一個完整的示例.

4.11.轉換器

轉換器Transformer 用于對請求數據和響應數據進行編解碼處理。Dio實現了一個默認轉換器DefaultTransformer作為默認的 Transformer. 如果你想對請求/響應數據進行自定義編解碼處理,可以提供自定義轉換器,通過 dio.transformer設置。

請求轉換器 Transformer.transformRequest(...) 只會被用于 'PUT'、 'POST'、 'PATCH'方法,因為只有這些方法才可以攜帶請求體(request body)。但是響應轉換器 Transformer.transformResponse() 會被用于所有請求方法的返回數據。

4.11.1.Flutter中設置

如果你在開發Flutter應用,強烈建議json的解碼通過compute方法在后臺進行,這樣可以避免在解析復雜json時導致的UI卡頓。

// 必須是頂層函數
_parseAndDecode(String response) {
  return jsonDecode(response);
}

parseJson(String text) {
  return compute(_parseAndDecode, text);
}

void main() {
  ...
  // 自定義 jsonDecodeCallback
  (dio.transformer as DefaultTransformer).jsonDecodeCallback = parseJson;
  runApp(MyApp());
}
4.11.2.其它示例

這里有一個 自定義Transformer的示例.

4.11.3.執行流

雖然在攔截器中也可以對數據進行預處理,但是轉換器主要職責是對請求/響應數據進行編解碼,之所以將轉化器單獨分離,一是為了和攔截器解耦,二是為了不修改原始請求數據(如果你在攔截器中修改請求數據(options.data),會覆蓋原始請求數據,而在某些時候您可能需要原始請求數據). Dio的請求流是:

請求攔截器 >> 請求轉換器 >> 發起請求 >> 響應轉換器 >> 響應攔截器 >> 最終結果。

這是一個自定義轉換器的示例.

4.12.HttpClientAdapter

HttpClientAdapter是 Dio 和 HttpClient之間的橋梁。2.0抽象出adapter主要是方便切換、定制底層網絡庫。Dio實現了一套標準的、強大API,而HttpClient則是真正發起Http請求的對象。我們通過HttpClientAdapter將Dio和HttpClient解耦,這樣一來便可以自由定制Http請求的底層實現,比如,在Flutter中我們可以通過自定義HttpClientAdapter將Http請求轉發到Native中,然后再由Native統一發起請求。再比如,假如有一天OKHttp提供了dart版,你想使用OKHttp發起http請求,那么你便可以通過適配器來無縫切換到OKHttp,而不用改之前的代碼。

Dio 使用DefaultHttpClientAdapter作為其默認HttpClientAdapter,DefaultHttpClientAdapter使用dart:io:HttpClient 來發起網絡請求。

這里 有一個簡單的自定義Adapter的示例,讀者可以參考。另外本項目的自動化測試用例全都是通過一個自定義的MockAdapter來模擬服務器返回數據的。

4.12.1.設置Http代理

DefaultHttpClientAdapter 提供了一個onHttpClientCreate 回調來設置底層 HttpClient的代理,我們想使用代理,可以參考下面代碼:

(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
    // config the http client
    client.findProxy = (uri) {
        //proxy all request to localhost:8888
        return "PROXY localhost:8888";
    };
    // you can also create a new HttpClient to dio
    // return new HttpClient();
};

完整的示例請查看這里.

4.12.2.Https證書校驗

有兩種方法可以校驗https證書,假設我們的后臺服務使用的是自簽名證書,證書格式是PEM格式,我們將證書的內容保存在本地字符串中,那么我們的校驗邏輯如下:

String PEM = "XXXXX"; // certificate content
(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;
    };
};

X509Certificate是證書的標準格式,包含了證書除私鑰外所有信息,讀者可以自行查閱文檔。另外,上面的示例沒有校驗host,是因為只要服務器返回的證書內容和本地的保存一致就已經能證明是我們的服務器了(而不是中間人),host驗證通常是為了防止證書和域名不匹配。

對于自簽名的證書,我們也可以將其添加到本地證書信任鏈中,這樣證書驗證時就會自動通過,而不會再走到badCertificateCallback回調中:

(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate  = (client) {
    SecurityContext sc = new SecurityContext();
    //file is the path of certificate
    sc.setTrustedCertificates(file);
    HttpClient httpClient = new HttpClient(context: sc);
    return httpClient;
};

注意,通過setTrustedCertificates()設置的證書格式必須為PEM或PKCS12,如果證書格式為PKCS12,則需將證書密碼傳入,這樣則會在代碼中暴露證書密碼,所以客戶端證書校驗不建議使用PKCS12格式的證書。

4.13.請求取消

你可以通過 cancel token 來取消發起的請求:

CancelToken token = new CancelToken();
dio.get(url, cancelToken: token)
    .catchError((DioError err){
        if (CancelToken.isCancel(err)) {
            print('Request canceled! '+ err.message)
        }else{
            // handle error.
        }
    });
// cancel the requests with "cancelled" message.
token.cancel("cancelled");

注意: 同一個cancel token 可以用于多個請求,當一個cancel token取消時,所有使用該cancel token的請求都會被取消。

完整的示例請參考取消示例.

參考資料:
https://pub.dev/packages/http
https://pub.dev/packages/dio

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容

  • 6.1 公鑰密鑰加密原理 6.1.1 基礎知識 密鑰:一般就是一個字符串或數字,在加密或者解密時傳遞給加密/解密算...
    AndroidMaster閱讀 4,028評論 1 8
  • iOS網絡編程讀書筆記 Facade Tester客戶端門面模式的實例(被動版本化) 被動版本化,所以硬編碼URL...
    melouverrr閱讀 1,619評論 3 7
  • dio是Flutter中文網開源的一個強大的Dart Http請求庫,支持Restful API、FormData...
    lazydu閱讀 69,102評論 10 56
  • API定義規范 本規范設計基于如下使用場景: 請求頻率不是非常高:如果產品的使用周期內請求頻率非常高,建議使用雙通...
    有涯逐無涯閱讀 2,575評論 0 6
  • 沙乙 稚子啟門扉,遲疑客所窺。 緣來琴人谷,劍影舞相隨。 黎夏藤瓜繞,暮冬山雪飛。 人皆京邑往,隱者武當歸。
    沙乙閱讀 414評論 0 0