## Json Model
在實戰中,后臺接口往往會返回一些結構化數據,如JSON、XML等,如之前我們請求Github API的示例,它返回的數據就是JSON格式的字符串,為了方便我們在代碼中操作JSON,我們先將JSON格式的字符串轉為Dart對象,這個可以通過`dart:convert`中內置的JSON解碼器json.decode() 來實現,該方法可以根據JSON字符串具體內容將其轉為List或Map,這樣我們就可以通過他們來查找所需的值,如:
```
//一個JSON格式的用戶列表字符串
String jsonStr='[{"name":"Jack"},{"name":"Rose"}]';
//將JSON字符串轉為Dart對象(此處是List)
List items=json.decode(jsonStr);
//輸出第一個用戶的姓名
print(items[0]["name"]);
```
通過json.decode() 將JSON字符串轉為List/Map的方法比較簡單,它沒有外部依賴或其它的設置,對于小項目很方便。但當項目變大時,這種手動編寫序列化邏輯可能變得難以管理且容易出錯,例如有如下JSON:
```
{
"name": "John Smith",
"email": "john@example.com"
}
```
我們可以通過調用`json.decode`方法來解碼JSON ,使用JSON字符串作為參數:
```
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>`,這意味著直到運行時我們才知道值的類型。 通過這種方法,我們失去了大部分靜態類型語言特性:類型安全、自動補全和最重要的編譯時異常。這樣一來,我們的代碼可能會變得非常容易出錯。例如,當我們訪問`name`或`email`字段時,我們輸入的很快,導致字段名打錯了。但由于這個JSON在map結構中,所以編譯器不知道這個錯誤的字段名,所以編譯時不會報錯。
其實,這個問題在很多平臺上都會遇到,而也早就有了好的解決方法即“Json Model化”,具體做法就是,通過預定義一些與Json結構對應的Model類,然后在請求到數據后再動態根據數據創建出Model類的實例。這樣一來,在開發階段我們使用的是Model類的實例,而不再是Map/List,這樣訪問內部屬性時就不會發生拼寫錯誤。例如,我們可以通過引入一個簡單的模型類(Model class)來解決前面提到的問題,我們稱之為`User`。在User類內部,我們有:
- 一個`User.fromJson` 構造函數, 用于從一個map構造出一個 `User`實例 map structure
- 一個`toJson` 方法, 將 `User` 實例轉化為一個map.
這樣,調用代碼現在可以具有類型安全、自動補全字段(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,
};
}
```
現在,序列化邏輯移到了模型本身內部。采用這種新方法,我們可以非常容易地反序列化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`方法。我們不需要手動調用`toJson`這個方法,因為`JSON.encode內部會自動調用。
```
String json = json.encode(user);
```
這樣,調用代碼就不用擔心JSON序列化了,但是,Model類還是必須的。在實踐中,`User.fromJson`和`User.toJson`方法都需要單元測試到位,以驗證正確的行為。
另外,實際場景中,JSON對象很少會這么簡單,嵌套的JSON對象并不罕見,如果有什么能為我們自動處理JSON序列化,那將會非常好。幸運的是,有!
### 自動生成Model
盡管還有其他庫可用,但在本書中,我們介紹一下官方推薦的[json\_serializable package](https://pub.dartlang.org/packages/json_serializable)包。 它是一個自動化的源代碼生成器,可以在開發階段為我們生成JSON序列化模板,這樣一來,由于序列化代碼不再由我們手寫和維護,我們將運行時產生JSON序列化異常的風險降至最低。
### 在項目中設置json\_serializable
要包含`json_serializable`到我們的項目中,我們需要一個常規和兩個**開發依賴**項。簡而言之,**開發依賴項**是不包含在我們的應用程序源代碼中的依賴項,它是開發過程中的一些輔助工具、腳本,和node中的開發依賴項相似。
**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”) 以在項目中使用這些新的依賴項.
### 以json\_serializable的方式創建model類
讓我們看看如何將我們的`User`類轉換為一個`json_serializable`。為了簡單起見,我們使用前面示例中的簡化JSON model。
**user.dart**
```
import 'package:json_annotation/json_annotation.dart';
// user.g.dart 將在我們運行生成命令后自動生成
part 'user.g.dart';
///這個標注是告訴生成器,這個類是需要生成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);
}
```
有了上面的設置,源碼生成器將生成用于序列化`name`和`email`字段的JSON代碼。
如果需要,自定義命名策略也很容易。例如,如果我們正在使用的API返回帶有*snake\_case*的對象,但我們想在我們的模型中使用*lowerCamelCase*, 那么我們可以使用@JsonKey標注:
```
//顯式關聯JSON字段名與Model屬性的對應關系
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;
```
### 運行代碼生成程序
`json_serializable`第一次創建類時,您會看到與下圖類似的錯誤。

這些錯誤是完全正常的,這是因為Model類的生成代碼還不存在。為了解決這個問題,我們必須運行代碼生成器來為我們生成序列化模板。有兩種運行代碼生成器的方法:
#### 一次性生成
通過在我們的項目根目錄下運行:
```
flutter packages pub run build_runner build
```
這觸發了一次性構建,我們可以在需要時為我們的Model生成json序列化代碼,它通過我們的源文件,找出需要生成Model類的源文件(包含@JsonSerializable標注的)來生成對應的.g.dart文件。一個好的建議是將所有Model類放在一個單獨的目錄下,然后在該目錄下執行命令。
雖然這非常方便,但如果我們不需要每次在Model類中進行更改時都要手動運行構建命令的話會更好。
#### 持續生成
使用*watcher*可以使我們的源代碼生成的過程更加方便。它會監視我們項目中文件的變化,并在需要時自動構建必要的文件,我們可以通過`flutter packages pub run build_runner watch`在項目根目錄下運行來啟動*watcher*。只需啟動一次觀察器,然后它就會在后臺運行,這是安全的。
### 自動化生成模板
上面的方法有一個最大的問題就是要為每一個json寫模板,這是比較枯燥的。如果有一個工具可以直接根據JSON文本生成模板,那我們就能徹底解放雙手了。筆者自己用dart實現了一個腳本,它可以自動生成模板,并直接將JSON轉為Model類,下面我們看看怎么做:
1. 定義一個"模板的模板",名為"template.dart":
```
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”為占位符,將在腳本運行時動態被替換為合適的導入頭和類名。
2. 寫一個自動生成模板的腳本(mo.dart),它可以根據指定的JSON目錄,遍歷生成模板,在生成時我們定義一些規則:
- 如果JSON文件名以下劃線“\_”開始,則忽略此JSON文件。
- 復雜的JSON對象往往會出現嵌套,我們可以通過一個特殊標志來手動指定嵌套的對象(后面舉例)。
腳本我們通過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());
//為了避免重復導入相同的包,我們用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類型轉為對應的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){ //處理特殊標志
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();
}
```
3. 寫一個shell(mo.sh),將生成模板和生成model串起來:
```
dart mo.dart
flutter packages pub run build_runner build --delete-conflicting-outputs
```
至此,我們的腳本寫好了,我們在根目錄下新建一個json目錄,然后把user.json移進去,然后在lib目錄下創建一個models目錄,用于保存最終生成的Model類。現在我們只需要一句命令即可生成Model類了:
```
./mo.sh
```
運行后,一切都將自動執行,現在好多了,不是嗎?
#### 嵌套JSON
我們定義一個person.json內容修改為:
```
{
"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(數組),所以我們期望生成的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,添加一些特殊標志,重新運行mo.sh即可:
```
{
"name": "John Smith",
"email": "john@example.com",
"mother":"$person",
"friends":"$[]person"
}
```
我們使用美元符“$”作為特殊標志符(如果與內容沖突,可以修改mo.dart中的`TAG`常量,自定義標志符),腳本在遇到特殊標志符后會先把相應字段轉為相應的對象或對象數組,對象數組需要在標志符后面添加數組符“\[\]”,符號后面接具體的類型名,此例中是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`字段已自動添加,并自動導入了“person.dart”。
## 使用IDE插件生成model
目前Android Studio(或IntelliJ)有一個[插件](https://github.com/neverwoodsS/idea_flutter_json_format),它可以自動將Json轉為model,該插件會對嵌套Json也會生成model。這個特性在有些時候可能會引起重定義,如兩個Json都內嵌了一個user的對象時,會導致user model在不同的文件中會被定義兩次,需要開發者手動去重。
## FAQ
很多人可能會問Flutter中有沒有像Java開發中的Gson/Jackson一樣的Json序列化類庫?答案是沒有!因為這樣的庫需要使用運行時反射,這在Flutter中是禁用的。運行時反射會干擾Dart的*tree shaking*,使用*tree shaking*,可以在release版中“去除”未使用的代碼,這可以顯著優化應用程序的大小。由于反射會默認應用到所有代碼,因此*tree shaking*會很難工作,因為在啟用反射時很難知道哪些代碼未被使用,因此冗余代碼很難剝離,所以Flutter中禁用了Dart的反射功能,而正因如此也就無法實現動態轉化Model的功能。
- 緣起
- 起步
- 移動開發技術簡介
- Flutter簡介
- 搭建Flutter開發環境
- 常見配置問題
- Dart語言簡介
- 第一個Flutter應用
- 計數器示例
- 路由管理
- 包管理
- 資源管理
- 調試Flutter APP
- Dart線程模型及異常捕獲
- 基礎Widgets
- Widget簡介
- 文本、字體樣式
- 按鈕
- 圖片和Icon
- 單選框和復選框
- 輸入框和表單
- 布局類Widgets
- 布局類Widgets簡介
- 線性布局Row、Column
- 彈性布局Flex
- 流式布局Wrap、Flow
- 層疊布局Stack、Positioned
- 容器類Widgets
- Padding
- 布局限制類容器ConstrainedBox、SizeBox
- 裝飾容器DecoratedBox
- 變換Transform
- Container容器
- Scaffold、TabBar、底部導航
- 可滾動Widgets
- 可滾動Widgets簡介
- SingleChildScrollView
- ListView
- GridView
- CustomScrollView
- 滾動監聽及控制ScrollController
- 功能型Widgets
- 導航返回攔截-WillPopScope
- 數據共享-InheritedWidget
- 主題-Theme
- 事件處理與通知
- 原始指針事件處理
- 手勢識別
- 全局事件總線
- 通知Notification
- 動畫
- Flutter動畫簡介
- 動畫結構
- 自定義路由過渡動畫
- Hero動畫
- 交錯動畫
- 自定義Widget
- 自定義Widget方法簡介
- 通過組合現有Widget實現
- 實例:TurnBox
- CustomPaint與Canvas
- 實例:圓形漸變進度條(自繪)
- 文件操作與網絡請求
- 文件操作
- Http請求-HttpClient
- Http請求-Dio package
- 實例:Http分塊下載
- WebSocket
- 使用Socket API
- Json轉Model
- 包與插件
- 開發package
- 插件開發:平臺通道簡介
- 插件開發:實現Android端API
- 插件開發:實現IOS端API
- 系統能力調用
- 國際化
- 讓App支持多語言
- 實現Localizations
- 使用Intl包
- Flutter核心原理
- Flutter UI系統
- Element和BuildContext
- RenderObject與RenderBox
- Flutter從啟動到顯示