fromJson、toJson、copyWith、そして信頼できる値の等価性を使って、JSONを不変なDartのクラスへと変換しましょう。
このレッスンを Kodokon で開くこのレベルでは、本当のテーマはもはやクラスの構文ではなく、外の世界とあなたのコードとの境界です。APIのレスポンスがMap<String, dynamic>のまま持ち回されている限り、フィールドを読むたびに実行時の賭けをしていることになります。プロフェッショナルの原則は、JSONを受け取ったその瞬間に型付きのオブジェクトへ変換し、それ以降はそのオブジェクトだけを持ち回すことです。これを不変性、つまりfinalフィールドとconstコンストラクタと組み合わせれば、まるごと一群のバグを取り除けます。共有されたオブジェクトを誰かが裏であなたに無断で変更することはもうできなくなりますし、同一のconstインスタンス二つはコンパイラによって正規化されます。
class User {
const User({
required this.id,
required this.name,
this.email,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: (json['id'] as num).toInt(),
name: json['name'] as String,
email: json['email'] as String?,
);
}
final int id;
final String name;
final String? email;
}三つの選択について、はっきり説明しておく価値があります。まず(json['id'] as num).toInt()について。多くのAPIは、バックエンドによって整数を3とシリアライズしたり3.0とシリアライズしたりします。intへの直接のキャストはdoubleに対してクラッシュします。次にas Stringは、フィールドが欠けていれば明確なTypeErrorとともにすぐに失敗します。これは意図的なものです。パースの時点で早く失敗するほうが、nullが伝播して三画面あとで爆発するよりましです。最後にas String?は、そのフィールドが本当に省略可能であることを型のなかに明記します。
class User {
const User({required this.id, this.email});
final int id;
final String? email;
Map<String, dynamic> toJson() => {
'id': id,
'email': email,
};
User copyWith({int? id, String? email}) {
return User(
id: id ?? this.id,
email: email ?? this.email,
);
}
}不変性は値の等価性を必要とします。デフォルトでは、Dartは参照を比較します。フィールドごとに同一な二つのUserは「異なる」とみなされてしまいます。これは状態を比較するその瞬間に、テストのなかで、Providerのselectのなかで、リストの検索のなかで、具体的な問題になります。==とhashCodeは必ず一緒に再定義しましょう。
class Point {
const Point(this.x, this.y);
final int x;
final int y;
@override
bool operator ==(Object other) =>
other is Point && other.x == x && other.y == y;
@override
int get hashCode => Object.hash(x, y);
}json['id'] as intではなく(json['id'] as num).toInt()と書くのはなぜですか?as intは実行時にas numより遅いから。3.0とシリアライズすることがあり、intへの直接のキャストはdoubleに対して失敗するから。json['id']は常に文字列だから。copyWithの実装で、user.copyWith(email: null)は何を生み出しますか?emailフィールドは要求どおりnullにリセットされる。emailを保つ。null ?? this.emailは既存の値へフォールバックする。==とhashCodeを再定義するのはなぜですか?constインスタンスの生成を速くするため。finalと宣言された時点で必須になるから。