Flutter json 2 model with Built Value
- 2020 年 3 月 16 日
- 筆記
Flutter json 2 model with Built Value
Flutter中json轉換model, 除了手動轉之外, 就是利用第三方庫做一些程式碼生成.
流行的庫有: json_serializable和built_value
本文介紹built_value的實際使用及問題處理.
Flutter中的json轉model方法
Flutter中json到model類型的轉換可以有多種方式:
- 利用官方自帶的dart convert中的json解碼. 該方法只能將json轉換為List或Map, 剩下的工作需要手動完成, 根據key取值賦值給model的欄位.
- 利用第三方的庫, 做程式碼生成, 流行的庫有: json_serializable和built_value. 原理都是相同的, 先寫一些模板程式碼, 說明一下model是什麼樣子的, 然後運行命令行生成一些程式碼, 之後就可以很方便地調用, 將json轉換為model了.
使用json_serializable可以看:
- 官網的例子: Serializing JSON using code generation libraries.
- Flutter實戰中文的: 11.7 Json轉Dart Model類
本篇文章主要介紹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, EventBuilder> { 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<Event, EventBuilder> { 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<Event> 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<Actor> 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<T>(dynamic value) => serializers.deserializeWith<T>(serializers.serializerForType(T), value); BuiltList<T> deserializeListOf<T>(dynamic value) => BuiltList.from( value.map((value) => deserialize<T>(value)).toList(growable: false)); 其中BuiltList需要import 'package:built_collection/built_collection.dart';.
反序列化Event數組的方法:
static List<Event> fromEventsListJson(String jsonString) { final BuiltList<Event> listOfEvents = deserializeListOf<Event>(json.decode(jsonString)); return listOfEvents.toList(); } 到這一步, 跑我們開頭寫的兩個測試應該都綠了. 如果沒綠見Troubleshooting部分.
泛型的fromJson方法.
上面給serializers中添加了兩個方法. 其中第一個方法是一個泛型的fromJson方法.
我們測試中的:
expect(Event.fromJson(jsonString).id, "11732036753"); 也可以這樣寫:
expect(deserialize<Event>(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<Actor> 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, EventBuilder> { 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<Event> 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<T>(dynamic value) => serializers.deserializeWith<T>(serializers.serializerForType(T), value); BuiltList<T> deserializeListOf<T>(dynamic value) => BuiltList.from( value.map((value) => deserialize<T>(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://charafau.github.io/json2builtvalue/

