Las extension methods [disponible desde Dart 2.7] son una forma de añadir funcionalidad a clases que ya existen, sin necesidad de heredar de ellas o modificar su código fuente original. Básicamente, “inyectas” nuevos métodos, getters, setters o incluso operadores en tipos que no te pertenecen, como String, List o incluso tipos de librerías de terceros.
Esto funciona mediante un mecanismo de despacho estático (static dispatch). A diferencia de la herencia tradicional, donde el método que se ejecuta se decide en tiempo de ejecución basándose en el tipo real del objeto, las extensiones se resuelven en tiempo de compilación. El compilador simplemente busca si hay una extensión disponible que coincida con el tipo del receptor y el nombre del método; si la encuentra, “conecta” la llamada a tu código.
Deberías usar extensiones cuando te encuentres escribiendo clases de utilidad infinitas (tipo StringUtils o ListHelper) que solo sirven para aplicar lógica específica sobre tipos conocidos, o cuando quieras mejorar la legibilidad del código haciendo que una operación parezca una característica nativa del lenguaje. Sin embargo, ten cuidado: si intentas definir un método que ya existe en la clase original, el compilador ignorará tu extensión y usará siempre el método de la clase, lo que puede causar comportamientos confusos si no comprendes la jerarquía de resolución.
import 'dart:convert';
// Extension con nombre para tipos no-nullables
extension StringValidator on String {
bool get isEmail => contains('@') && contains('.');
bool get isNumeric => double.tryParse(this) != null;
}
// Extension para tipos nullables (String?)
extension NullableStringExt on String? {
bool get isPresent => this != null && this.isNotEmpty;
}
// Extension genérica sobre un tipo Iterable<T>
extension CollectionSummary<T> on Iterable<T> {
String summarize() {
final count = length;
return 'Colección con $count elementos';
}
}
void main() {
// Uso de StringValidator (no-nullable)
const emailInput = 'dev@example.com';
if (emailInput.isEmail) {
print('Email válido: $emailInput');
}
// Uso de NullableStringExt (manejo de nulos)
String? inputNulo = null;
print('¿El input está presente?: ${inputNulo.isPresent}');
// Uso de CollectionSummary (genérica)
final lista = ['Dart', 'Extensiones', 'Estructuras'];
print(lista.summarize());
// Uso de una extensión que simula un campo mediante un getter
// Nota: No puedes declarar variables de instancia, solo getters.
extension StringFormat on String {
String get uppercase => strtoupper();
}
print('UPPERCASE: ${emailInput.uppercase}');
}
// Helper para el ejemplo anterior
String strtoupper() => 'TODO MAYUSCULAS'; // Simplificación para el ejemplo
En el código anterior, observa cómo StringValidator añade isEmail y isNumeric directamente a cualquier String. El compilador sabe que cuando escribes emailInput.isEmail, debe buscar en las extensiones cargadas. Por otro lado, NullableStringExt es particularmente potente porque actúa sobre String?; esto nos permite evitar el típico chequeo if (variable != null && variable.isNotEmpty) para algo tan limpio como variable.isPresent.
La extensión CollectionSummary<T> demuestra la versatilidad de los tipos genéricos en las extensiones. Al usar <T> y extender de Iterable<T>, la función summarize está disponible para cualquier lista, conjunto (Set) o iterable, sin importar qué tipo de datos contenga, manteniendo la integridad de los tipos.
El error frecuente
Un error común es intentar añadir campos de instancia (variables) a una clase mediante una extensión. Por ejemplo, intentar hacer esto:
// ESTO DARÁ UN ERROR DE COMPILACIÓN
extension BadExtension on String {
String myProperty = 'Hola';
}
Las extensiones no pueden añadir estado al objeto; solo pueden añadir comportamiento. No puedes almacenar datos nuevos en el objeto original. Si necesitas que parezca una propiedad, debes usar un getter que devuelva un valor calculado o constante, pero nunca una variable que persista en el objeto, ya que la instancia original de String no tiene espacio en memoria para tu nueva variable.
Asimismo, si defines un método llamado isEmpty en tu extensión, este será ignorado siempre que llames a string.isEmpty(), porque el método original de la clase String tiene prioridad absoluta sobre las extensiones.
Dominar las extensiones es clave para escribir código que se sienta parte del lenguaje, manteniendo la lógica de negocio cerca de los tipos que la operan.
N° 59