跳至主要內容

JSON 與序列化

很難想像一個行動應用程式不需要在某個時候與網頁伺服器通訊或輕鬆儲存結構化資料。當製作連網應用程式時,很可能遲早需要使用一些常見的 JSON。

本指南將探討在 Flutter 中使用 JSON 的方法。它涵蓋了在不同情境下應該使用哪種 JSON 解決方案,以及為什麼。

哪個 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 的工具嗎?

#

簡單的答案是沒有。

此類程式庫需要使用執行時間反射,這在 Flutter 中會被停用。執行時間反射會干擾樹狀搖動,而 Dart 已經支援樹狀搖動很長一段時間。透過樹狀搖動,您可以從發行組建中「搖掉」未使用的程式碼。這會顯著最佳化應用程式的大小。

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

雖然您無法在 Flutter 中使用執行時間反射,但某些程式庫會提供類似的易用 API,但基於程式碼產生。本節的程式碼產生程式庫一節會更詳細地介紹此方法。

使用 dart:convert 手動序列化 JSON

#

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

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

json
{
  "name": "John Smith",
  "email": "[email protected]"
}

使用 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 位於對應結構中。

在模型類別中序列化 JSON

#

若要解決先前提到問題,請引入一個簡單的模型類別,在本範例中稱為 User。在 User 類別中,您會找到

  • 一個 User.fromJson() 建構函式,用於從對應結構建構新的 User 執行個體。
  • 一個 toJson() 方法,將 User 執行個體轉換為對應。

使用此方法,呼叫程式碼可以具有型別安全、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,您需要一個常規相依性,以及兩個開發相依性。簡而言之,開發相依性是不包含在應用程式原始碼中的相依性,它們僅在開發環境中使用。

若要新增相依性,請執行 flutter pub add

flutter pub add json_annotation dev:build_runner dev:json_serializable

在您的專案根資料夾內執行 flutter pub 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,您可以使用具有 name 參數的 @JsonKey 註釋

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 類別時,您會收到類似於下圖中顯示的錯誤。

IDE warning when the generated code for a model class does not exist
yet.

這些錯誤是完全正常的,而且只是因為模型類別的產生的程式碼還不存在。若要解決此問題,請執行產生序列化樣板的程式碼產生器。

有兩種執行程式碼產生器的方式。

一次性程式碼產生

#

藉由在專案根目錄中執行 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);
}

有關更多資訊,請參閱 explicitToJsonJsonSerializable 類別中的說明,此類別屬於 json_annotation 套件。

進一步參考

#

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