Овладение Freezed в Flutter: Как писать неизменяемый, безопасный с точки зрения типов и масштабируемый код как профессионал

Овладение 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 может быть ровно одним из:

  • AuthLoading
  • AuthenticatedUser)
  • Unauthenticated
  • AuthErrorString сообщением)

Идеально для управления состоянием и асинхронных потоков.

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 не просто полезен. Это необходимость.

Чем больше масштабируется ваш проект, тем больше вы будете благодарны себе за то, что с самого начала пошли по пути неизменяемости, герметичности и типизированной безопасности.

🧰 Хотите углубиться?

🧩 Бонус: До/После

До:

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 необычным или творческим способом? Поделитесь своими мыслями или уроками в комментариях — мне тоже будет интересно узнать что-то новое из вашего опыта!

Report Page