Flutter 中的資料的獲取
APP 中的資料來源大致分為兩部分,一是本地,包括 檔案、資料庫;二是通過網路從遠處獲取,今天一起來學習Flutter 如何獲取這兩類資料的。
Dark 單執行緒如何實現非同步
在學習本地資料獲取和網路資料獲取之前,先了解Flutter 中如何處理完成非同步的處理的。
非同步:和同步相對,不等任務執行完,直接執行下一個任務。
首先我們瞭解到 Dark 是單執行緒,單執行緒是如何執行非同步呢?
因為App 絕大多數時間都在等待。比如,等使用者點選、等網路請求返回、等檔案 IO 結果,等等。而這些等待行為並不是阻塞的。比如說,網路請求,Socket 本身提供了 select 模型可以非同步查詢;而檔案 IO,作業系統也提供了基於事件的回撥機制。所以,基於這些特點,單執行緒模型可以在等待的過程中做別的事情,等真正需要響應結果了,再去做對應的處理
。因為等待過程並不是阻塞的,所以給我們的感覺就像是同時在做多件事情一樣。但其實始終只有一個執行緒在處理你的事情。
和大多數語言一樣,完成非同步任務也是Event Loop來完成。把 需要在主執行緒響應的事件放入佇列中,然後再主執行緒空閒的時候,不斷的從 佇列中取出訊息,然後在主執行緒完成。
在Dark 中,其實有兩個佇列,一個是事件佇列(Event Queue)、一個是微任務佇列(Microtask Queue);微任務佇列的優先順序要比 事件佇列要高,所以每次都會先檢查 微任務佇列,有就優先執行,沒有了才從 事件佇列取出事件進行處理。
瞭解了Dark 的非同步處理機制後,看看在程式碼中如何實現的。
Dart 為 Event Queue 的任務建立提供了一層封裝,叫作 Future。從名字上也很容易理解,它表示一個在未來時間才會完成的任務。把一個函式體放到了 Future 中就完成了從同步到非同步的轉換。
void syncDemo1() {
Future(() => print("print future1"));//a
print("print 1");//b
Future(() => print("print 2"))//c
..then((value) => print("print 3"))//d
..then((value) => print("print 4"));//e
}
複製程式碼
print 1
print future1
print 2
print 3
print 4
複製程式碼
呼叫的順序是:
- a 新增到任務佇列;
- b 在主執行緒,執行執行;
- c 新增到任務佇列;這時候在主執行緒沒有任務了,從任務佇列取出 a,並執行
- 執行a 後,主執行緒沒有任務,還是從任務佇列取出c 執行,then 後面的函式會依次同步執行;
非同步函式
對於一個非同步函式來說,其返回時內部執行動作並未結束,因此需要返回一個 Future 物件,供呼叫者使用。呼叫者根據 Future 物件,來決定:
- 是在這個 Future 物件上註冊一個 then,等 Future 的執行體結束了以後再進行非同步處理;
- 還是一直同步等待 Future 執行體結束。對於非同步函式返回的 Future 物件,如果呼叫者決定同步等待,則需要在呼叫處使用 await 關鍵字,並且在呼叫處的函式體使用 async 關鍵字。
void syncFunDemo() async{
Future<String> future() => Future<String>.delayed(Duration(seconds: 3),()=>"hello 2021");
// future.then((value) => print("獲取到非同步資料$value"));//等待完成後,執行then裡面的函式
print("獲取到非同步資料"+ (await future()));//同步等待
}
複製程式碼
為什麼要加上 async?
因為 Dart 中的 await 並不是阻塞等待,而是非同步等待。Dart 會將呼叫體的函式也視作非同步函式,將等待語句的上下文放入 Event Queue 中,一旦有了結果,Event Loop 就會把它從 Event Queue 中取出,等待程式碼繼續執行。
Flutter 中的網路請求
在瞭解了非同步操作之後,趁熱打鐵,學習flutter 中的網路請求。
網路與服務端資料互動時,不可避免地需要用到三個概念:定位、傳輸與應用。
其中,定位,定義瞭如何準確地找到網路上的一臺或者多臺主機(即 IP 地址);傳輸,則主要負責在找到主機後如何高效且可靠地進行資料通訊(即 TCP、UDP 協議);而應用,則負責識別雙方通訊的內容(即 HTTP 協議)。
一般的網路請求框架中,依次http 網路呼叫 可以分為以下幾個部分:
- 建立網路呼叫例項 client,設定通用請求行為(如超時時間);
- 構造 URI,設定請求 header、body;
- 發起請求, 等待響應;
- 解碼響應的內容。
在 Flutter 中,Http 網路程式設計的實現方式主要分為三種:dart:io 裡的 HttpClient 實現、Dart 原生 http 請求庫實現、第三方庫 dio 實現。
HttpClient
Dart 原生 http 請求庫實現
第三方庫 dio 實現
HttpClient 和 http 使用方式雖然簡單,但其暴露的定製化能力都相對較弱,很多常用的功能都不支援(或者實現異常繁瑣),比如取消請求、定製攔截器、Cookie 管理等。因此對於複雜的網路請求行為,我推薦使用目前在 Dart 社群人氣較高的第三方 dio 來發起網路請求。
- 加入dio 依賴
dio: 3.0.10
複製程式碼
- 簡單的get 請求
void getRequest() async {
//建立網路呼叫示例
Dio dio = Dio();
//設定URI及請求user-agent後發起請求
var response = await dio.get("https://wanandroid.com/wxarticle/chapters/json",
options: Options(headers: {"user-agent": "Custom-UA"}));
//列印請求結果
if (response.statusCode == HttpStatus.ok) {
print(response.data.toString());
} else {
print("Error: ${response.statusCode}");
}
}
複製程式碼
- 同時發起多個請求
// 同時發起多個請求
void getRequest2() async {
//建立網路呼叫示例
Dio dio = Dio();
//同時發起兩個並行請求
List<Response> responseX = await Future.wait([
dio.get("https://wanandroid.com/wxarticle/chapters/json"),
dio.get("https://www.wanandroid.com/article/list/0/json")
]);
//列印請求1響應結果
print("Response1: ${responseX[0].toString()}");
//列印請求2響應結果
print("Response2: ${responseX[1].toString()}");
}
複製程式碼
- 新增攔截器
// 新增攔截器
void getRequest3() async {
//建立網路呼叫示例
Dio dio = Dio();
//增加攔截器
dio.interceptors.add(InterceptorsWrapper(onRequest: (RequestOptions options) {
//為每個請求頭都增加user-agent
options.headers["user-agent"] = "Custom-UA";
print("interceptor request ${options.uri}");
print("interceptor request ${options.headers}");
print("interceptor request ${options.data}");
//放行請求
return options;
}, onResponse: (Response response) {
print("interceptor response ${response.data.toString()}");
}));
//增加try catch,防止請求報錯
try {
await dio.get("https://wanandroid.com/wxarticle/chapters/json");
} catch (e) {
print(e);
}
}
複製程式碼
以上都是dio 簡單的網路請求示例,更多的高階用法,可以到 GitHub 去看看。
Json 解析
在完成了網路請求後,得到的資料我們還不能直接使用,要先解析伺服器給我們返回的資料。而json 是常用的服務端和客戶端傳輸一種資料格式。
在拿到服務端返回的json 資料後,如何解析呢?
由於 Flutter 不支援執行時反射,因此並沒有提供像 Gson、Mantle 這樣自動解析 JSON 的庫來降低解析成本。在 Flutter 中,JSON 解析完全是手動的,開發者要做的事情多了一些,但使用起來倒也相對靈活。
自動解析:使用 dart:convert 庫中內建的 JSON 解碼器,將 JSON 字串解析成自定義物件的過程。使用這種方式,我們需要先將 JSON 字串傳遞給 JSON.decode 方法解析成一個 Map,然後把這個 Map 傳給自定義的類,進行相關屬性的賦值。
- json 解析
class Student{
//屬性id,名字與成績
String id;
String name;
int score;
//構造方法
Student({
this.id,
this.name,
this.score
});
//JSON解析工廠類,把map 解析成model 物件
factory Student.fromJson(Map<String, dynamic> parsedJson){
return Student(
id: parsedJson['id'],
name : parsedJson['name'],
score : parsedJson ['score']
);
}
}
void parseJson1(){
var jsonString = """{ "id":"1234", "name":"周結", "score" : 95}""" ;
//jsonString為JSON文字
final jsonResponse = json.decode(jsonString);
Student student = Student.fromJson(jsonResponse);
print(student.name);
}
複製程式碼
json 資料解析生成類中,都有一個 json 解析工廠;要是裡面巢狀 比較多層,估計解析起來也挺累的,這樣的事情交給外掛就可以了。
在 Android studio plugins 中搜尋 FlutterJsonBeanFactory
安裝該外掛,然後重啟studio。
/// 使用 FlutterJsonBeanFactory外掛實現 json轉model
void parseJson2() {
var jsonString = """{ "id":"123", "name":"張三", "score" : 95}""";
//jsonString為JSON文字
final jsonResponse = json.decode(jsonString);
UserEntity student = JsonConvert.fromJsonAsT(jsonResponse);
print(student.name);
}
複製程式碼
FlutterJsonBeanFactory 外掛幫我們做了好多事情,讓我們更能專注於開發。
Flutter 中本地資料管理
通過上面的學習,我們瞭解了在dart 中的網路請求和json 解析,完成了從遠端獲取資料的學習,接下來看看 flutter 如何處理本地資料的。
Flutter 提供了三種資料持久化方法,即檔案、SharedPreferences 與資料庫。
檔案
檔案是儲存在某種介質(比如磁碟)上指定路徑的、具有檔名的一組有序資訊的集合。從其定義看,要想以檔案的方式實現資料持久化,我們首先需要確定一件事兒:資料放在哪兒?這,就意味著要定義檔案的儲存路徑。
Flutter 提供了兩種檔案儲存的目錄,即臨時(Temporary)目錄與文件(Documents)目錄:
- 臨時目錄是作業系統可以隨時清除的目錄,通常被用來存放一些不重要的臨時快取資料。這個目錄在 iOS 上對應著 NSTemporaryDirectory 返回的值,而在 Android 上則對應著 getCacheDir 返回的值。
- 文件目錄則是隻有在刪除應用程式時才會被清除的目錄,通常被用來存放應用產生的重要資料檔案。在 iOS 上,這個目錄對應著 NSDocumentDirectory,而在 Android 上則對應著 AppData 目錄。
// 檔案的讀取,要先依賴 path_provider: ^2.0.1
class LocalDataDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("本地資料讀取"),
),
body: ListView(
children: [
ListTile(
title: Text("本地檔案讀寫"),
onTap: () => Navigator.pushNamed(context, "LocalFileDemo"),
),ListTile(
title: Text("SharePreferenceDemoDemo"),
onTap: () => Navigator.pushNamed(context, "SharePreferenceDemo"),
)
],
),
);
}
}
class LocalFileDemo extends StatelessWidget {
//建立檔案目錄
Future<File> get _localFile async {
final directory = await getApplicationDocumentsDirectory();
final path = directory.path;
return File('$path/test.txt');
}
//將字串寫入檔案
Future<File> writeContent(String content) async {
final file = await _localFile;
return file.writeAsString(content);
}
//從檔案讀出字串
Future<String> readContent() async {
try {
final file = await _localFile;
String contents = await file.readAsString();
return contents;
} catch (e) {
return "";
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("本地檔案讀寫"),
),
body: Center(
child: Column(
children: [
MaterialButton(
onPressed: () {
writeContent("hello jay 2021");
},
child: Text("寫入資料"),
),
MaterialButton(
onPressed: () async {
print("從檔案讀取到的資料:${await readContent()}");
},
child: Text("讀取資料"),
)
],
),
),
);
}
}
複製程式碼
sharePreference
使用前先新增依賴 shared_preferences: ^2.0.5
,接下來的使用就很簡單了。
// sharePreference 適合儲存資料量比較小的鍵值對,要先依賴 shared_preferences: ^2.0.5
class SharePreferenceDemo extends StatelessWidget {
String spName = "sp_name";
_spSaveString() async {
SharedPreferences.setMockInitialValues({});// 需要新增,否則會報錯 No implementation found for method getAll on channel plugins.flutter.io/shared_preferences
var sharePreference = await SharedPreferences.getInstance();
await sharePreference.setString(spName, "this is sharePreference");
}
_readSpString() async {
var sharePreference = await SharedPreferences.getInstance();
print("這是從sharePreference 讀取的資料:${sharePreference.getString(spName)}");
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("sharePreferenceDemo"),
),
body: Center(
child: Column(
children: [
MaterialButton(
onPressed: () {
_spSaveString();
},
child: Text("寫入資料"),
),
MaterialButton(
onPressed: () {
_readSpString();
},
child: Text("讀取資料"),
)
],
),
),
);
}
}
複製程式碼
當然 sharePreference 提供了基本資料型別的儲存,更多api 可以看看 sharePreference 的 GitHub
資料庫
除了上面兩種儲存方式外,Flutter 還提供了資料庫的儲存,適用於需要持久化大量格式化後的資料,並且這些資料還會以較高的頻率更新。與檔案和 SharedPreferences 相比,資料庫在資料讀寫上可以提供更快、更靈活的解決方案。
下面看一個小案例,瞭解資料庫的使用;
首先需要 依賴 sqflite: ^1.3.0