JSON 和序列化
如何在 Flutter 中使用 JSON。
很難想象一個不需要與 Web 伺服器通訊或以某種方式輕鬆儲存結構化資料的移動應用程式。在構建聯網應用程式時,遲早需要使用一些經典的 JSON 格式。
本指南探討了在 Flutter 中使用 JSON 的方法。它涵蓋了在不同場景下使用哪種 JSON 解決方案以及原因。
哪種 JSON 序列化方法適合我?
#本文涵蓋了兩種通用的 JSON 處理策略
- 手動序列化
- 使用程式碼生成進行自動序列化
不同的專案具有不同的複雜性和用例。對於較小的概念驗證專案或快速原型,使用程式碼生成器可能過於繁瑣。對於具有多個 JSON 模型且複雜度較高的應用程式,手動編碼會很快變得繁瑣、重複且容易出現許多小錯誤。
對於小型專案,使用手動序列化
#手動 JSON 解碼是指使用 dart:convert 中的內建 JSON 解碼器。它涉及將原始 JSON 字串傳遞給 jsonDecode() 函式,然後在生成的 Map<String, dynamic> 中查詢所需的值。它沒有外部依賴項或特定的設定過程,並且適用於快速的概念驗證。
當您的專案變得更大時,手動解碼的效能不會很好。手動編寫解碼邏輯會變得難以管理且容易出錯。如果您在訪問不存在的 JSON 欄位時出現拼寫錯誤,您的程式碼將在執行時丟擲錯誤。
如果您在專案中沒有很多 JSON 模型並且希望快速測試概念,手動序列化可能是您想要開始的方式。有關手動編碼的示例,請參閱 使用 dart:convert 手動序列化 JSON。
對於中大型專案,使用程式碼生成
#使用程式碼生成進行 JSON 序列化意味著讓外部庫為您生成編碼樣板程式碼。在進行一些初始設定後,您執行一個檔案監視器,該監視器從您的模型類中生成程式碼。例如,json_serializable 和 built_value 是這些型別的庫。
這種方法可以很好地擴充套件到更大的專案。無需手動編寫樣板程式碼,並且訪問 JSON 欄位時的拼寫錯誤會在編譯時捕獲。程式碼生成的缺點是它需要一些初始設定。此外,生成的原始碼可能會在您的專案導航器中產生視覺混亂。
如果您有一箇中型或大型專案,則可能需要使用生成的程式碼進行 JSON 序列化。有關基於程式碼生成 JSON 編碼的示例,請參閱 使用程式碼生成庫序列化 JSON。
Flutter 中是否有 GSON/Jackson/Moshi 的等效替代方案?
#
簡單的答案是:沒有。
這樣的庫需要使用執行時 反射,這在 Flutter 中被停用。執行時反射會干擾 樹搖,而 Dart 已經支援樹搖很長時間了。透過樹搖,您可以“搖掉”釋出版本中的未使用的程式碼。這可以顯著最佳化應用程式的大小。
由於反射預設使所有程式碼都處於使用狀態,因此使得樹搖變得困難。工具無法知道執行時哪些部分未被使用,因此很難刪除冗餘程式碼。使用反射時,應用程式大小無法輕鬆最佳化。
雖然您無法在 Flutter 中使用執行時反射,但有些庫為您提供類似易於使用的 API,但它們基於程式碼生成。這種方法將在 程式碼生成庫 部分中更詳細地介紹。
使用 dart:convert 手動序列化 JSON
#Flutter 中的基本 JSON 序列化非常簡單。Flutter 具有內建的 dart:convert 庫,其中包含一個簡單的 JSON 編碼器和解碼器。
以下示例 JSON 實現了一個簡單的使用者模型。
{
"name": "John Smith",
"email": "john@example.com"
}
使用 dart:convert,您可以以兩種方式序列化此 JSON 模型。
內聯序列化 JSON
#透過檢視 dart:convert 文件,您會看到可以透過呼叫 jsonDecode() 函式(JSON 字串作為方法引數)來解碼 JSON。
final user = jsonDecode(jsonString) as Map<String, dynamic>;
print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');
不幸的是,jsonDecode() 返回一個 dynamic,這意味著您不知道值的型別,直到執行時。使用這種方法,您將失去大部分靜態型別語言的功能:型別安全、自動完成,最重要的是,編譯時異常。您的程式碼會立即變得更容易出錯。
例如,每當您訪問 name 或 email 欄位時,您可能會快速引入拼寫錯誤。編譯器不知道的拼寫錯誤,因為 JSON 存在於 map 結構中。
在模型類中序列化 JSON
#透過引入一個普通模型類(在本例中稱為 User)來解決上述問題。在 User 類中,您會找到
- 一個
User.fromJson()建構函式,用於從 map 結構構造一個新的User例項。 - 一個
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'] as String,
email = json['email'] as String;
Map<String, dynamic> toJson() => {'name': name, 'email': email};
}
解碼邏輯的責任現在轉移到模型本身。使用這種新方法,您可以輕鬆解碼使用者。
final userMap = jsonDecode(jsonString) as Map<String, dynamic>;
final user = User.fromJson(userMap);
print('Howdy, ${user.name}!');
print('We sent the verification link to ${user.email}.');
要編碼使用者,請將 User 物件傳遞給 jsonEncode() 函式。您不需要呼叫 toJson() 方法,因為 jsonEncode() 已經為您完成了此操作。
String json = jsonEncode(user);
使用這種方法,呼叫程式碼無需擔心 JSON 序列化。但是,模型類仍然必須這樣做。在生產應用程式中,您需要確保序列化正常工作。實際上,User.fromJson() 和 User.toJson() 方法都需要有單元測試來驗證正確的行為。
但是,現實世界的場景並不總是那麼簡單。有時 JSON API 響應更復雜,例如,因為它們包含巢狀的 JSON 物件,這些物件必須透過自己的模型類進行解析。
如果有一種方法可以為您處理 JSON 編碼和解碼,那就太好了!幸運的是,有!
使用程式碼生成庫序列化 JSON
#雖然還有其他庫可用,但本指南使用 json_serializable,這是一個自動原始碼生成器,可為您生成 JSON 序列化樣板程式碼。
由於序列化程式碼不再手動編寫或手動維護,因此您最大限度地降低了在執行時出現 JSON 序列化異常的風險。
在專案中設定 json_serializable
#要在您的專案中包含 json_serializable,您需要一個常規依賴項和兩個開發依賴項。簡而言之,開發依賴項是不包含在我們的應用程式原始碼中的依賴項——它們僅在開發環境中使用的依賴項。
要新增依賴項,請執行 flutter pub add
flutter pub add json_annotation dev:build_runner dev:json_serializable
在您的專案根資料夾中執行 flutter pub get(或在您的編輯器中單擊Packages get),以使這些新依賴項在您的專案中可用。
以 json_serializable 的方式建立模型類
#以下顯示瞭如何將 User 類轉換為 json_serializable 類。為了簡單起見,此程式碼使用來自先前示例的簡化 JSON 模型。
user.dart
import 'package:json_annotation/json_annotation.dart';
/// This allows the `User` class to access private members in
/// the generated file. The value for this is *.g.dart, where
/// the star denotes the source file name.
part 'user.g.dart';
/// An annotation for the code generator to know that this class needs the
/// JSON serialization logic to be generated.
@JsonSerializable()
class User {
User(this.name, this.email);
String name;
String email;
/// A necessary factory constructor for creating a new User instance
/// from a map. Pass the map to the generated `_$UserFromJson()` constructor.
/// The constructor is named after the source class, in this case, User.
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
/// `toJson` is the convention for a class to declare support for serialization
/// to JSON. The implementation simply calls the private, generated
/// helper method `_$UserToJson`.
Map<String, dynamic> toJson() => _$UserToJson(this);
}
使用此設定,原始碼生成器會生成用於編碼和解碼 name 和 email 欄位的程式碼。
如果需要,也可以輕鬆自定義命名策略。例如,如果 API 返回帶有snake_case的物件,並且您想在模型中使用lowerCamelCase,則可以使用帶有 name 引數的 @JsonKey 註釋
/// Tell json_serializable that "registration_date_millis" should be
/// mapped to this property.
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;
最好是伺服器和客戶端都遵循相同的命名策略。@JsonSerializable() 提供 fieldRename 列舉,用於完全將 dart 欄位轉換為 JSON 鍵。
修改 @JsonSerializable(fieldRename: FieldRename.snake) 等效於將 @JsonKey(name: '<snake_case>') 新增到每個欄位。
有時伺服器資料不確定,因此有必要在客戶端驗證和保護資料。其他常用的 @JsonKey 註解包括
/// Tell json_serializable to use "defaultValue" if the JSON doesn't
/// contain this key or if the value is `null`.
@JsonKey(defaultValue: false)
final bool isAdult;
/// When `true` tell json_serializable that JSON must contain the key,
/// If the key doesn't exist, an exception is thrown.
@JsonKey(required: true)
final String id;
/// When `true` tell json_serializable that generated code should
/// ignore this field completely.
@JsonKey(ignore: true)
final String verificationCode;
執行程式碼生成工具
#首次建立 json_serializable 類時,您會遇到類似以下的錯誤
Target of URI hasn't been generated: 'user.g.dart'.
這些錯誤完全正常,僅僅是因為模型類的生成程式碼尚未存在。要解決此問題,請執行程式碼生成器以生成序列化樣板程式碼。
有兩種方法可以執行程式碼生成器。
一次性程式碼生成
#透過在專案根目錄下執行 dart run build_runner build --delete-conflicting-outputs,您可以在需要時為模型生成 JSON 序列化程式碼。這將觸發一次性構建,它會遍歷原始碼檔案,選擇相關檔案,併為它們生成必要的序列化程式碼。
雖然這很方便,但如果您每次更改模型類時都不需要手動執行構建,那就更好了。
持續生成程式碼
#一個觀察者使我們的原始碼生成過程更加方便。它會監視專案檔案的更改,並在需要時自動構建必要的檔案。透過在專案根目錄下執行 dart run build_runner watch --delete-conflicting-outputs 來啟動觀察者。
可以安全地啟動觀察者並將其在後臺執行。
使用 json_serializable 模型
#要以 json_serializable 的方式解碼 JSON 字串,您實際上不需要對我們之前的程式碼進行任何更改。
final userMap = jsonDecode(jsonString) as Map<String, dynamic>;
final user = User.fromJson(userMap);
編碼也是如此。呼叫的 API 與之前相同。
String json = jsonEncode(user);
使用 json_serializable 後,您可以忘記在 User 類中進行任何手動 JSON 序列化。原始碼生成器會建立一個名為 user.g.dart 的檔案,其中包含所有必要的序列化邏輯。您不再需要編寫自動化測試來確保序列化正常工作——現在庫的責任是確保序列化能夠適當地工作。
生成巢狀類的程式碼
#您可能有一些包含類內巢狀類的程式碼。如果是這樣,並且您嘗試將該類作為引數傳遞給服務(例如 Firebase),您可能會遇到 Invalid argument 錯誤。
考慮以下 Address 類
import 'package:json_annotation/json_annotation.dart';
part 'address.g.dart';
@JsonSerializable()
class Address {
String street;
String city;
Address(this.street, this.city);
factory Address.fromJson(Map<String, dynamic> json) =>
_$AddressFromJson(json);
Map<String, dynamic> toJson() => _$AddressToJson(this);
}
Address 類巢狀在 User 類中
import 'package:json_annotation/json_annotation.dart';
import 'address.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
User(this.name, this.address);
String name;
Address address;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
在終端中執行 dart run build_runner build --delete-conflicting-outputs 會建立 *.g.dart 檔案,但私有的 _$UserToJson() 函式看起來如下
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'name': instance.name,
'address': instance.address,
};
現在一切看起來都很好,但是如果您對 user 物件執行 print()
Address address = Address('My st.', 'New York');
User user = User('John', address);
print(user.toJson());
結果是
{name: John, address: Instance of 'address'}
而您可能想要輸出如下所示
{name: John, address: {street: My st., city: New York}}
要使其工作,請在類宣告上方的 @JsonSerializable() 註解中傳遞 explicitToJson: true。現在 User 類如下所示
import 'package:json_annotation/json_annotation.dart';
import 'address.dart';
part 'user.g.dart';
@JsonSerializable(explicitToJson: true)
class User {
User(this.name, this.address);
String name;
Address address;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
有關更多資訊,請參閱 explicitToJson 在 JsonSerializable 類中的說明,以及 json_annotation 包的說明。
進一步參考
#有關更多資訊,請參閱以下資源
dart:convert和JsonCodec文件- pub.dev 上的
json_serializable包 - GitHub 上的
json_serializable示例 - 深入瞭解 Dart 的模式和記錄的 codelab
- 關於 如何在 Dart/Flutter 中解析 JSON 的終極指南