借助 fromJson、toJson、copyWith 以及可靠的值相等,把 JSON 转换成不可变的 Dart 类。
在 Kodokon 中打开本课到了这个阶段,真正的主题不再是类的语法,而是外部世界与你的代码之间的边界。只要一份 API 响应还以 Map<String, dynamic> 的形式四处传递,每一次读取字段都是一场运行时的赌博。专业的做法是:在收到 JSON 的那一刻就把它转换成类型化对象,之后只传递这些对象。再把这一点和不可变性结合起来 - final 字段、const 构造函数 - 你就能消除一整类 bug:再也没有人能在你背后修改一个共享对象,而两个相同的 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 num).toInt(),而不是 json['id'] as int?as int 在运行时比 as num 慢。3.0:直接转换成 int 会在遇到 double 时失败。json['id'] 总是一个字符串。copyWith 实现下,user.copyWith(email: null) 会产生什么结果?email 字段如所要求的那样被重置为 null。email:null ?? this.email 退回到原有的值。== 和 hashCode?const 实例的创建速度。final,这就是强制要求的。