在實戰(zhàn)中,后臺接口往往會返回一些結(jié)構(gòu)化數(shù)據(jù),如 JSON、XML 等,如之前我們請求 Github API 的示例,它返回的數(shù)據(jù)就是 JSON 格式的字符串,為了方便我們在代碼中操作 JSON,我們先將 JSON 格式的字符串轉(zhuǎn)為 Dart 對象,這個可以通過dart:convert
中內(nèi)置的 JSON 解碼器 json.decode() 來實現(xiàn),該方法可以根據(jù) JSON 字符串具體內(nèi)容將其轉(zhuǎn)為 List 或 Map,這樣我們就可以通過他們來查找所需的值,如:
//一個JSON格式的用戶列表字符串
String jsonStr='[{"name":"Jack"},{"name":"Rose"}]';
//將JSON字符串轉(zhuǎn)為Dart對象(此處是List)
List items=json.decode(jsonStr);
//輸出第一個用戶的姓名
print(items[0]["name"]);
通過 json.decode() 將 JSON 字符串轉(zhuǎn)為 List/Map 的方法比較簡單,它沒有外部依賴或其它的設(shè)置,對于小項目很方便。但當(dāng)項目變大時,這種手動編寫序列化邏輯可能變得難以管理且容易出錯,例如有如下 JSON:
{
"name": "John Smith",
"email": "john@example.com"
}
我們可以通過調(diào)用json.decode
方法來解碼 JSON ,使用 JSON 字符串作為參數(shù):
Map<String, dynamic> user = json.decode(json);
print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');
由于json.decode()
僅返回一個Map<String, dynamic>
,這意味著直到運行時我們才知道值的類型。 通過這種方法,我們失去了大部分靜態(tài)類型語言特性:類型安全、自動補(bǔ)全和最重要的編譯時異常。這樣一來,我們的代碼可能會變得非常容易出錯。例如,當(dāng)我們訪問name
或email
字段時,我們輸入的很快,導(dǎo)致字段名打錯了。但由于這個 JSON 在 map 結(jié)構(gòu)中,所以編譯器不知道這個錯誤的字段名,所以編譯時不會報錯。
其實,這個問題在很多平臺上都會遇到,而也早就有了好的解決方法即“Json Model 化”,具體做法就是,通過預(yù)定義一些與 Json 結(jié)構(gòu)對應(yīng)的 Model 類,然后在請求到數(shù)據(jù)后再動態(tài)根據(jù)數(shù)據(jù)創(chuàng)建出 Model 類的實例。這樣一來,在開發(fā)階段我們使用的是 Model 類的實例,而不再是 Map/List,這樣訪問內(nèi)部屬性時就不會發(fā)生拼寫錯誤。例如,我們可以通過引入一個簡單的模型類(Model class)來解決前面提到的問題,我們稱之為User
。在 User 類內(nèi)部,我們有:
User.fromJson
構(gòu)造函數(shù), 用于從一個 map 構(gòu)造出一個 User
實例 map structuretoJson
方法, 將 User
實例轉(zhuǎn)化為一個 map.
這樣,調(diào)用代碼現(xiàn)在可以具有類型安全、自動補(bǔ)全字段(name 和 email)以及編譯時異常。如果我們將拼寫錯誤字段視為int
類型而不是String
, 那么我們的代碼就不會通過編譯,而不是在運行時崩潰。
user.dart
class User {
final String name;
final String email;
User(this.name, this.email);
User.fromJson(Map<String, dynamic> json)
: name = json['name'],
email = json['email'];
Map<String, dynamic> toJson() =>
<String, dynamic>{
'name': name,
'email': email,
};
}
現(xiàn)在,序列化邏輯移到了模型本身內(nèi)部。采用這種新方法,我們可以非常容易地反序列化 user.
Map userMap = json.decode(json);
var user = new User.fromJson(userMap);
print('Howdy, ${user.name}!');
print('We sent the verification link to ${user.email}.');
要序列化一個 user,我們只是將該User
對象傳遞給該json.encode
方法。我們不需要手動調(diào)用toJson
這個方法,因為`JSON.encode 內(nèi)部會自動調(diào)用。
String json = json.encode(user);
這樣,調(diào)用代碼就不用擔(dān)心 JSON 序列化了,但是,Model 類還是必須的。在實踐中,User.fromJson
和User.toJson
方法都需要單元測試到位,以驗證正確的行為。
另外,實際場景中,JSON 對象很少會這么簡單,嵌套的 JSON 對象并不罕見,如果有什么能為我們自動處理 JSON 序列化,那將會非常好。幸運的是,有!
盡管還有其他庫可用,但在本書中,我們介紹一下官方推薦的json_serializable package (opens new window)包。 它是一個自動化的源代碼生成器,可以在開發(fā)階段為我們生成 JSON 序列化模板,這樣一來,由于序列化代碼不再由我們手寫和維護(hù),我們將運行時產(chǎn)生 JSON 序列化異常的風(fēng)險降至最低。
要包含json_serializable
到我們的項目中,我們需要一個常規(guī)和兩個開發(fā)依賴項。簡而言之,開發(fā)依賴項是不包含在我們的應(yīng)用程序源代碼中的依賴項,它是開發(fā)過程中的一些輔助工具、腳本,和 node 中的開發(fā)依賴項相似。
pubspec.yaml
dependencies:
# Your other regular dependencies here
json_annotation: ^2.0.0
dev_dependencies:
# Your other dev_dependencies here
build_runner: ^1.0.0
json_serializable: ^2.0.0
在您的項目根文件夾中運行 flutter packages get
(或者在編輯器中點擊 “Packages Get”) 以在項目中使用這些新的依賴項.
讓我們看看如何將我們的User
類轉(zhuǎn)換為一個json_serializable
。為了簡單起見,我們使用前面示例中的簡化 JSON model。
user.dart
import 'package:json_annotation/json_annotation.dart';
// user.g.dart 將在我們運行生成命令后自動生成
part 'user.g.dart';
///這個標(biāo)注是告訴生成器,這個類是需要生成Model類的
@JsonSerializable()
class User{
User(this.name, this.email);
String name;
String email;
//不同的類使用不同的mixin即可
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
有了上面的設(shè)置,源碼生成器將生成用于序列化name
和email
字段的 JSON 代碼。
如果需要,自定義命名策略也很容易。例如,如果我們正在使用的 API 返回帶有 _snakecase 的對象,但我們想在我們的模型中使用 lowerCamelCase, 那么我們可以使用 @JsonKey 標(biāo)注:
//顯式關(guān)聯(lián)JSON字段名與Model屬性的對應(yīng)關(guān)系
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;
json_serializable
第一次創(chuàng)建類時,您會看到與圖11-4類似的錯誤。
這些錯誤是完全正常的,這是因為 Model 類的生成代碼還不存在。為了解決這個問題,我們必須運行代碼生成器來為我們生成序列化模板。有兩種運行代碼生成器的方法:
通過在我們的項目根目錄下運行:
flutter packages pub run build_runner build
這觸發(fā)了一次性構(gòu)建,我們可以在需要時為我們的 Model 生成 json 序列化代碼,它通過我們的源文件,找出需要生成 Model 類的源文件(包含@JsonSerializable 標(biāo)注的)來生成對應(yīng)的 .g.dart 文件。一個好的建議是將所有 Model 類放在一個單獨的目錄下,然后在該目錄下執(zhí)行命令。
雖然這非常方便,但如果我們不需要每次在 Model 類中進(jìn)行更改時都要手動運行構(gòu)建命令的話會更好。
使用 _watcher _可以使我們的源代碼生成的過程更加方便。它會監(jiān)視我們項目中文件的變化,并在需要時自動構(gòu)建必要的文件,我們可以通過flutter packages pub run build_runner watch
在項目根目錄下運行來啟動 watcher。只需啟動一次觀察器,然后它就會在后臺運行,這是安全的。
上面的方法有一個最大的問題就是要為每一個 json 寫模板,這是比較枯燥的。如果有一個工具可以直接根據(jù) JSON 文本生成模板,那我們就能徹底解放雙手了。筆者自己用 dart 實現(xiàn)了一個腳本,它可以自動生成模板,并直接將 JSON 轉(zhuǎn)為 Model 類,下面我們看看怎么做:
import 'package:json_annotation/json_annotation.dart';
%t
part '%s.g.dart';
@JsonSerializable()
class %s {
%s();
%s
factory %s.fromJson(Map<String,dynamic> json) => _$%sFromJson(json);
Map<String, dynamic> toJson() => _$%sToJson(this);
}
模板中的“%t”、“%s”為占位符,將在腳本運行時動態(tài)被替換為合適的導(dǎo)入頭和類名。
腳本我們通過 Dart 來寫,源碼如下:
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
const TAG="\$";
const SRC="./json"; //JSON 目錄
const DIST="lib/models/"; //輸出model目錄
void walk() { //遍歷JSON目錄生成模板
var src = new Directory(SRC);
var list = src.listSync();
var template=new File("./template.dart").readAsStringSync();
File file;
list.forEach((f) {
if (FileSystemEntity.isFileSync(f.path)) {
file = new File(f.path);
var paths=path.basename(f.path).split(".");
String name=paths.first;
if(paths.last.toLowerCase()!="json"||name.startsWith("_")) return ;
if(name.startsWith("_")) return;
//下面生成模板
var map = json.decode(file.readAsStringSync());
//為了避免重復(fù)導(dǎo)入相同的包,我們用Set來保存生成的import語句。
var set= new Set<String>();
StringBuffer attrs= new StringBuffer();
(map as Map<String, dynamic>).forEach((key, v) {
if(key.startsWith("_")) return ;
attrs.write(getType(v,set,name));
attrs.write(" ");
attrs.write(key);
attrs.writeln(";");
attrs.write(" ");
});
String className=name[0].toUpperCase()+name.substring(1);
var dist=format(template,[name,className,className,attrs.toString(),
className,className,className]);
var _import=set.join(";\r\n");
_import+=_import.isEmpty?"":";";
dist=dist.replaceFirst("%t",_import );
//將生成的模板輸出
new File("$DIST$name.dart").writeAsStringSync(dist);
}
});
}
String changeFirstChar(String str, [bool upper=true] ){
return (upper?str[0].toUpperCase():str[0].toLowerCase())+str.substring(1);
}
//將JSON類型轉(zhuǎn)為對應(yīng)的dart類型
String getType(v,Set<String> set,String current){
current=current.toLowerCase();
if(v is bool){
return "bool";
}else if(v is num){
return "num";
}else if(v is Map){
return "Map<String,dynamic>";
}else if(v is List){
return "List";
}else if(v is String){ //處理特殊標(biāo)志
if(v.startsWith("$TAG[]")){
var className=changeFirstChar(v.substring(3),false);
if(className.toLowerCase()!=current) {
set.add('import "$className.dart"');
}
return "List<${changeFirstChar(className)}>";
}else if(v.startsWith(TAG)){
var fileName=changeFirstChar(v.substring(1),false);
if(fileName.toLowerCase()!=current) {
set.add('import "$fileName.dart"');
}
return changeFirstChar(fileName);
}
return "String";
}else{
return "String";
}
}
//替換模板占位符
String format(String fmt, List<Object> params) {
int matchIndex = 0;
String replace(Match m) {
if (matchIndex < params.length) {
switch (m[0]) {
case "%s":
return params[matchIndex++].toString();
}
} else {
throw new Exception("Missing parameter for string format");
}
throw new Exception("Invalid format string: " + m[0].toString());
}
return fmt.replaceAllMapped("%s", replace);
}
void main(){
walk();
}
dart mo.dart
flutter packages pub run build_runner build --delete-conflicting-outputs
至此,我們的腳本寫好了,我們在根目錄下新建一個 json 目錄,然后把 user.json 移進(jìn)去,然后在 lib 目錄下創(chuàng)建一個 models 目錄,用于保存最終生成的 Model 類?,F(xiàn)在我們只需要一句命令即可生成 Model 類了:
./mo.sh
運行后,一切都將自動執(zhí)行,現(xiàn)在好多了,不是嗎?
我們定義一個 person.json 內(nèi)容修改為:
{
"name": "John Smith",
"email": "john@example.com",
"mother":{
"name": "Alice",
"email":"alice@example.com"
},
"friends":[
{
"name": "Jack",
"email":"Jack@example.com"
},
{
"name": "Nancy",
"email":"Nancy@example.com"
}
]
}
每個 Person 都有name
、email
、 mother
和friends
四個字段,由于mother
也是一個 Person,朋友是多個 Person(數(shù)組),所以我們期望生成的 Model 是下面這樣:
import 'package:json_annotation/json_annotation.dart';
part 'person.g.dart';
@JsonSerializable()
class Person {
Person();
String name;
String email;
Person mother;
List<Person> friends;
factory Person.fromJson(Map<String,dynamic> json) => _$PersonFromJson(json);
Map<String, dynamic> toJson() => _$PersonToJson(this);
}
這時,我們只需要簡單修改一下 JSON,添加一些特殊標(biāo)志,重新運行 mo.sh 即可:
{
"name": "John Smith",
"email": "john@example.com",
"mother":"$person",
"friends":"$[]person"
}
我們使用美元符“$”作為特殊標(biāo)志符(如果與內(nèi)容沖突,可以修改 mo.dart 中的TAG
常量,自定義標(biāo)志符),腳本在遇到特殊標(biāo)志符后會先把相應(yīng)字段轉(zhuǎn)為相應(yīng)的對象或?qū)ο髷?shù)組,對象數(shù)組需要在標(biāo)志符后面添加數(shù)組符“[]”,符號后面接具體的類型名,此例中是 person。其它類型同理,加入我們給 User 添加一個 Person 類型的 boss
字段:
{
"name": "John Smith",
"email": "john@example.com",
"boss":"$person"
}
重新運行 mo.sh,生成的 user.dart 如下:
import 'package:json_annotation/json_annotation.dart';
import "person.dart";
part 'user.g.dart';
@JsonSerializable()
class User {
User();
String name;
String email;
Person boss;
factory User.fromJson(Map<String,dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
可以看到,boss
字段已自動添加,并自動導(dǎo)入了“person.dart”。
如果每個項目都要構(gòu)建一個上面這樣的腳本顯然很麻煩,為此,我們將上面腳本和生成模板封裝了一個包,已經(jīng)發(fā)布到了 Pub 上,包名為 Json_model (opens new window),開發(fā)者把該包加入開發(fā)依賴后,便可以用一條命令,根據(jù) Json 文件生成 Dart 類。另外 Json_model (opens new window)處于迭代中,功能會逐漸完善,所以建議讀者直接使用該包(而不是手動復(fù)制上面的代碼)。
目前 Android Studio(或IntelliJ)有幾個插件,可以將 json 文件轉(zhuǎn)成 Model 類,但插件質(zhì)量參差不齊,甚至還有一些沾染上了抄襲風(fēng)波,故筆者在此不做優(yōu)先推薦,讀者有興趣可以自行了解。但是,我們還是要了解一下 IDE 插件和 Json_model (opens new window)的優(yōu)劣:
很多人可能會問 Flutter 中有沒有像 Java 開發(fā)中的 Gson/Jackson 一樣的 Json 序列化類庫?答案是沒有!因為這樣的庫需要使用運行時反射,這在 Flutter 中是禁用的。運行時反射會干擾 Dart 的 tree shaking,使用 tree shaking,可以在 release 版中“去除”未使用的代碼,這可以顯著優(yōu)化應(yīng)用程序的大小。由于反射會默認(rèn)應(yīng)用到所有代碼,因此_ tree shaking _會很難工作,因為在啟用反射時很難知道哪些代碼未被使用,因此冗余代碼很難剝離,所以 Flutter 中禁用了 Dart 的反射功能,而正因如此也就無法實現(xiàn)動態(tài)轉(zhuǎn)化 Model 的功能。
更多建議: