Cuando trabajas con colecciones en Dart, es fundamental entender que List<T> no es solo una estructura de datos para almacenar elementos, sino que hereda de Iterable<T>. Esto significa que las listas poseen superpoderes de procesamiento funcional. En esencia, estos métodos te permiten transformar, filtrar y reducir datos de forma declarativa, centrándote en qué quieres obtener en lugar de cómo iterar manualmente cada elemento.
La clave del rendimiento en Dart reside en la evaluación perezosa (lazy evaluation). Métodos como map, where y expand no ejecutan la lógica de inmediato ni crean una nueva lista en memoria en el momento de llamarlos. En su lugar, devuelven un nuevo Iterable que actúa como una promesa: la transformación o el filtrado solo se ejecutará en el momento exacto en que comiences a consumir los elementos (por ejemplo, en un for-in o al llamar a toList()). Esto evita la creación de colecciones intermedias innecesarias, optimizando drásticamente el uso de memoria cuando encadenas múltiples operaciones.
Sin embargo, esta potencia tiene riesgos. Si intentas usar reduce sobre una lista vacía, la ejecución fallará porque reduce no tiene un valor inicial con el cual empezar a comparar. Por otro lado, si asumes que un map modifica la lista original, te llevarás una sorpresa: los métodos de Iterable son inmutables por diseño; siempre devuelven un nuevo objeto.
Para evitar desastres, debes saber cuándo materializar un Iterable usando toList() o toSet() para convertir esa promesa en datos reales, y cuándo usar fold en lugar de reduce para asegurar que tu código sea robusto ante colecciones vacías.
class Tarea {
final int id;
final String titulo;
final List<String> etiquetas;
final bool completada;
final int prioridad; // 1 (baja) a 5 (crítica)
Tarea(this.id, this.titulo, this.etiquetas, this.completada, this.prioridad);
}
void main() {
final tareas = [
Tarea(1, 'Configurar servidor', ['backend', 'devops'], true, 5),
Tarea(2, 'Escribir documentación', ['docs'], false, 3),
Tarea(3, 'Review de PR', ['code', 'backend'], false, 4),
Tarea(4, 'Fix bug crítico', ['hotfix', 'backend'], false, 5),
];
// 1. Transformación, filtrado y aplanado (Lazy)
// Aplicamos where para filtrar, expand para "aplanar" las etiquetas
// de cada tarea en un solo flujo, y map para normalizar texto.
final etiquetasPrioritarias = tareas
.where((t) => t.prioridad >= 4)
.expand((t) => t.etiquetas)
.map((tag) => tag.toUpperCase());
// Hasta aquí, 'etiquetasPrioritarias' es un Iterable perezoso.
// No se ha procesado ninguna tarea aún.
// 2. Materialización y eliminación de duplicados
// Al llamar a toSet(), obligamos a Dart a ejecutar la cadena anterior.
final tagsUnicos = etiquetasPrioritarias.toSet();
// 3. Reducción segura con fold
// Usamos fold para sumar la cantidad de etiquetas de tareas prioritarias.
// 'fold' es seguro con listas vacías porque requiere un valor inicial (0).
final totalEtiquetasPrioritarias = tareas
.where((t) => t.prioridad >= 4)
.fold<int>(0, (acumulado, t) => acumulado + t.etiquetas.length);
// 4. Comprobaciones booleanas
final hayBackend = tareas.any((t) => t.etiquetas.contains('backend'));
final todasCompletadas = tareas.every((t) => t.completada);
// 5. Búsqueda específica con valor por defecto
final tareaUrgente = tareas.firstWhere(
(t) => t.prioridad == 5,
orElse: () => Tarea(0, 'Sin tarea', [], false, 0),
);
// 6. Segmentación (Take y Skip)
// Saltamos la primera y tomamos las siguientes 2
final subConjunto = tareas.skip(1).take(2).toList();
print('Etiquetas únicas prioritarias: $tagsUnicos');
print('Total etiquetas en tareas de prioridad alta: $totalEtiquetasPrioritarias');
print('¿Alguna tarea tiene "backend"?: $hayBackend');
print('¿Están todas terminadas?: $todasCompletadas');
print('Tarea más urgente: ${tareaUrgente.titulo}');
print('Subconjunto (skip 1, take 2): ${subConjunto.map((t) => t.titulo).toList()}');
}
Análisis del flujo de datos
Fíjate cómo la variable etiquetasPrioritarias no es una lista, sino un objeto Iterable. Cuando ejecutamos expand((t) => t.etiquetas), estamos realizando lo que en otros lenguajes se conoce como flatMap: transformamos cada Tarea en un Iterable<String> y luego aplanamos todos esos iterables en un único flujo continuo de etiquetas.
Es vital la distinción entre fold y reduce que aplicamos en totalEtiquetasPrioritarias. Mientras que reduce toma el primer elemento de la lista como base para la operación, fold nos permite definir un valor semilla (en este caso, 0). Si tareas.where(...) hubiera devuelto una colección vacía, reduce habría lanzado una excepción de estado, mientras que fold simplemente habría devuelto 0.
En la búsqueda con firstWhere, hemos pasado un parámetro orElse. Esto es una práctica esencial en producción; sin él, si la condición t.prioridad == 5 no se cumple, Dart lanzaría una excepción. Al proporcionar un objeto por defecto, hacemos que el flujo sea determinista y seguro. Finalmente, toSet() actúa como el punto de materialización que “empuja” los datos a través de la tubería de transformaciones que definimos previamente.
El error frecuente
Un error clásico ocurre al intentar usar reduce en listas que podrían estar vacías debido a un filtrado previo.
final precios = [100, 200, 300];
// Intentar sumar mediante un filtro que puede dejar la lista vacía
final total = precios
.where((p) => p > 1000) // Esto devuelve un Iterable vacío
.reduce((a, b) => a + b); // ❌ Lanza: StateError (Bad state: No element)
// La solución correcta es usar fold
final totalSeguro = precios
.where((p) => p > 1000)
.fold<int>(0, (a, b) => a + b); // ✅ Retorna 0 sin errores
N° 33