Овладение Freezed в Flutter: Как писать неизменяемый, безопасный с точки зрения типов и масштабируемый код как профессионал
FlutterPulseЭта статья переведена специально для канала FlutterPulse. В этом канале вы найдёте много интересных вещей, связанных с Flutter. Не забывайте подписываться! 🚀

🧠 Крючок: "Я думал, что Freezed нужен только для классов данных…"
🧠 Крючок: "Я думал, что Freezed нужен только для классов данных…"
Это было со мной, два проекта назад. Я использовал Freezed только для избежания шаблонного кода в моделях данных — copyWith, равенство, hashCode, обычные вещи.
Но затем я столкнулся с проблемой в сложной настройке управления состоянием BLoC. В приложении было 7+ состояний, несколько вложенных моделей и смесь синхронных и асинхронных ошибок. Это было беспорядок.
Именно тогда Freezed перешел от "просто приятная опция" к "о, вот это спасло мой весь поток состояний."
Если вы только начинали, давайте поднимемся на новый уровень вместе. 💪
🚨 Почему Freezed?
Проблема с изменяемыми данными
Dart по умолчанию поощряет изменяемые классы. Это может тихо вводить ошибки в больших кодовых базах:
user.name = "New Name"; // Происходит где-то... но где?
Этот вид тихого изменения опасен, особенно когда вы работаете с общим состоянием между виджетами или слоями.
Freezed на помощь 🧊
Freezed делает ваши данные неизменяемыми по умолчанию. Вы не можете их изменять — вы клонируете с намерением:
final updatedUser = user.copyWith(name: "New Name");
Плюс, вы получаете:
- 🔄 Автоматическое
copyWith - 🧪 Глубокое равенство и hashCode
- 🔀 Объединенные типы и закрытые классы
- 🎭 Соответствие шаблону
- 📦 Сериализация JSON
- 💥 Уничтожение шаблонного кода
⚙️ Настройка и конфигурация
Сначала добавьте эти зависимости в ваш pubspec.yaml:
dependencies: freezed_annotation: ^2.4.1 json_annotation: ^4.8.1 dev_dependencies: build_runner: ^2.4.6 freezed: ^2.4.6 json_serializable: ^6.7.1
Теперь создайте свою модель:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required String id,
required String name,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
Запустите:
flutter pub run build_runner build --delete-conflicting-outputs
Вы получите:
user.freezed.dart: неизменяемость, равенство, объединения, сопоставление с образцомuser.g.dart: сериализация JSON
✅ Чисто. Типобезопасно. Без шаблонного кода.
🔍 Подробный разбор: функции, которые вы, вероятно, недооцениваете
1. Объединения/Запечатанные классы
В основе своей объединение или запечатанный класс в Freezed представляет собой значение, которое может быть одним из многих различных, предопределённых типов — каждый со своими собственными связанными данными (или без них).
Freezed делает эту концепцию очень удобной в Dart.
@freezed
class AuthState with _$AuthState {
const factory AuthState.loading() = AuthLoading;
const factory AuthState.authenticated(User user) = Authenticated;
const factory AuthState.unauthenticated() = Unauthenticated;
const factory AuthState.error(String message) = AuthError;
}
Здесь AuthState может быть ровно одним из:
AuthLoadingAuthenticated(сUser)UnauthenticatedAuthError(сStringсообщением)
Идеально для управления состоянием и асинхронных потоков.
2. Сопоставление с образцом
Freezed предоставляет вам мощные инструменты для сопоставления с образцом, которые позволяют вам реагировать на каждое состояние или вариант объединения/запечатанного класса чистым и читаемым способом.
state.when( loading: () => showSpinner(), authenticated: (user) => showHome(user), unauthenticated: () => showLogin(), error: (msg) => showError(msg), );
Или maybeWhen если вы хотите логику резервного копирования.
Это заменяет грязную логику if/else или switch/case которая полагается на проверку runtimeType или булевых флагов. Вместо этого вы получаете:
- 🧠 Безопасность на этапе компиляции: Если вы добавите новое состояние и забудете его обработать, компилятор выдаст предупреждение.
- 😌 Больше нет проверок на null: Вам не нужно вручную проверять, если
user != nullилиstate is Authenticated. - ✨ Чище, декларативное отображение UI
3. CopyWith & глубокая неизменяемость
Неизменяемость — это здорово, но обновление вложенных значений может стать проблемой, если у вас нет copyWith
final updated = user.copyWith( name: "Updated Name", profile: user.profile.copyWith(age: 30), );
Это даёт вам:
- 🔒 Безопасность неизменяемости: отсутствие случайных мутаций.
- 🧼 Предсказуемые обновления: вы знаете, что и где изменилось.
- ✨ Лаконичный синтаксис: не нужно переназначать каждое поле.
И да — Freezed автоматически генерирует глубокий copyWith для всех вложенных Freezed-моделей. 🙌
Попрощайтесь с глубоко вложенной логикой мутаций.
4. Композиция и вложенные модели
Предположим, вы создаёте модель User, и у этого пользователя есть вложенная модель Profile. С Freezed вы можете четко определить и повторно использовать эту вложенную структуру:
@freezed
class Profile with _$Profile {
const factory Profile({
required int age,
required String bio,
}) = _Profile;
factory Profile.fromJson(Map<String, dynamic> json) => _$ProfileFromJson(json);
}
Теперь скомпонуем это в другую модель:
@freezed
class User with _$User {
const factory User({
required String id,
required String name,
required Profile profile,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
🔑 Преимущества такого подхода:
- 🔄 Многоразовые строительные блоки: используйте Profile и в других местах (например, для администраторов, гостей и т. д.).
- 🧪 Безопасность типов во всем дереве: user.profile.age всегда присутствует — никаких значений NULL, если вы их не определите.
- 🔧 Упрощенное тестирование и имитация: вы можете создавать изолированные экземпляры Profile и User для тестов.
А поскольку оба поддерживают сериализацию JSON и copyWith, эта структура на 100% готова к управлению API и состоянием.
🔗 Freezed + JSON сериализация
Если вы работаете с API, вот что вам нужно::
Вложенные модели
@freezed
class Post with _$Post {
const factory Post({
required String title,
required User author,
}) = _Post;
factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
}
Пользовательские конвертеры
Если вы создаете что-то серьезное в Flutter — особенно с асинхронной логикой, сложным состоянием или API-слоями — Freezed не просто полезен. Это необходимость.
Чем больше масштабируется ваш проект, тем больше вы будете благодарны себе за то, что с самого начала пошли по пути неизменяемости, герметичности и типизированной безопасности.
🧰 Хотите углубиться?
- sealed_unions [https://pub.dev/packages/sealed_unions]
- equatable [https://pub.dev/packages/equatable]
- dartz [https://pub.dev/packages/dartz]
🧩 Бонус: До/После
До:
class User {
String name;
int age;
User(this.name, this.age);
bool operator ==(Object other) => ... // 😵
User copyWith({String? name, int? age}) => ... // 😵
}
После:
@freezed
class User with _$User {
const factory User({required String name, required int age}) = _User;
}
Вы только что перешли от "сделай сам" к "профи."
💬 А у вас как? Использовали ли вы Freezed необычным или творческим способом? Поделитесь своими мыслями или уроками в комментариях — мне тоже будет интересно узнать что-то новое из вашего опыта!