Преврати JSON в неизменяемые классы Dart с fromJson, toJson, copyWith и надёжным сравнением по значению.
Открыть этот урок в 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, одинаковых поле в поле, считаются «разными». Это вполне конкретная проблема, как только ты начинаешь сравнивать состояния - в тестах, в select из Provider, при поиске по списку. Переопределяй == и 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'] всегда строка.user.copyWith(email: null) при классической реализации copyWith?email сбрасывается в null, как и запрошено.email: null ?? this.email возвращается к существующему значению.== и hashCode на неизменяемой модели?const.final.