En Dart, a diferencia de lenguajes como Java o C#, no existe la sobrecarga de constructores (es decir, no puedes definir varios constructores con el mismo nombre si cambian los tipos de parámetros). Para resolver esto, el lenguaje utiliza constructores con nombre. Un constructor con nombre es un constructor estándar que tiene un identificador único mediante la sintaxis Clase.nombre(), lo que permite definir múltiples formas de instanciar una clase con propósitos semánticamente distintos.
Esto funciona así porque el nombre del constructor actúa como una etiqueta que comunica la intención del desarrollador: no es lo mismo llamar a un constructor por defecto que a uno que indique una conversión desde un mapa o un estado vacío. Debes usarlos siempre que la lógica de creación dependa de una fuente de datos externa o de un estado inicial específico, como implementar la convención .fromJson para deserialización o .empty para objetos con valores por defecto. Si ignoras este mecanismo y tratas de usar un único constructor con decenas de parámetros opcionales, terminarás con un código difícil de leer y propenso a errores de lógica. El error más crítico ocurre cuando asumes que la herencia funciona igual que los métodos: los constructores nombrados no se heredan. Si una clase padre tiene un Padre.desdeMapa(), la clase hija no tendrá acceso a ese constructor automáticamente; si intentas llamarlo desde la subclase sin declararlo explícitamente, el compilador lanzará un error.
class Usuario {
final String id;
final String email;
final bool esPremium;
// Constructor principal
Usuario(this.id, this.email, {this.esPremium = false});
// Constructor nombrado para un estado por defecto (convención .empty)
Usuario.invitado()
: id = '0',
email = 'invitado@ejemplo.com',
esPremium = false;
// Constructor nombrado para deserialización (convención .fromJson)
// [disponible desde Dart 2.12] - Sound null safety
Usuario.fromJson(Map<String, dynamic> json)
: id = json['id'] as String,
email = json['email'] as String,
esPremium = json['esPremium'] as bool? ?? false;
}
class Administrador extends Usuario {
final int nivelAcceso;
// Uso de super parameters para simplificar la delegación al padre
// [disponible desde Dart 2.17]
Administrador(super.id, super.email, {this.nivelAcceso = 1})
: super(esPremium: true);
// Los constructores nombrados deben declararse explícitamente en la subclase
// si se desea mantener la capacidad de instanciación desde JSON.
Administrador.fromJson(Map<String, dynamic> json)
: super.fromJson(json), // Delegamos la lógica de los campos heredados
nivelAcceso = json['nivelAcceso'] as int? ?? 1;
}
void main() {
// Instancia usando un constructor nombrado de la clase base
final invitado = Usuario.invitado();
// Instancia usando un constructor nombrado en la subclase
final admin = Administrador.fromJson({
'id': 'ADM-99',
'email': 'root@sistema.local',
'esPremium': true,
'nivelAcceso': 10,
});
print('Usuario invitado: ${invitado.email}');
print('Admin: ${admin.email}, Nivel: ${admin.nivelAcceso}');
}
Desglose del ejemplo
En el código anterior, fíjate cómo Usuario.invitado() utiliza una lista de inicialización (la sección después de los :) para asignar valores constantes sin necesidad de ejecutar un cuerpo de función {}. Esto es más eficiente, ya que la VM de Dart puede optimizar la asignación de campos finales durante la fase de construcción.
Cuando definimos Usuario.fromJson, estamos aplicando la convención estándar para la deserialización. Al usar as String o as bool?, estamos realizando type casting explícito para asegurar que la seguridad de tipos de Dart se mantenga incluso cuando los datos vienen de un mapa dinámico.
En la clase Administrador, el uso de super parameters (super.id, super.email) es una mejora sintáctica que elimina la necesidad de escribir this.id = id y luego pasar id al super(). El compilador entiende que estos parámetros deben ser pasados directamente al constructor de Usuario.
Un punto clave es Administrador.fromJson. Al llamar a super.fromJson(json), estamos reutilizando la lógica de construcción de la clase padre. Esto es fundamental para mantener la integridad del objeto: si la lógica para extraer un email cambia, solo la modificamos en Usuario y la subclase heredará el comportamiento correctamente sin duplicar código.
El error frecuente
Un error común es intentar acceder a un constructor nombrado de la clase padre desde una instancia de la clase hija sin haberlo re-implementado.
// ERROR: Esto fallará en tiempo de compilación // final admin = Administrador.invitado();
Aunque Administrador es un Usuario, no “hereda” el constructor .invitado(). Si tu arquitectura requiere que todos los tipos de Usuario puedan crearse como invitados, debes declararlo en Administrador llamando a super.invitado().
N° 50