跳到主內容

JSON 和序列化

如何在 Flutter 中使用 JSON。

很難想象有一個移動應用在某個時刻不需要與 Web 伺服器通訊或儲存結構化資料。在開發聯網應用時,遲早都需要處理傳統的 JSON 資料。

本指南探討了在 Flutter 中使用 JSON 的方法。它涵蓋了在不同場景下應使用哪種 JSON 解決方案,以及原因。

在 YouTube 上觀看(在新標籤頁開啟):“dart:convert (每週技術分享)”

哪種 JSON 序列化方法適合我?

#

本文涵蓋了處理 JSON 的兩種通用策略:

  • 手動序列化
  • 使用程式碼生成進行自動化序列化

不同的專案具有不同的複雜性和使用場景。對於較小的概念驗證專案或快速原型,使用程式碼生成器可能有些大材小用。對於具有多個複雜 JSON 模型的應用,手動編碼很快會變得乏味、重複,且容易產生各種小錯誤。

小型專案使用手動序列化

#

手動 JSON 解碼是指使用 dart:convert 中內建的 JSON 解碼器。它涉及將原始 JSON 字串傳遞給 jsonDecode() 函式,然後在生成的 Map<String, dynamic> 中查詢所需的值。它沒有外部依賴,也不需要特別的配置過程,非常適合快速概念驗證。

當專案變大時,手動解碼的表現不佳。手動編寫解碼邏輯變得難以管理且容易出錯。如果訪問不存在的 JSON 欄位時出現拼寫錯誤,程式碼會在執行時丟擲錯誤。

如果你的專案中 JSON 模型不多,並且想快速測試一個概念,手動序列化可能是你的首選。關於手動編碼的示例,請參閱 使用 dart:convert 手動序列化 JSON

中大型專案使用程式碼生成

#

使用程式碼生成的 JSON 序列化意味著由外部庫為你生成編碼樣板程式碼。在進行一些初始設定後,你可以執行一個檔案監視器,根據你的模型類生成程式碼。例如,json_serializablebuilt_value 就是這類庫。

這種方法非常適合大型專案。無需手寫樣板程式碼,訪問 JSON 欄位時的拼寫錯誤會在編譯時被捕獲。程式碼生成的缺點是需要一些初始設定,並且生成的原始檔可能會在專案導航器中造成視覺混亂。

當你擁有中型或大型專案時,可能需要使用生成的程式碼進行 JSON 序列化。要檢視基於程式碼生成的 JSON 編碼示例,請參閱 使用程式碼生成庫序列化 JSON

Flutter 中有 GSON/Jackson/Moshi 的等效庫嗎?

#

簡單的回答是:沒有。

這樣的庫需要使用執行時 反射 (reflection),但在 Flutter 中是被停用的。執行時反射會干擾 搖樹最佳化 (tree shaking),而 Dart 對此支援已久。透過搖樹最佳化,你可以從釋出版本中“剔除”未使用的程式碼。這顯著優化了應用的大小。

由於反射使得所有程式碼在預設情況下都是隱式使用的,因此它使搖樹最佳化變得困難。工具無法知道哪些部分在執行時未被使用,因此難以剔除冗餘程式碼。使用反射時,應用大小無法輕易最佳化。

儘管你無法在 Flutter 中使用執行時反射,但有些庫提供了同樣易於使用的 API,但它們是基於程式碼生成的。這種方法在 程式碼生成庫 一節中有更詳細的介紹。

使用 dart:convert 手動序列化 JSON

#

Flutter 中的基本 JSON 序列化非常簡單。Flutter 有一個內建的 dart:convert 庫,其中包含一個直接的 JSON 編碼器和解碼器。

以下示例 JSON 實現了一個簡單的使用者模型。

json
{
  "name": "John Smith",
  "email": "john@example.com"
}

使用 dart:convert,你可以透過兩種方式序列化此 JSON 模型。

內聯序列化 JSON

#

檢視 dart:convert 文件,你會發現可以透過呼叫 jsonDecode() 函式並傳入 JSON 字串作為引數來解碼 JSON。

dart
final user = jsonDecode(jsonString) as Map<String, dynamic>;

print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');

不幸的是,jsonDecode() 返回的是一個 dynamic,這意味著在執行時之前你無法確定值的型別。使用這種方法,你失去了大部分靜態型別語言的特性:型別安全、自動補全,最重要的是,編譯時異常。你的程式碼會瞬間變得更容易出錯。

例如,每當你訪問 nameemail 欄位時,都可能引發拼寫錯誤。由於 JSON 存在於 Map 結構中,編譯器無法識別這種拼寫錯誤。

在模型類中序列化 JSON

#

透過引入一個簡單的模型類(在本例中稱為 User)來解決上述問題。在 User 類中,你會發現:

  • 一個 User.fromJson() 建構函式,用於從 Map 結構構造一個新的 User 例項。
  • 一個 toJson() 方法,將 User 例項轉換為 Map。

透過這種方法,呼叫程式碼 可以擁有型別安全、nameemail 欄位的自動補全以及編譯時異常。如果你拼寫錯誤或將欄位當作 int 而不是 String 處理,應用將無法編譯,而不是在執行時崩潰。

user.dart

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};
}

解碼邏輯的責任現在被移到了模型內部。使用這種新方法,你可以輕鬆解碼使用者。

dart
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() 已經為你完成了。

dart
String json = jsonEncode(user);

使用這種方法,呼叫程式碼完全不需要擔心 JSON 序列化。然而,模型類顯然必須擔心。在生產環境中,你希望確保序列化正常工作。在實踐中,User.fromJson()User.toJson() 方法都需要進行單元測試,以驗證其行為是否正確。

然而,現實場景並不總是那麼簡單。有時 JSON API 響應更為複雜,例如,因為它們包含必須透過各自模型類解析的巢狀 JSON 物件。

如果能有一些東西自動幫你處理 JSON 編碼和解碼就太好了。幸運的是,確實有!

使用程式碼生成庫序列化 JSON

#

雖然還有其他庫可用,但本指南使用 json_serializable,這是一個自動原始碼生成器,可以為你生成 JSON 序列化樣板程式碼。

由於序列化程式碼不再是手寫或手動維護的,你將執行時出現 JSON 序列化異常的風險降至最低。

在專案中配置 json_serializable

#

要在你的專案中包含 json_serializable,你需要一個常規依賴項和兩個 開發依賴項 (dev dependencies)。簡而言之,開發依賴項 是指不包含在應用原始碼中的依賴項——它們僅在開發環境中使用。

要新增依賴項,請執行 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

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);
}

透過此設定,原始碼生成器將生成從 JSON 編碼和解碼 nameemail 欄位的程式碼。

如果需要,自定義命名策略也很容易。例如,如果 API 返回的物件使用 snake_case,而你希望在模型中使用 lowerCamelCase,你可以使用 @JsonKey 註解並帶有 name 引數。

dart
/// 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 註解包括:

dart
/// 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 序列化程式碼。這會觸發一次性構建,遍歷原始檔,挑選相關檔案,併為它們生成必要的序列化程式碼。

雖然這很方便,但如果你每次更改模型類時都不必手動執行構建,那就更好了。

持續生成程式碼

#

監視器 (watcher) 使我們的原始碼生成過程更加方便。它會監視專案中檔案的更改,並在需要時自動構建必要的檔案。透過在專案根目錄執行 dart run build_runner watch --delete-conflicting-outputs 啟動監視器。

啟動監視器並在後臺執行它是安全的。

使用 json_serializable 模型

#

要以 json_serializable 方式解碼 JSON 字串,實際上不需要對之前的程式碼做任何更改。

dart
final userMap = jsonDecode(jsonString) as Map<String, dynamic>;
final user = User.fromJson(userMap);

編碼也是如此。呼叫 API 與之前相同。

dart
String json = jsonEncode(user);

使用 json_serializable,你可以忘記 User 類中的任何手動 JSON 序列化。原始碼生成器會建立一個名為 user.g.dart 的檔案,其中包含所有必要的序列化邏輯。你不再需要編寫自動化測試來確保序列化正常工作——現在確保序列化正常工作是 庫的責任

為巢狀類生成程式碼

#

你的程式碼中可能包含巢狀類。如果是這樣,並且你嘗試以 JSON 格式將該類作為引數傳遞給服務(例如 Firebase),你可能遇到過 Invalid argument 錯誤。

考慮以下 Address 類:

dart
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 類中:

dart
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() 函式看起來像下面這樣:

dart
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
  'name': instance.name,
  'address': instance.address,
};

現在看起來一切正常,但如果你對 user 物件執行 print()

dart
Address address = Address('My st.', 'New York');
User user = User('John', address);
print(user.toJson());

結果是:

json
{name: John, address: Instance of 'address'}

而你可能想要的輸出是:

json
{name: John, address: {street: My st., city: New York}}

要實現這一點,請在類宣告上方的 @JsonSerializable() 註解中傳入 explicitToJson: true。現在的 User 類如下所示:

dart
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);
}

有關更多資訊,請參閱 json_annotation 包中 JsonSerializable 類的 explicitToJson 說明。

更多參考

#

有關更多資訊,請參閱以下資源: