在上一篇文章中,我帶你一起學習了 Dart 中異步與并發的機制及實現原理。與其他語言類似,Dart 的異步是通過事件循環與隊列實現的,我們可以使用 Future 來封裝異步任務。而另一方面,盡管 Dart 是基于單線程模型的,但也提供了 Isolate 這樣的“多線程”能力,這使得我們可以充分利用系統資源,在并發 Isolate 中搞定 CPU 密集型的任務,并通過消息機制通知主 Isolate 運行結果。
異步與并發的一個典型應用場景,就是網絡編程。一個好的移動應用,不僅需要有良好的界面和易用的交互體驗,也需要具備和外界進行信息交互的能力。而通過網絡,信息隔離的客戶端與服務端間可以建立一個雙向的通信通道,從而實現資源訪問、接口數據請求和提交、上傳下載文件等操作。
為了便于我們快速實現基于網絡通道的信息交換實時更新 App 數據,Flutter 也提供了一系列的網絡編程類庫和工具。因此在今天的分享中,我會通過一些小例子與你講述在 Flutter 應用中,如何實現與服務端的數據交互,以及如何將交互響應的數據格式化。
## Http 網絡編程
我們在通過網絡與服務端數據交互時,不可避免地需要用到三個概念:定位、傳輸與應用。
其中,**定位**,定義了如何準確地找到網絡上的一臺或者多臺主機(即 IP 地址);**傳輸**,則主要負責在找到主機后如何高效且可靠地進行數據通信(即 TCP、UDP 協議);而**應用**,則負責識別雙方通信的內容(即 HTTP 協議)。
我們在進行數據通信時,可以只使用傳輸層協議。但傳輸層傳遞的數據是二進制流,如果沒有應用層,我們無法識別數據內容。如果想要使傳輸的數據有意義,則必須要用到應用層協議。移動應用通常使用 HTTP 協議作應用層協議,來封裝 HTTP 信息。
在編程框架中,一次 HTTP 網絡調用通常可以拆解為以下步驟:
1. 創建網絡調用實例 client,設置通用請求行為(如超時時間);
2. 構造 URI,設置請求 header、body;
3. 發起請求, 等待響應;
4. 解碼響應的內容。
當然,Flutter 也不例外。在 Flutter 中,Http 網絡編程的實現方式主要分為三種:dart:io 里的 HttpClient 實現、Dart 原生 http 請求庫實現、第三方庫 dio 實現。接下來,我依次為你講解這三種方式。
### HttpClient
HttpClient 是 dart:io 庫中提供的網絡請求類,實現了基本的網絡編程功能。
接下來,我將和你分享一個實例,對照著上面提到的網絡調用步驟,來演示 HttpClient 如何使用。
在下面的代碼中,我們創建了一個 HttpClien 網絡調用實例,設置了其超時時間為 5 秒。隨后構造了 Flutter 官網的 URI,并設置了請求 Header 的 user-agent 為 Custom-UA。然后發起請求,等待 Flutter 官網響應。最后在收到響應后,打印出返回結果:
~~~
get() async {
// 創建網絡調用示例,設置通用請求行為 (超時時間)
var httpClient = HttpClient();
httpClient.idleTimeout = Duration(seconds: 5);
// 構造 URI,設置 user-agent 為 "Custom-UA"
var uri = Uri.parse("https://flutter.dev");
var request = await httpClient.getUrl(uri);
request.headers.add("user-agent", "Custom-UA");
// 發起請求,等待響應
var response = await request.close();
// 收到響應,打印結果
if (response.statusCode == HttpStatus.ok) {
print(await response.transform(utf8.decoder).join());
} else {
print('Error: \nHttp status ${response.statusCode}');
}
}
~~~
可以看到,使用 HttpClient 來發起網絡調用還是相對比較簡單的。
這里需要注意的是,由于網絡請求是異步行為,因此**在 Flutter 中,所有網絡編程框架都是以 Future 作為異步請求的包裝**,所以我們需要使用 await 與 async 進行非阻塞的等待。當然,你也可以注冊 then,以回調的方式進行相應的事件處理。
### http
HttpClient 使用方式雖然簡單,但其接口卻暴露了不少內部實現細節。比如,異步調用拆分得過細,鏈接需要調用方主動關閉,請求結果是字符串但卻需要手動解碼等。
http 是 Dart 官方提供的另一個網絡請求類,相比于 HttpClient,易用性提升了不少。同樣,我們以一個例子來介紹 http 的使用方法。
首先,我們需要將 http 加入到 pubspec 中的依賴里:
~~~
dependencies:
http: '>=0.11.3+12'
~~~
在下面的代碼中,與 HttpClient 的例子類似的,我們也是先后構造了 http 網絡調用實例和 Flutter 官網 URI,在設置 user-agent 為 Custom-UA 后,發出請求,最后打印請求結果:
~~~
httpGet() async {
// 創建網絡調用示例
var client = http.Client();
// 構造 URI
var uri = Uri.parse("https://flutter.dev");
// 設置 user-agent 為 "Custom-UA",隨后立即發出請求
http.Response response = await client.get(uri, headers : {"user-agent" : "Custom-UA"});
// 打印請求結果
if(response.statusCode == HttpStatus.ok) {
print(response.body);
} else {
print("Error: ${response.statusCode}");
}
}
~~~
可以看到,相比于 HttpClient,http 的使用方式更加簡單,僅需一次異步調用就可以實現基本的網絡通信。
### dio
HttpClient 和 http 使用方式雖然簡單,但其暴露的定制化能力都相對較弱,很多常用的功能都不支持(或者實現異常繁瑣),比如取消請求、定制攔截器、Cookie 管理等。因此對于復雜的網絡請求行為,我推薦使用目前在 Dart 社區人氣較高的第三方 dio 來發起網絡請求。
接下來,我通過幾個例子來和你介紹 dio 的使用方法。與 http 類似的,我們首先需要把 dio 加到 pubspec 中的依賴里:
~~~
dependencies:
dio: '>2.1.3'
~~~
在下面的代碼中,與前面 HttpClient 與 http 例子類似的,我們也是先后創建了 dio 網絡調用實例、創建 URI、設置 Header、發出請求,最后等待請求結果:
~~~
void getRequest() async {
// 創建網絡調用示例
Dio dio = new Dio();
// 設置 URI 及請求 user-agent 后發起請求
var response = await dio.get("https://flutter.dev", options:Options(headers: {"user-agent" : "Custom-UA"}));
// 打印請求結果
if(response.statusCode == HttpStatus.ok) {
print(response.data.toString());
} else {
print("Error: ${response.statusCode}");
}
}
~~~
> 這里需要注意的是,創建 URI、設置 Header 及發出請求的行為,都是通過 dio.get 方法實現的。這個方法的 options 參數提供了精細化控制網絡請求的能力,可以支持設置 Header、超時時間、Cookie、請求方法等。這部分內容不是今天分享的重點,如果你想深入理解的話,可以訪問其[API 文檔](https://github.com/flutterchina/dio#dio-apis)學習具體使用方法。
對于常見的上傳及下載文件需求,dio 也提供了良好的支持:文件上傳可以通過構建表單 FormData 實現,而文件下載則可以使用 download 方法搞定。
在下面的代碼中,我們通過 FormData 創建了兩個待上傳的文件,通過 post 方法發送至服務端。download 的使用方法則更為簡單,我們直接在請求參數中,把待下載的文件地址和本地文件名提供給 dio 即可。如果我們需要感知下載進度,可以增加 onReceiveProgress 回調函數:
~~~
// 使用 FormData 表單構建待上傳文件
FormData formData = FormData.from({
"file1": UploadFileInfo(File("./file1.txt"), "file1.txt"),
"file2": UploadFileInfo(File("./file2.txt"), "file1.txt"),
});
// 通過 post 方法發送至服務端
var responseY = await dio.post("https://xxx.com/upload", data: formData);
print(responseY.toString());
// 使用 download 方法下載文件
dio.download("https://xxx.com/file1", "xx1.zip");
// 增加下載進度回調函數
dio.download("https://xxx.com/file1", "xx2.zip", onReceiveProgress: (count, total) {
//do something
});
~~~
有時,我們的頁面由多個并行的請求響應結果構成,這就需要等待這些請求都返回后才能刷新界面。在 dio 中,我們可以結合 Future.wait 方法輕松實現:
~~~
// 同時發起兩個并行請求
List<Response> responseX= await Future.wait([dio.get("https://flutter.dev"),dio.get("https://pub.dev/packages/dio")]);
// 打印請求 1 響應結果
print("Response1: ${responseX[0].toString()}");
// 打印請求 2 響應結果
print("Response2: ${responseX[1].toString()}");
~~~
此外,與 Android 的 okHttp 一樣,dio 還提供了請求攔截器,通過攔截器,我們可以在請求之前,或響應之后做一些特殊的操作。比如可以為請求 option 統一增加一個 header,或是返回緩存數據,或是增加本地校驗處理等等。
在下面的例子中,我們為 dio 增加了一個攔截器。在請求發送之前,不僅為每個請求頭都加上了自定義的 user-agent,還實現了基本的 token 認證信息檢查功能。而對于本地已經緩存了請求 uri 資源的場景,我們可以直接返回緩存數據,避免再次下載:
~~~
// 增加攔截器
dio.interceptors.add(InterceptorsWrapper(
onRequest: (RequestOptions options){
// 為每個請求頭都增加 user-agent
options.headers["user-agent"] = "Custom-UA";
// 檢查是否有 token,沒有則直接報錯
if(options.headers['token'] == null) {
return dio.reject("Error: 請先登錄 ");
}
// 檢查緩存是否有數據
if(options.uri == Uri.parse('http://xxx.com/file1')) {
return dio.resolve(" 返回緩存數據 ");
}
// 放行請求
return options;
}
));
// 增加 try catch,防止請求報錯
try {
var response = await dio.get("https://xxx.com/xxx.zip");
print(response.data.toString());
}catch(e) {
print(e);
}
~~~
需要注意的是,由于網絡通信期間有可能會出現異常(比如,域名無法解析、超時等),因此我們需要使用 try-catch 來捕獲這些未知錯誤,防止程序出現異常。
除了這些基本的用法,dio 還支持請求取消、設置代理,證書校驗等功能。不過,這些高級特性不屬于本次分享的重點,故不再贅述,詳情可以參考 dio 的[GitHub 主頁](https://github.com/flutterchina/dio/blob/master/README-ZH.md)了解具體用法。
## JSON 解析
移動應用與 Web 服務器建立好了連接之后,接下來的兩個重要工作分別是:服務器如何結構化地去描述返回的通信信息,以及移動應用如何解析這些格式化的信息。
### 如何結構化地描述返回的通信信息?
在如何結構化地去表達信息上,我們需要用到 JSON。JSON 是一種輕量級的、用于表達由屬性值和字面量組成對象的數據交換語言。
一個簡單的表示學生成績的 JSON 結構,如下所示:
~~~
String jsonString = '''
{
"id":"123",
"name":" 張三 ",
"score" : 95
}
''';
~~~
需要注意的是,由于 Flutter 不支持運行時反射,因此并沒有提供像 Gson、Mantle 這樣自動解析 JSON 的庫來降低解析成本。在 Flutter 中,JSON 解析完全是手動的,開發者要做的事情多了一些,但使用起來倒也相對靈活。
接下來,我們就看看 Flutter 應用是如何解析這些格式化的信息。
### 如何解析格式化的信息?
所謂手動解析,是指使用 dart:convert 庫中內置的 JSON 解碼器,將 JSON 字符串解析成自定義對象的過程。使用這種方式,我們需要先將 JSON 字符串傳遞給 JSON.decode 方法解析成一個 Map,然后把這個 Map 傳給自定義的類,進行相關屬性的賦值。
以上面表示學生成績的 JSON 結構為例,我來和你演示手動解析的使用方法。
首先,我們根據 JSON 結構定義 Student 類,并創建一個工廠類,來處理 Student 類屬性成員與 JSON 字典對象的值之間的映射關系:
~~~
class Student{
// 屬性 id,名字與成績
String id;
String name;
int score;
// 構造方法
Student({
this.id,
this.name,
this.score
});
//JSON 解析工廠類,使用字典數據為對象初始化賦值
factory Student.fromJson(Map<String, dynamic> parsedJson){
return Student(
id: parsedJson['id'],
name : parsedJson['name'],
score : parsedJson ['score']
);
}
}
~~~
數據解析類創建好了,剩下的事情就相對簡單了,我們只需要把 JSON 文本通過 JSON.decode 方法轉換成 Map,然后把它交給 Student 的工廠類 fromJson 方法,即可完成 Student 對象的解析:
~~~
loadStudent() {
//jsonString 為 JSON 文本
final jsonResponse = json.decode(jsonString);
Student student = Student.fromJson(jsonResponse);
print(student.name);
}
~~~
在上面的例子中,JSON 文本所有的屬性都是基本類型,因此我們直接從 JSON 字典取出相應的元素為對象賦值即可。而如果 JSON 下面還有嵌套對象屬性,比如下面的例子中,Student 還有一個 teacher 的屬性,我們又該如何解析呢?
~~~
String jsonString = '''
{
"id":"123",
"name":" 張三 ",
"score" : 95,
"teacher": {
"name": " 李四 ",
"age" : 40
}
}
''';
~~~
這里,teacher 不再是一個基本類型,而是一個對象。面對這種情況,我們需要為每一個非基本類型屬性創建一個解析類。與 Student 類似,我們也需要為它的屬性 teacher 創建一個解析類 Teacher:
~~~
class Teacher {
//Teacher 的名字與年齡
String name;
int age;
// 構造方法
Teacher({this.name,this.age});
//JSON 解析工廠類,使用字典數據為對象初始化賦值
factory Teacher.fromJson(Map<String, dynamic> parsedJson){
return Teacher(
name : parsedJson['name'],
age : parsedJson ['age']
);
}
}
~~~
然后,我們只需要在 Student 類中,增加 teacher 屬性及對應的 JSON 映射規則即可:
~~~
class Student{
...
// 增加 teacher 屬性
Teacher teacher;
// 構造函數增加 teacher
Student({
...
this.teacher
});
factory Student.fromJson(Map<String, dynamic> parsedJson){
return Student(
...
// 增加映射規則
teacher: Teacher.fromJson(parsedJson ['teacher'])
);
}
}
~~~
完成了 teacher 屬性的映射規則添加之后,我們就可以繼續使用 Student 來解析上述的 JSON 文本了:
~~~
final jsonResponse = json.decode(jsonString);// 將字符串解碼成 Map 對象
Student student = Student.fromJson(jsonResponse);// 手動解析
print(student.teacher.name);
~~~
可以看到,通過這種方法,無論對象有多復雜的非基本類型屬性,我們都可以創建對應的解析類進行處理。
不過到現在為止,我們的 JSON 數據解析還是在主 Isolate 中完成。如果 JSON 的數據格式比較復雜,數據量又大,這種解析方式可能會造成短期 UI 無法響應。對于這類 CPU 密集型的操作,我們可以使用上一篇文章中提到的 compute 函數,將解析工作放到新的 Isolate 中完成:
~~~
static Student parseStudent(String content) {
final jsonResponse = json.decode(content);
Student student = Student.fromJson(jsonResponse);
return student;
}
doSth() {
...
// 用 compute 函數將 json 解析放到新 Isolate
compute(parseStudent,jsonString).then((student)=>print(student.teacher.name));
}
~~~
通過 compute 的改造,我們就不用擔心 JSON 解析時間過長阻塞 UI 響應了。
## 總結
好了,今天的分享就到這里了,我們簡單回顧一下主要內容。
首先,我帶你學習了實現 Flutter 應用與服務端通信的三種方式,即 HttpClient、http 與 dio。其中 dio 提供的功能更為強大,可以支持請求攔截、文件上傳下載、請求合并等高級能力。因此,我推薦你在實際項目中使用 dio 的方式。
然后,我和你分享了 JSON 解析的相關內容。JSON 解析在 Flutter 中相對比較簡單,但由于不支持反射,所以我們只能手動解析,即:先將 JSON 字符串轉換成 Map,然后再把這個 Map 給到自定義類,進行相關屬性的賦值。
如果你有原生 Android、iOS 開發經驗的話,可能會覺得 Flutter 提供的 JSON 手動解析方案并不好用。在 Flutter 中,沒有像原生開發那樣提供了 Gson 或 Mantle 等庫,用于將 JSON 字符串直接轉換為對應的實體類。而這些能力無一例外都需要用到運行時反射,這是 Flutter 從設計之初就不支持的,理由如下:
1. 運行時反射破壞了類的封裝性和安全性,會帶來安全風險。就在前段時間,Fastjson 框架就爆出了一個巨大的安全漏洞。這個漏洞使得精心構造的字符串文本,可以在反序列化時讓服務器執行任意代碼,直接導致業務機器被遠程控制、內網滲透、竊取敏感信息等操作。
2. 運行時反射會增加二進制文件大小。因為搞不清楚哪些代碼可能會在運行時用到,因此使用反射后,會默認使用所有代碼構建應用程序,這就導致編譯器無法優化編譯期間未使用的代碼,應用安裝包體積無法進一步壓縮,這對于自帶 Dart 虛擬機的 Flutter 應用程序是難以接受的。
反射給開發者編程帶來了方便,但也帶來了很多難以解決的新問題,因此 Flutter 并不支持反射。而我們要做的就是,老老實實地手動解析 JSON 吧。
我把今天分享所涉及到的知識點打包到了[GitHub](https://github.com/cyndibaby905/24_network_demo)中,你可以下載下來,反復運行幾次,加深理解與記憶。
## 思考題
最后,我給你留兩道思考題吧。
1. 請使用 dio 實現一個自定義攔截器,攔截器內檢查 header 中的 token:如果沒有 token,需要暫停本次請求,同時訪問"[http://xxxx.com/token](http://xxxx.com/token)",在獲取新 token 后繼續本次請求。
2. 為以下 Student JSON 寫相應的解析類:
~~~
String jsonString = '''
{
"id":"123",
"name":" 張三 ",
"score" : 95,
"teachers": [
{
"name": " 李四 ",
"age" : 40
},
{
"name": " 王五 ",
"age" : 45
}
]
}
''';
~~~
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略