09/04/2019
En el mundo del desarrollo con Dart y Flutter, la creación de clases de modelo (o data classes) es una tarea diaria. Sin embargo, puede convertirse rápidamente en un proceso tedioso y propenso a errores. Por cada modelo, necesitamos definir un constructor, propiedades, sobrescribir los métodos toString, operator ==, hashCode, e implementar un método copyWith para la clonación de objetos. Si a esto le sumamos la serialización y deserialización JSON, estamos hablando de cientos de líneas de código boilerplate que no aportan lógica de negocio y dificultan la legibilidad. Aquí es donde entra en juego Freezed, un generador de código que viene a rescatarnos de esta monotonía.

Freezed automatiza la creación de todo este código repetitivo, permitiéndonos enfocarnos exclusivamente en la definición de la estructura de nuestros datos. Nos ofrece clases inmutables por defecto, una implementación robusta de copyWith, manejo de uniones (union types) y una integración perfecta con json_serializable, convirtiéndose en una herramienta casi indispensable en cualquier proyecto moderno de Dart o Flutter.
¿Qué es Freezed y por qué deberías usarlo?
Freezed es un generador de código que, a partir de una definición de clase abstracta muy simple, crea todo el código necesario para tener una clase de datos completa y funcional. La diferencia es abismal. Mientras que una clase manual requiere un esfuerzo considerable, con Freezed solo necesitas definir la "fachada".
Imagina que quieres crear una clase Usuario. Manulamente, se vería así:
// Versión manual en Dart class Usuario { final String nombre; final int edad; const Usuario({required this.nombre, required this.edad}); Usuario copyWith({String? nombre, int? edad}) { return Usuario( nombre: nombre ?? this.nombre, edad: edad ?? this.edad, ); } @override String toString() => 'Usuario(nombre: $nombre, edad: $edad)'; @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is Usuario && other.nombre == nombre && other.edad == edad; } @override int get hashCode => nombre.hashCode ^ edad.hashCode; }Ahora, mira la misma clase definida con Freezed:
// La misma clase con Freezed import 'package:freezed_annotation/freezed_annotation.dart'; part 'usuario.freezed.dart'; @freezed abstract class Usuario with _$Usuario { const factory Usuario({ required String nombre, required int edad, }) = _Usuario; }¡Eso es todo! Freezed generará automáticamente el constructor, las propiedades inmutables, copyWith, toString, == y hashCode. Los beneficios son evidentes: menos código, mayor legibilidad y menos posibilidades de cometer errores.
Primeros Pasos: Instalación y Configuración
Para empezar a usar Freezed, necesitas añadir algunas dependencias a tu archivo pubspec.yaml. Típicamente, trabajarás con build_runner para ejecutar el generador de código.
Abre tu terminal y ejecuta:
# Para un proyecto Flutter flutter pub add freezed_annotation flutter pub add dev:freezed flutter pub add dev:build_runner # Si necesitas serialización JSON flutter pub add json_annotation flutter pub add dev:json_serializableUna vez instaladas las dependencias, la estructura de un archivo que use Freezed debe seguir este patrón:
import 'package:freezed_annotation/freezed_annotation.dart'; // Asocia este archivo con el código generado por Freezed part 'nombre_del_archivo.freezed.dart'; // Opcional: para serialización JSON part 'nombre_del_archivo.g.dart'; @freezed abstract class MiClase with _$MiClase { // ... tu definición aquí }Para activar el generador de código y que cree los archivos .freezed.dart y .g.dart, ejecuta el siguiente comando en tu terminal. El flag watch mantendrá el proceso activo, regenerando los archivos automáticamente cada vez que guardes un cambio.
dart run build_runner watch -dCreando Modelos Inmutables y Funcionales
La forma más común de usar Freezed es mediante constructores de tipo `factory`. Estos definen la "interfaz pública" de tu clase, y Freezed se encarga de la implementación privada.
copyWith y la Magia de la Copia Profunda (Deep Copy)
Una de las características más potentes es el método copyWith. Te permite clonar un objeto modificando solo las propiedades que te interesan, manteniendo la inmutabilidad del objeto original.
var usuario1 = Usuario(nombre: 'Ana', edad: 30); var usuario2 = usuario1.copyWith(edad: 31); // Crea una nueva instancia print(usuario1); // Usuario(nombre: Ana, edad: 30) print(usuario2); // Usuario(nombre: Ana, edad: 31)Pero Freezed va un paso más allá. Si tienes objetos anidados que también fueron creados con Freezed, puedes realizar copias profundas de una manera increíblemente elegante. Considera estas clases:
@freezed abstract class Empresa with _$Empresa { const factory Empresa({required String nombre, required Direccion direccion}) = _Empresa; } @freezed abstract class Direccion with _$Direccion { const factory Direccion({required String calle, required Ciudad ciudad}) = _Direccion; } @freezed abstract class Ciudad with _$Ciudad { const factory Ciudad({required String nombre, required String pais}) = _Ciudad; }Si quieres cambiar el país de la ciudad de una empresa, sin Freezed sería un anidamiento verboso. Con Freezed, es así de simple:
Empresa nuevaEmpresa = miEmpresa.copyWith.direccion.ciudad(pais: 'España');Esta sintaxis es limpia, legible y mucho menos propensa a errores que anidar múltiples llamadas a copyWith.
Añadiendo Métodos y Getters Personalizados
¿Qué pasa si necesitas añadir lógica personalizada a tu clase, como un getter que combine el nombre y la edad? Freezed lo permite. Solo necesitas añadir un constructor privado y vacío a tu clase. Esto le indica a Freezed que debe extender tu clase en lugar de implementarla, heredando así tus métodos personalizados.
@freezed abstract class Persona with _$Persona { // Constructor privado para permitir métodos personalizados const Persona._(); const factory Persona({ required String nombre, required String apellido, }) = _Persona; // Getter personalizado String get nombreCompleto => '$nombre $apellido'; // Método personalizado void saludar() { print('¡Hola, soy $nombreCompleto!'); } }El Poder de los Union Types para Manejar Estados
Quizás la funcionalidad más revolucionaria de Freezed son los union types (también conocidos como clases selladas o `sealed classes`). Te permiten definir una clase que puede tener varios estados o formas diferentes y mutuamente excluyentes. Esto es extremadamente útil para modelar estados de UI, respuestas de una API, o cualquier situación que pueda tener múltiples resultados posibles.

Imagina que estás haciendo una llamada a una API. La respuesta puede ser exitosa (con datos), puede estar cargando, o puede haber fallado (con un error).
@freezed abstract class Resultado with _$Resultado { const factory Resultado.cargando() = Cargando; const factory Resultado.exito(T data) = Exito; const factory Resultado.error(String mensaje) = Error; } Con esta definición, una variable de tipo Resultado solo puede ser una de estas tres opciones. Para manejar estos estados de forma segura y exhaustiva, usamos el pattern matching de Dart 3, que funciona a la perfección con las clases generadas por Freezed.
void manejarRespuesta(Resultado respuesta) { switch (respuesta) { case Cargando(): print('Mostrando indicador de carga...'); break; case Exito(data: final datos): print('Datos recibidos: $datos'); break; case Error(mensaje: final errorMsg): print('Ocurrió un error: $errorMsg'); break; } } El compilador de Dart se asegurará de que manejes todos los casos posibles, eliminando una categoría entera de errores en tiempo de ejecución.
Integración Perfecta con JSON
Hacer que tus clases de Freezed sean compatibles con json_serializable es trivial. Simplemente añade un constructor factory fromJson a tu clase y el `part` correspondiente al archivo generado.
import 'package:freezed_annotation/freezed_annotation.dart'; part 'producto.freezed.dart'; part 'producto.g.dart'; @freezed abstract class Producto with _$Producto { const factory Producto({ required int id, required String nombre, @JsonKey(name: 'precio_unitario') required double precio, }) = _Producto; factory Producto.fromJson(Map json) => _$ProductoFromJson(json); } Con esto, Freezed coordinará con json_serializable para generar los métodos fromJson y toJson, respetando incluso anotaciones como @JsonKey para personalizar los nombres de los campos en el JSON.
Tabla Comparativa: Freezed vs. Clase Dart Manual
| Característica | Clase Dart Manual | Clase con Freezed |
|---|---|---|
| Inmutabilidad | Manual (requiere final en todas las propiedades) | Automática y por defecto |
copyWith | Implementación manual, propensa a errores | Generado automáticamente, con soporte para copia profunda |
== y hashCode | Implementación manual, fácil de olvidar campos | Generados automáticamente, siempre correctos |
toString() | Implementación manual y simple | Generado automáticamente, con formato claro y útil |
| Union Types | Difícil de implementar de forma segura | Soporte nativo, potente y seguro |
| Líneas de Código | Muchas | Mínimas |
Preguntas Frecuentes (FAQ)
¿Freezed es solo para Flutter?
No. Freezed es una herramienta para el ecosistema Dart. Funciona perfectamente en cualquier proyecto de Dart puro, ya sea para un backend con Dart Frog, una aplicación de consola, o por supuesto, en proyectos de Flutter.
¿Cómo manejo propiedades que pueden ser nulas?
De la misma forma que lo harías en Dart estándar, utilizando el operador de nulabilidad ?. Por ejemplo: String? descripcion. Freezed y sus métodos generados, como copyWith, manejarán correctamente los valores nulos.
¿Qué pasa si necesito una clase mutable?
Aunque la inmutabilidad es una de sus grandes ventajas, Freezed también contempla casos de uso donde la mutabilidad es necesaria. Simplemente reemplaza la anotación @freezed por @unfreezed. Esto generará una clase con propiedades públicas y setters, a menos que marques explícitamente una propiedad con final.
¿El generador de código ralentiza el desarrollo?
El proceso de generación de código añade un paso a la compilación. Sin embargo, al usar el comando dart run build_runner watch -d, la regeneración es incremental y muy rápida, ocurriendo en segundo plano cada vez que guardas un archivo. Los enormes beneficios en productividad y seguridad del código suelen compensar con creces este pequeño coste de tiempo.
Conclusión
Freezed no es solo un generador de código; es un cambio de paradigma en cómo escribimos y pensamos sobre las clases de modelo en Dart. Al eliminar el código repetitivo, nos permite concentrarnos en lo que realmente importa: la lógica de nuestra aplicación. La seguridad que proporciona a través de la inmutabilidad y los union types, combinada con la conveniencia de métodos como copyWith, lo convierten en una herramienta esencial en el arsenal de cualquier desarrollador de Dart y Flutter. Si aún no lo has probado, te animo a que lo integres en tu próximo proyecto. No te arrepentirás.
Si quieres conocer otros artículos parecidos a Freezed: El superpoder para tus clases en Dart puedes visitar la categoría Juegos.
