For years, I added freezed to almost every Flutter project I started, without really asking myself whether I needed it. It was just what you did.
Over time, I started noticing the cost. In this article I'll walk through the problems I ran into, and the plain Dart 3 alternatives I use today.
The problems I ran into
1. Code generation time
Every time I add a field or a new state, I have to run:
dart run build_runner build --delete-conflicting-outputs
And then wait. On a small project that's tolerable. On a large one it happens dozens of times a day, and every time it breaks your focus.
2. The project becomes coupled to the package
In my projects, nearly every state and model depended on freezed. When a new version of the package came out, it caused errors in the generated code across multiple files, and I lost hours fixing them instead of building features. That actually happened to me, and it's the moment I started questioning how much I should depend on it.
3. The architectural problem
In Clean Architecture, dependencies point inward only: nothing in an inner layer should know anything about an outer layer. That's why the Domain layer, the core of your app, should be pure Dart as much as possible.
If your domain entities and states depend on a code generator, your business logic is tied to a tool that can change or break. The most important part of your app ends up depending on something it shouldn't know about.
The alternatives
Union states
With freezed:
@freezed
class AuthState with _$AuthState {
const factory AuthState.initial() = AuthInitial;
const factory AuthState.loading() = AuthLoading;
const factory AuthState.success(User user) = AuthSuccess;
const factory AuthState.failure(String message) = AuthFailure;
}
The same thing with plain Dart 3, no generation:
sealed class AuthState {
const AuthState();
}
final class AuthInitial extends AuthState {
const AuthInitial();
}
final class AuthLoading extends AuthState {
const AuthLoading();
}
final class AuthSuccess extends AuthState {
const AuthSuccess(this.user);
final User user;
}
final class AuthFailure extends AuthState {
const AuthFailure(this.message);
final String message;
}
And the compiler makes sure you handle every case in a switch:
Widget build(AuthState state) => switch (state) {
AuthInitial() => const LoginForm(),
AuthLoading() => const CircularProgressIndicator(),
AuthSuccess(:final user) => HomePage(user: user),
AuthFailure(:final message) => ErrorView(message),
};
If you add a new state and forget to handle it, you get a compile error immediately. This is the main reason I stopped missing freezed's unions: the language now does the job itself.
Value equality
You can use equatable, or write == and hashCode by hand if you want your Domain layer to have zero dependencies:
class UserData with EquatableMixin {
const UserData({required this.name, required this.age});
final String name;
final int age;
@override
List<Object?> get props => [name, age];
}
copyWith
You have two options. The first is copy_with_extension_gen. You annotate the class with @CopyWith() and it generates the method. It's much lighter than freezed because it generates just that one piece:
@CopyWith()
class UserData {
const UserData({required this.name, required this.age});
final String name;
final int age;
}
The second is to write it yourself, with no package at all:
UserData copyWith({String? name, int? age}) =>
UserData(name: name ?? this.name, age: age ?? this.age);
I use the package only for classes with many fields, and write it by hand everywhere else.
Isn't it more code to write?
Yes, but that's no longer a real problem. In the age of AI assistants, writing boilerplate takes seconds. Whatever tool you use can write copyWith, ==, or a whole set of sealed subclasses for you in moments, with no package involved.
In return, you get ordinary Dart code that's easy to read, change, and debug, and that doesn't force a specific shape on your classes just to keep a generator happy. The one rule: review generated code the way you'd review any other code.
Conclusion
freezed is a powerful tool, not a bad one, and plenty of teams get real value from it. But I've started asking one question before adding any dependency: what will relying on this cost me a year or two from now?
That matters most in the Domain layer. The fewer dependencies it has, the easier your project is to maintain and test.
Are you still using freezed, or have you moved to something else? I'd love to hear about your experience in the comments.

Top comments (0)