Flutter json 2 model with Built Value
阿新 • • 發佈:2020-03-16
# Flutter json 2 model with Built Value
Flutter中json轉換model, 除了手動轉之外, 就是利用第三方庫做一些程式碼生成.
流行的庫有: [json_serializable](https://pub.dev/packages/json_serializable)和[built_value](https://pub.dev/packages/built_value)
本文介紹[built_value](https://pub.dev/packages/built_value)的實際使用及問題處理.
## Flutter中的json轉model方法
Flutter中json到model型別的轉換可以有多種方式:
* 利用官方自帶的dart convert中的json解碼. 該方法只能將json轉換為List或Map, 剩下的工作需要手動完成, 根據key取值賦值給model的欄位.
* 利用第三方的庫, 做程式碼生成, 流行的庫有: [json_serializable](https://pub.dev/packages/json_serializable)和[built_value](https://pub.dev/packages/built_value). 原理都是相同的, 先寫一些模板程式碼, 說明一下model是什麼樣子的, 然後執行命令列生成一些程式碼, 之後就可以很方便地呼叫, 將json轉換為model了.
使用json_serializable可以看:
* 官網的例子: [Serializing JSON using code generation libraries](https://flutter.dev/docs/development/data-and-backend/json#code-generation).
* Flutter實戰中文的: [11.7 Json轉Dart Model類](https://book.flutterchina.club/chapter11/json_model.html)
本篇文章主要介紹built value的使用.
## built value使用指南
例項: 用github api拿到的events: https://api.github.com/events?per_page=10
如何轉化成model物件呢?
### TDD
先寫個測試, 明確一下我們想要的目標.
在`test`下建立一個檔案, 比如叫`json_test.dart`.
裡面寫main函式和兩個測試:
```
void main() {
test("parse events list", () {
const jsonString = """replace with events list json string""";
expect(Event.fromEventsListJson(jsonString).first.id, "11732023561");
});
test("parse event", () {
const jsonString = """replace with event json string""";
expect(Event.fromJson(jsonString).id, "11732036753");
});
}
```
這裡面應該放json字串的, 太長了我就省略了, 這樣看比較清晰.
用`"""`之後可以支援多行. (IDE裡面可以摺疊的.)
這個`Event`類和方法我們都還沒有寫, 所以暫時報錯.
### setup
新增依賴, 去package頁面看新增什麼版本: https://pub.dev/packages/built_value
在`pubspec.yaml`中新增:
```
dependencies:
flutter:
sdk: flutter
# other dependencies here
built_value: ^7.0.9
built_collection: ^4.3.2
dev_dependencies:
flutter_test:
sdk: flutter
# other dev_dependencies here
build_runner: ^1.8.0
built_value_generator: ^7.0.9
```
然後點`Packages get`.
### Live Templates
這個是IntelliJ系IDE(包括Android Studio)的快捷設定, 目的是為了減少手動輸入. (可選.)
開啟`Preferences`, 搜`Live Templates`.
在`Dart`的部分點+號新增一個Live Template.
下面Abbreviation選一個適當的縮寫, 比如`built`.
Template text貼入這段:
```
abstract class $CLASS_NAME$ implements Built<$CLASS_NAME$, $CLASS_NAME$Builder> {
$CLASS_NAME$._();
factory $CLASS_NAME$([void Function($CLASS_NAME$Builder) updates]) = _$$$CLASS_NAME$;
}
```
Applicable in Dart選: top-level.
建好之後以後就直接用啦.
### 建立models抽象類
輸入剛才建立的live template的關鍵字`built`, 就會出現要生成的程式碼, 其中寫好自己的類名.
比如我們要建立的model型別是`Event`類.
新建`event.dart`檔案.
在其中輸入`built`按確認之後, 輸入類名`Event`, 就建好了:
```
abstract class Event implements Built {
Event._();
factory Event([void Function(EventBuilder) updates]) = _$Event;
}
```
包括一個私有構造和一個工廠方法. 此時會有一些紅色的報錯.
這裡`import 'package:built_value/built_value.dart'`消除`Built`類的報錯.
根據觀察API: https://api.github.com/events 返回的json, 發現還應該有`Actor`, `Repo`, `Payload`三個類.
也都按這個方法建立好.
然後在其中新增欄位, 現在看起來是這樣了:
```
import 'package:built_value/built_value.dart';
// imports for models
part 'event.g.dart';
abstract class Event implements Built {
String get id;
String get type;
Actor get actor;
Repo get repo;
Payload get payload;
bool get public;
String get createdAt;
Event._();
factory Event([void Function(EventBuilder) updates]) = _$Event;
}
```
很重要的一步, 就是在類前面新增上一句: `part 'event.g.dart';`.
`g.dart`是一個慣例, 表明這個檔案是生成的程式碼. `part`表示目前這個檔案是另一個檔案的一部分.
按照同樣的方法把幾個類都建好.
注意如果有列表欄位, 要宣告為`BuiltList`型別.
### 執行生成命令
生成命令:
```
flutter packages pub run build_runner build
```
需要持續構建和可以用:
```
flutter packages pub run build_runner watch
```
這樣就不用每次改完程式碼都需要跑一次命令了.
我們這裡用watch, 因為還沒有改完.
執行完成之後, 可以看到`.g.dart`的檔案們都生成了, 報錯也消失了.
### 寫Serializers
新建檔案`serializers.dart`.
```
import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';
// imports for models
part 'serializers.g.dart';
@SerializersFor(const [
Event,
Actor,
Repo,
Payload,
])
final Serializers serializers =
(_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();
```
`@SerializersFor`裡面列出想要序列化的類.
注意這裡要加上`StandardJsonPlugin`, 因為built value的json格式不是標準的, 而是所有欄位逗號分隔的.
用了`StandardJsonPlugin`之後就轉換成了標準的JSON格式.
因為我們跑命令的時候用的是`watch`, 所以儲存修改後`serializers.g.dart`檔案此時自動生成了.
### 新增model序列化和反序列化程式碼
在Event類中新增:
```
static Serializer get serializer => _$eventSerializer;
String toJson() {
return json.encode(serializers.serializeWith(Event.serializer, this));
}
static Event fromJson(String jsonString) {
return serializers.deserializeWith(
Event.serializer, json.decode(jsonString));
}
```
import中除了model類還有:
```
import 'dart:convert';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'serializers.dart';
```
此時其他幾個model類也要新增`serializer`, 比如`Actor`類中新增:
```
static Serializer get serializer => _$actorSerializer;
```
重新build生成程式碼, 報錯消失.
現在可以執行測試:
```
test("parse event", () {
const jsonString = """replace with event json string""";
expect(Event.fromJson(jsonString).id, "11732036753");
});
```
來檢驗單個的Event model建立.
可能會遇到的失敗情況:
* 一些欄位需要被標記為可為空`@nullable`.
* 一些欄位名和key不匹配, 用`@BuiltValueField`的`wireName`標記.
詳見後面的`Troubleshooting`部分.
### 如何反序列化頂層列表?
Event的API返回的是一個Event的陣列: `[]`. 這種怎麼做呢?
這裡有個issue就是關於這個問題, 裡面的解決辦法挺好: https://github.com/google/built_value.dart/issues/565
在`serializers.dart`中新增方法:
```
T deserialize(dynamic value) =>
serializers.deserializeWith(serializers.serializerForType(T), value);
BuiltList deserializeListOf(dynamic value) => BuiltList.from(
value.map((value) => deserialize(value)).toList(growable: false));
```
其中`BuiltList`需要`import 'package:built_collection/built_collection.dart';`.
反序列化`Event`陣列的方法:
```
static List fromEventsListJson(String jsonString) {
final BuiltList listOfEvents =
deserializeListOf(json.decode(jsonString));
return listOfEvents.toList();
}
```
到這一步, 跑我們開頭寫的兩個測試應該都綠了. 如果沒綠見`Troubleshooting`部分.
### 泛型的fromJson方法.
上面給`serializers`中添加了兩個方法. 其中第一個方法是一個泛型的`fromJson`方法.
我們測試中的:
```
expect(Event.fromJson(jsonString).id, "11732036753");
```
也可以這樣寫:
```
expect(deserialize(json.decode(jsonString)).id, "11732036753");
```
這樣不用給每一個類都寫一個`fromJson`方法了.
## Troubleshooting
可能會有的報錯, 問題原因和解決方式.
### 報錯1: `failed due to: Invalid argument(s): Unknown type on deserialization. Need either specifiedType or discriminator field.`
比如這個樣子:
```
Deserializing '[id, 11732036753, type, PushEvent, actor, {id: 54496419, login: supershell201...' to 'Event' failed due to: Invalid argument(s): Unknown type on deserialization. Need either specifiedType or discriminator field.
```
這是因為`Event`中依賴的類(`Actor`, `Repo`, `Payload`)沒有新增serializer.
比如`Actor`中:
```
static Serializer get serializer => _$actorSerializer;
```
新增上重新build生成程式碼即可.
### 報錯2: `Tried to construct class "XXX" with null field`
比如:
```
Deserializing '[id, 11732036753, type, PushEvent, actor, {id: 54496419, login: supershell201...' to 'Event' failed due to: Deserializing '[id, 54496419, login, supershell2019, display_login, supershell2019, gravatar...' to 'Actor' failed due to: Tried to construct class "Actor" with null field "displayLogin". This is forbidden; to allow it, mark "displayLogin" with @nullable.
```
此時, 先不要著急把欄位標記為`@nullable`.
而是要看這個欄位是否真的為null, 很有可能是因為欄位名稱和json中的key不匹配造成的, 比如json中是個蛇形命名.
查看了一下果然就是, 解決辦法:
```
@BuiltValueField(wireName: 'display_login')
String get displayLogin;
```
如果欄位真的是有可能為null的情況, 那麼加上`@nullable`:
比如:
```
@BuiltValueField(wireName: 'ref_type')
@nullable
String get refType;
```
### 報錯3: `FormatException: Control character in string`
比如:
```
FormatException: Control character in string (at line 25, character 129)
... replica::on_client_write(dsn::message_ex *request, bool ignore_throttling)
```
相關issue: https://github.com/dart-lang/convert/issues/10
解決的辦法就是在測試的字串宣告前加一個`r`:
```
const jsonString = r"""replace with events list json string""";
```
## Model生成工具推薦
有個很棒的工具: https://charafau.github.io/json2builtvalue/
左邊輸入json字串, 寫好命名, 點選之後右邊就會出現那些本來需要手動寫的程式碼.
生成的`Event`類是這樣:
```
library event;
import 'dart:convert';
import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
part 'event.g.dart';
abstract class Event implements Built {
Event._();
factory Event([updates(EventBuilder b)]) = _$Event;
@BuiltValueField(wireName: 'id')
String get id;
@BuiltValueField(wireName: 'type')
String get type;
@BuiltValueField(wireName: 'actor')
Actor get actor;
@BuiltValueField(wireName: 'repo')
Repo get repo;
@BuiltValueField(wireName: 'payload')
Payload get payload;
@BuiltValueField(wireName: 'public')
bool get public;
@BuiltValueField(wireName: 'created_at')
String get createdAt;
String toJson() {
return json.encode(serializers.serializeWith(Event.serializer, this));
}
static Event fromJson(String jsonString) {
return serializers.deserializeWith(
Event.serializer, json.decode(jsonString));
}
static Serializer get serializer => _$eventSerializer;
}
```
哈哈, 看到這裡是不是有種被騙了的感覺.
有了這個很棒的工具之後根本不用自己很小心地寫一個一個model類了, 只需要寫一個`serializers.dart`檔案:
```
part 'serializers.g.dart';
@SerializersFor(const [
Event,
Actor,
Repo,
Payload,
])
final Serializers serializers =
(_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();
T deserialize(dynamic value) =>
serializers.deserializeWith(serializers.serializerForType(T), value);
BuiltList deserializeListOf(dynamic value) => BuiltList.from(
value.map((value) => deserialize(value)).toList(growable: false));
```
然後把要反序列化的類加進來, 再跑命令列生成程式碼, 就可以了.
經歷一下前面的手動過程可能理解得更好一些, 也知道各種問題的原因.
以後使用直接用工具就方便多了.
## 參考資料
* 官方文件: https://flutter.dev/docs/development/data-and-backend/json
* Flutter實戰11.7: https://book.flutterchina.club/chapter11/json_model.html
* [built value github](https://github.com/google/built_value.dart)
* 生成工具: https://charafau.github.io/json2bui