Índice del curso

Dart desde cero hasta experto

Un tutorial secuencial y profundo del lenguaje Dart: instalación en Windows y Linux, fundamentos, POO, null safety, las novedades de Dart 3 y programación asíncrona. Todo lo necesario para llegar a Flutter dominando el lenguaje.

38 capítulos Bootstrap 5.3 Modo claro / oscuro Optimizado para móvil Dart 3.x
38
Capítulos
150+
Ejemplos de código
3
Niveles: básico a experto
0
Requisitos previos
Cómo usar este tutorial: sigue los capítulos en orden (el índice está en el menú si lees desde el móvil). Cada capítulo tiene objetivos, teoría, ejemplos ejecutables y puntos clave al final. Escribe y ejecuta cada ejemplo: la práctica es la única vía para dominar un lenguaje.

1 · ¿Qué es Dart?

Básico ~10 min

Dart es un lenguaje de programación creado por Google en 2011 (por el equipo de Lars Bak y Kasper Lund, los mismos ingenieros detrás del motor V8 de Chrome). Está optimizado para construir aplicaciones cliente: apps móviles, web, escritorio y de línea de comandos. Es, además, el lenguaje oficial de Flutter, uno de los frameworks multiplataforma más populares del mundo.

¿Por qué aprender Dart antes de Flutter?

  • En Flutter todo es Dart. Un botón es una clase, una pantalla es una clase, un estado es una clase…
  • Los layouts usan colecciones y funciones (map, where, spreads).
  • Las llamadas a APIs usan async/await y Futures.
  • Dominar primero el lenguaje te hará el doble de rápido aprendiendo Flutter.

Características clave

Tipado estático con inferencia: el compilador detecta errores antes de ejecutar, pero escribes poco código extra.
Null Safety: imposible sufrir el clásico error «null reference» si sigues las reglas del lenguaje.
100 % orientado a objetos: incluso los números y las funciones son objetos.
Asíncrono de primera clase: Future, Stream, async/await integrados en el lenguaje.
JIT y AOT: desarrollo con recarga instantánea (hot reload) y producción compilada a código nativo.
Multiplataforma: Android, iOS, Windows, macOS, Linux, Web y servidores.
JIT vs AOT: durante el desarrollo, Dart usa compilación Just-In-Time (compila mientras ejecuta, permitiendo el famoso hot reload). Para publicar, compila Ahead-Of-Time a código máquina nativo: apps rápidas y sin dependencias.

Dart frente a otros lenguajes

LenguajeSemejanza con Dart
Java / C#Sintaxis muy familiar: clases, interfaces, genéricos. Dart añade inferencia y null safety moderno.
KotlinMuy parecidos en filosofía: concisos, null-safe, con expresiones when/switch potentes.
JavaScript / TSFunciones de primera clase, closures y async/await idénticos; pero con tipos estáticos reales.
PythonList comprehensions equivalentes a colecciones + funciones de orden superior.

Un vistazo rápido al lenguaje

No te preocupes si no entiendes nada todavía: este programa combina clases, colecciones, funciones flecha y bucles. Al terminar el curso lo leerás como un libro abierto.

// Un primer vistazo. Todo esto se explica capítulo por capítulo.
class Usuario {
  final String nombre;
  final int edad;

  Usuario({required this.nombre, required this.edad});

  bool get esMayorDeEdad => edad >= 18;

  @override
  String toString() => 'Usuario($nombre, $edad)';
}

void main() {
  final usuarios = [
    Usuario(nombre: 'Ana', edad: 34),
    Usuario(nombre: 'Luis', edad: 15),
  ];

  final adultos = usuarios.where((u) => u.esMayorDeEdad).toList();

  for (final u in adultos) {
    print('Hola $u');
  }
}
Hola Usuario(Ana, 34)
Prueba sin instalar nada: en DartPad puedes escribir y ejecutar Dart desde el navegador. Ideal para los primeros capítulos.

Puntos clave

  • Dart es el lenguaje de Flutter: dominarlo acelera tu camino hacia las apps móviles.
  • Es tipado estático con inferencia, null-safe, orientado a objetos y asíncrono.
  • Compila JIT para desarrollar (hot reload) y AOT nativo para producción.

2 · Instalación paso a paso (Windows y Linux)

Básico ~15 min

El Dart SDK incluye todo lo necesario para programar: el compilador, la máquina virtual, el gestor de paquetes pub y herramientas como dart format o dart analyze.

2.1 · Instalación en Windows

Tienes tres caminos. Elige uno:

  1. Abre PowerShell como administrador.
  2. Ejecuta:
choco install dart-sdk choco upgrade dart-sdk # para actualizar más adelante

scoop install dart scoop update dart # actualizaciones futuras

  1. Descarga el zip del SDK desde dart.dev/get-dart/archive.
  2. Descomprímelo, por ejemplo en C:\dev\dart-sdk.
  3. Pulsa Windows, escribe «variables de entorno» y abre Editar las variables de entorno del sistemaVariables de entorno…
  4. En la variable Path (de tu usuario) pulsa EditarNuevo y añade C:\dev\dart-sdk\bin.
  5. Acepta todo y abre una terminal nueva para que el cambio tenga efecto.

2.2 · Instalación en Linux

Debian / Ubuntu / Mint (repositorio oficial):

sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg wget

wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo gpg --dearmor -o /usr/share/keyrings/dart.gpg

echo 'deb [signed-by=/usr/share/keyrings/dart.gpg arch=amd64] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list

sudo apt-get update && sudo apt-get install -y dart

Cualquier otra distribución (Fedora, Arch…) o si prefieres no tocar repositorios: descarga el tarball desde el archivo oficial, descomprímelo y añade su carpeta bin al PATH:

# ejemplo con el SDK descomprimido en ~/herramientas
mkdir -p ~/herramientas && cd ~/herramientas
# descarga el .zip adecuado a tu arquitectura (x64 o arm64) y luego:
unzip dartsdk-linux-x64.zip

# añade al PATH permanentemente (bash):
echo 'export PATH="$PATH:$HOME/herramientas/dart-sdk/bin"' >> ~/.bashrc
source ~/.bashrc

# si usas zsh, hazlo en ~/.zshrc; en fish: fish_add_path ~/herramientas/dart-sdk/bin
Nota: si instalas Flutter más adelante, este trae su propio Dart incluido. No hay conflicto, pero conviene saber cuál está usando tu terminal con which dart (Linux/macOS) o where dart (Windows).

2.3 · Verifica la instalación

En cualquier sistema, abre una terminal nueva:

dart --version
Dart SDK version: 3.5.4 (stable) on "linux_x64"

Si ves un error tipo «command not found», el PATH no se aplicó: revisa la ruta y abre una terminal nueva.

2.4 · Editor recomendado: Visual Studio Code

  1. Instala VS Code.
  2. Abre la pestaña de extensiones (Ctrl+Shift+X).
  3. Busca e instala «Dart» (editorial Dart Code). Para Flutter instalarás también «Flutter», pero aún no hace falta.
  4. La extensión detecta el SDK automáticamente. Aporta autocompletado, análisis en vivo, depurador, refactorizaciones y formateo.
  5. Opcional muy útil: en ajustes activa Editor: Format On Save.

Puntos clave

  • Instala el Dart SDK con tu gestor favorito o manualmente añadiendo bin al PATH.
  • Verifica siempre con dart --version tras instalar.
  • VS Code + extensión Dart = experiencia completa gratis (análisis, debug, formato).

3 · Tu primer programa y el entorno

Básico ~15 min

3.1 · El clásico «Hola, mundo»

Crea un archivo llamado hola.dart y escribe:

void main() {
  print('Hola, mundo');
}

Ejecútalo desde la terminal, en la carpeta del archivo:

dart run hola.dart
Hola, mundo

Todo programa Dart empieza a ejecutarse en la función main(). print() muestra texto en la consola. En los próximos capítulos desmenuzaremos cada palabra de este código.

3.2 · Crear un proyecto real con dart create

Para proyectos serios usamos el generador oficial:

dart create mi_app cd mi_app && dart run
Building package executable... Built mi_app:mi_app. Hola mundo: 42!

La estructura generada es el estándar profesional:

mi_app/ ├── analysis_options.yaml # reglas de análisis y estilo ├── bin/ │ └── mi_app.dart # punto de entrada ejecutable ├── lib/ │ └── mi_app.dart # tu lógica reutilizable ├── pubspec.yaml # nombre, versión y dependencias └── test/ └── mi_app_test.dart # pruebas unitarias
  • bin/: lo que se ejecuta (dart run busca aquí).
  • lib/: el código importable de tu app o paquete.
  • test/: pruebas automatizadas (capítulo 36).
  • pubspec.yaml: metadatos y dependencias (capítulo 34).

3.3 · Tu primer cambio de verdad

Sustituye el contenido de bin/mi_app.dart:

void main() {
  String saludo = 'Dart';
  int anio = 2026;
  print('Aprendiendo $saludo en el año $anio');
}
dart run bin/mi_app.dart
Aprendiendo Dart en el año 2026

Fíjate en $saludo: así se insertan valores dentro de un texto (interpolación). Lo veremos en detalle en el capítulo 7.

3.4 · Herramientas que usarás todos los días

ComandoPara qué sirve
dart runEjecuta un archivo o el proyecto actual.
dart analyzeRevisa errores y avisos sin ejecutar nada.
dart format .Da formato automático al código (estilo uniforme).
dart fix --applyCorrige automáticamente problemas simples.
dart pub getDescarga las dependencias declaradas.
dart compile exeCrea un ejecutable nativo independiente.
Flujo recomendado: escribe código → guarda → dart run …. Si el editor marca algo en rojo antes de ejecutar, confía en él: casi siempre tiene razón.

Puntos clave

  • Toda ejecución comienza en main(); usa dart run archivo.dart.
  • dart create genera la estructura estándar: bin, lib, test y pubspec.
  • dart analyze/format/fix/pub son tus herramientas diarias.

4 · Sintaxis básica: reglas del juego

Básico ~12 min
  • Objetivos: entender cómo se escribe Dart — sentencias, bloques, identificadores, comentarios y la función principal.

Dart desciende de la familia C/Java/JavaScript, así que su sintaxis te resultará familiar si ya programas. Si este es tu primer lenguaje, estas son las cinco reglas que gobiernan todo el código:

ReglaEjemplo
Cada sentencia termina en ;print('hola');
Los bloques se delimitan con llaves { }void main() { ... }
Distingue mayúsculas y minúsculasedad, Edad y EDAD son distintos
Los identificadores empiezan por letra o _, nunca por número_privado, nombreUsuario2
Los espacios y saltos de línea no importan; el punto y coma sípuedes partir una línea larga en varias

4.1 · Comentarios

// Comentario de una línea.

/*
   Comentario de bloque:
   puede ocupar varias líneas.
*/

/// Documentación oficial: describe funciones y clases.
/// Los IDE muestran este texto en el autocompletado.
int duplicar(int n) => n * 2;

El comentario /// no es decorativo: genera documentación automática (dart doc) y aparece al pasar el ratón. Acostúmbrate a usarlo desde hoy.

4.2 · La función main() y sus variantes

// 1) La forma habitual
void main() {
  print('Sin argumentos');
}

// 2) Recibiendo argumentos de la línea de comandos: dart run app.dart Ana 30
void main(List<String> args) {
  print('Argumentos recibidos: $args');
}

// 3) Asíncrona (la verás mucho en Flutter y servidores)
Future<void> main() async {
  print('Puedo usar await aquí');
}

4.3 · print() con varios valores

void main() {
  print('Un solo valor');
  print('Varios:', ); // ERROR real: print acepta UN argumento
}

Para combinar valores usa interpolación (capítulo 7):

void main() {
  var producto = 'Café';
  var precio = 1.5;
  print('$producto cuesta $precio euros'); // Café cuesta 1.5 euros
}
No discutas con el formatter. Ejecuta dart format . y tu código tendrá el estilo oficial (indentación de 2 espacios, comillas consistentes…). En equipo, todos escriben igual y nadie pierde tiempo en debates de estilo.

Puntos clave

  • Sentencias terminadas en ; y bloques entre { }.
  • Tres tipos de comentario: //, /* */ y documental ///.
  • Todo arranca en main(); usa dart format siempre.

5 · Variables: var, dynamic y tipos

Básico ~15 min
  • Objetivos: declarar variables con tipo explícito o inferido; entender qué son var, dynamic y cuándo usar cada uno.

Una variable es un nombre asociado a un espacio de memoria con un tipo. En Dart el tipo se puede escribir explícitamente o dejar que el compilador lo infiera:

void main() {
  // Declaración explícita: dices tú el tipo
  String ciudad = 'Madrid';
  int habitantes = 3300000;
  double temperatura = 21.7;
  bool esCapital = true;

  // Inferencia: Dart deduce el tipo a partir del valor inicial
  var pais = 'España';        // se infiere String
  var codigos = [1, 2, 3];    // se infiere List<int>

  print('$ciudad tiene $habitantes habitantes. ¿Capital? $esCapital');
  print('$pais - $codigos - $temperatura');
}
Madrid tiene 3300000 habitantes. ¿Capital? true España - [1, 2, 3] - 21.7
Idea fundamental: el tipo se fija en la declaración y ya no cambia. Puedes cambiar el valor, nunca el tipo:
var edad = 34;
edad = 35;        // bien: sigue siendo int
// edad = 'treinta'; // ERROR: A value of type 'String' can't be assigned...

5.1 · Reglas para nombrar variables

  • lowerCamelCase: nombreUsuario, totalConImpuestos. Nunca guiones bajos ni mayúscula inicial (eso es para clases).
  • Puede empezar por letra o _; los números van después: jugador2.
  • No puede ser una palabra reservada (class, if, return…).
  • Nombres descriptivos: precioFinal antes que pf o x.

5.2 · dynamic: la válvula de escape

void main() {
  dynamic flexible = 'ahora soy texto';
  print(flexible.length);       // 16

  flexible = 42;
  print(flexible + 8);          // 50

  flexible = [1, 2];
  print(flexible.first);        // 1
}
16 50 1

Con dynamic el compilador deja de comprobar tipos: cualquier operación compila y los errores aparecen en ejecución. Es útil al interactuar con datos externos impredecibles (JSON dinámico), pero úsalo como último recurso: pierdes autocompletado y seguridad.

5.3 · ¿Qué pasa si no inicializo?

void main() {
  int x;
  print(x); // ERROR: The variable 'x' can't be used because it is potentially unassigned.
}

Gracias al análisis de flujo, Dart impide leer una variable local sin asignar. Este sistema se extiende y perfecciona con Null Safety (capítulo 17), donde también existe el modificador late.

5.4 · ¿Explicito o var?

Regla práctica usada en código profesional:

  • Usa var cuando el tipo sea obvio por el valor inicial.
  • Usa el tipo explícito cuando el valor venga de una llamada y quieras documentarlo: final List<String> nombres = obtenerNombres();
  • Nunca declares String nombre = 'Ana'; y tampoco var n = obtener(); ambiguo: busca equilibrio y legibilidad.

Puntos clave

  • El tipo se fija al declarar: cambia el valor, no el tipo.
  • var = inferencia del compilador; el resultado es igual de tipado.
  • dynamic desactiva la comprobación: úsalo con moderación.
  • Nombres en lowerCamelCase, claros y descriptivos.

6 · Números y booleanos con detalle

Básico ~15 min

Dart tiene dos tipos numéricos: int (enteros de 64 bits) y double (decimales IEEE 754). Ambos heredan de num, y todos son objetos con métodos.

void main() {
  int enteros = -42;
  int hex = 0xFF;            // hexadecimal: 255
  int grande = 1_000_000;    // separador visual (Dart 3.6+): un millón

  double precio = 19.99;
  double cientifico = 2.5e8; // 250000000.0

  num cualquiera = 10;       // num acepta int...
  cualquiera = 3.14;         // ...y también double

  print('$enteros $hex $grande');
  print('$precio $cientifico $cualquiera');
}
-42 255 1000000 19.99 250000000.0 3.14

6.1 · Operaciones básicas y la división trampa

void main() {
  var suma = 7 + 5;      // 12
  var resta = 7 - 5;     // 2
  var mult = 7 * 5;      // 35

  var division = 7 / 2;  // 3.5  -> SIEMPRE devuelve double
  var entera = 7 ~/ 2;   // 3    -> división entera (trunca)
  var modulo = 7 % 2;    // 1    -> resto

  print('division: $division | entera: $entera | resto: $modulo');

  // El operador ~ es útil para saber si un número es par:
  print(10 % 2 == 0);    // true -> es par
}
division: 3.5 | entera: 3 | resto: 1 true

6.2 · Métodos útiles de los números

void main() {
  int n = 7;
  print(n.isEven);          // false
  print(n.isOdd);           // true
  print(n.isNegative);      // false
  print((-n).abs());        // 7
  print(n.toString());      // '7'
  print(n.toRadixString(2)); // '111' en binario

  double d = 3.14159;
  print(d.toStringAsFixed(2)); // '3.14' (redondeado, como texto)
  print(d.round());         // 3   redondeo clásico
  print(d.ceil());          // 4   techo
  print(d.floor());         // 3   suelo
  print(d.truncate());      // 3   elimina decimales

  double infinito = 1 / 0;
  print(infinito.isInfinite); // true
}
false true false 7 7 111 3.14 3 4 3 3 true

6.3 · Conversiones entre tipos

void main() {
  // De texto a número
  int a = int.parse('123');
  double b = double.parse('3.14');
  int? c = int.tryParse('hola'); // null en vez de excepción
  print('$a $b $c');

  // De número a texto
  String t = 42.toString();
  String moneda = 9.5.toStringAsFixed(2); // '9.50'

  // Entre numéricos
  double d = 7.toDouble();     // 7.0
  int i = 7.9.toInt();         // 7 (trunca)

  print('"$t" "$moneda" $d $i');
}
123 3.14 null "42" "9.50" 7.0 7

6.4 · El tipo bool: sin medias tintas

Diferencia clave con JavaScript/Python: en Dart solo true y false son booleanos. No hay valores «truthy/falsy»: if (1) es un error de compilación.
void main() {
  bool activo = true;
  bool mayorEdad = 21 >= 18;

  // if (activo) ... correcto
  // if (1) ...       ERROR: conditions must have a static type of 'bool'

  print(activo && mayorEdad); // AND lógico: true
  print(!activo);             // NOT lógico: false
}

Puntos clave

  • int, double y su supertipo común num; todo son objetos.
  • / devuelve siempre double; usa ~/ para división entera.
  • tryParse convierte sin lanzar excepciones (devuelve null).
  • bool estricto: nada de truthy/falsy al estilo JS.

7 · Cadenas de texto (String) a fondo

Básico ~20 min

Las cadenas son inmutables: cada método «modificador» devuelve una cadena nueva. Dominarlas es esencial: en Flutter pintarás textos todo el día.

7.1 · Formas de escribir una cadena

void main() {
  var a = 'comillas simples';
  var b = "o dobles";              // equivalentes

  var escape = 'Ruta: C:\\temp\nSalto de línea\tTabulación';
  print(escape);

  var multilinea = '''
Querido diario:
  hoy aprendí Dart.
''';                              // respeta saltos y sangrías
  print(multilinea);

  var cruda = r'C:\nueva_carpeta\tabla'; // raw: \n NO es salto de línea
  print(cruda);

  var adyacente = 'texto en '
      'varias líneas '
      'sin símbolos raros';
  print(adyacente);
}
Ruta: C:\temp Salto de línea Tabulación Querido diario: hoy aprendí Dart. C:\nueva_carpeta\tabla texto en varias líneas sin símbolos raros

7.2 · Interpolación: el superpoder

void main() {
  var nombre = 'Ana';
  int edad = 34;

  print('Me llamo $nombre');                 // variable directa
  print('El próximo año tendré ${edad + 1}'); // expresión entre llaves
  print('Objeto: ${[1, 2, 3]}');             // cualquier objeto
  print(r'Sin interpolar: $nombre');         // cruda no interpola
}
Me llamo Ana El próximo año tendré 35 Objeto: [1, 2, 3] Sin interpolar: $nombre

7.3 · Métodos imprescindibles

void main() {
  var frase = '  Aprendiendo Dart paso a paso  ';

  print(frase.trim());            // quita espacios extremos
  print(frase.trim().length);     // 28
  print(frase.isEmpty);           // false
  print(''.isEmpty);              // true

  var f = frase.trim().toLowerCase();

  print(f.contains('dart'));      // true
  print(f.startsWith('aprend'));  // true
  print(f.endsWith('paso'));      // true
  print(f.indexOf('Dart'.toLowerCase())); // 12 (posición)

  print(f.substring(12, 16));     // 'dart'
  print(f.split(' '));            // ['aprendiendo', 'dart', 'paso', 'a', 'paso']
  print(f.replaceAll('paso', 'nivel'));
  print(f.replaceFirst('a', 'A')); // solo la primera aparición

  var codigo = '42';
  print(codigo.padLeft(5, '0'));  // '00042'
  print('*'.padRight(5, '-'));    // '*----'
}
Aprendiendo Dart paso a paso 28 false true true true true 12 dart [aprendiendo, dart, paso, a, paso] aprendiendo dart nivel a nivel AprenDiendo dart paso a paso 00042 *----

7.4 · Comparar cadenas

void main() {
  var a = 'hola';
  var b = 'ho' + 'la';

  print(a == b);           // true: compara CONTENIDO
  print(a.compareTo(b));   // 0: iguales; negativo si va antes, positivo si después

  var lista = ['pera', 'manzana', 'kiwi'];
  lista.sort();
  print(lista);            // orden lexicográfico
}
true 0 [kiwi, manzana, pera]
Unicode: un carácter acentuado puede ocupar más de una unidad interna (length cuenta unidades UTF-16, no letras). Para contar «letras visuales» reales usa el paquete characters. Lo verás en apps con emojis o alfabetos complejos.

Puntos clave

  • Los strings son inmutables: cada operación crea uno nuevo.
  • Interpola con $variable y ${expresión}.
  • r'...' evita escapes; '''...''' para bloques multilínea.
  • == compara contenido; trim/split/replaceAll/padLeft son tus cuchillos suizos.

8 · Constantes: final frente a const

Básico ~12 min

Dart ofrece dos formas de declarar valores que no cambian. Parecen gemelos, pero la diferencia es cuándo se calcula el valor:

finalconst
Cuándo se fija el valorEn tiempo de ejecución, una sola vezEn tiempo de compilación
Puede depender de datos externosSí (DateTime.now(), APIs…)No, solo literales y otras constantes
Inmutabilidad de coleccionesLa referencia es final; la lista puede cambiarProfunda: lista completamente inmutable
Uso típicoValores calculados una vezTablas, configuraciones, claves fijas
void main() {
  // FINAL: valor único, decidido al ejecutarse
  final ahora = DateTime.now();     // correcto: se evalúa en ejecución
  print(ahora.year);

  // CONST: debe poder calcularse al compilar
  const pi = 3.14159;
  const diasSemana = ['lunes', 'martes']; // colección inmutable profunda
  print('$pi - $diasSemana');
}

8.1 · La diferencia que sorprende: colecciones

void main() {
  final listaFinal = [1, 2, 3];
  listaFinal.add(4);        // PERFECTO: la referencia es final, no el contenido
  print(listaFinal);

  const listaConst = [1, 2, 3];
  // listaConst.add(4);     // ERROR en tiempo de ejecución:
  // Unsupported operation: Cannot add to an unmodifiable list
  print(listaConst);

  var normal = [1, 2];
  normal.add(3);            // var también es modificable
  print(normal);
}
[1, 2, 3, 4] [1, 2, 3] [1, 2, 3]

8.2 · const compuesto y canonicalización

const segundosPorDia = 60 * 60 * 24;      // el compilador lo calcula
const minutosPorAnio = segundosPorDia / 60; // puede basarse en otras const

void main() {
  print(segundosPorDia);   // 86400
  print(minutosPorAnio);   // 1440.0

  // Dos listas const idénticas SON EL MISMO objeto en memoria:
  const a = [1, 2];
  const b = [1, 2];
  print(identical(a, b));  // true (canonicalización)
}
Guía práctica: empieza siempre por final; sube a const cuando el valor sea literal o fijo. En Flutter verás const por todas partes porque permite a los widgets no reconstruirse: rendimiento gratis.

Puntos clave

  • final: una asignación, en ejecución. Ideal por defecto.
  • const: valor conocido al compilar e inmutabilidad profunda.
  • DateTime.now() o cualquier dato externo exige final.

9 · Operadores y expresiones

Básico ~20 min

9.1 · Aritméticos y de incremento

void main() {
  var x = 10;
  x += 5;   print(x);   // 15   (equivale a x = x + 5)
  x -= 3;   print(x);   // 12
  x *= 2;   print(x);   // 24
  x ~/= 5;  print(x);   // 4    (división entera con asignación)
  x %= 3;   print(x);   // 1

  var n = 5;
  print(n++);  // 5  -> devuelve ANTES de incrementar (post-incremento)
  print(n);    // 6
  print(++n);  // 7  -> incrementa ANTES de devolver (pre-incremento)
  print(-n);   // -7 (unario negativo)
}
15 12 24 4 1 5 6 7 -7

9.2 · Comparación y lógicos

void main() {
  var edad = 25;

  print(edad == 25);  // true   igualdad de VALOR
  print(edad != 30);  // true
  print(edad >= 18 && edad < 65); // AND lógico: true
  print(edad < 18 || edad > 64);  // OR lógico: false

  String? nombre;                 // puede ser null (capítulo 17)
  print(identical('a', 'a'));     // identidad de objetos (misma memoria)

  // == compara valor (para strings, números...); las clases pueden redefinirlo
  print([1, 2] == [1, 2]);        // false: dos listas distintas en memoria
}
true true true false true false

9.3 · Ternario: if en formato exprés

void main() {
  var nota = 8;
  var resultado = nota >= 5 ? 'aprobado' : 'suspenso';
  print(resultado);

  var estado = nota >= 9
      ? 'excelente'
      : nota >= 5
          ? 'aprobado'
          : 'suspenso';
  print(estado);
}
aprobado aprobado

9.4 · Comprobar tipos: is e is!

Object valor = 'tengo 42 años';

void main() {
  print(valor is String);       // true
  print(valor is! int);         // true (no es int)
  print(valor.length);          // 14

  if (valor is String) {
    // Dentro del if, 'valor' ya se sabe String: autocompletado completo
    print(valor.toUpperCase());
  }
}
true true 14 TENGO 42 AÑOS

9.5 · Operadores bit a bit (nivel bajo)

Poco frecuentes en apps, pero presentes en permisos, protocolos y entrevistas técnicas:

void main() {
  print(5 & 3);   // 1  AND bit a bit  (0101 & 0011)
  print(5 | 3);   // 7  OR bit a bit   (0101 | 0011)
  print(5 ^ 3);   // 6  XOR bit a bit
  print(~5);      // -6 complemento
  print(1 << 3);  // 8  desplazar bits a la izquierda
  print(16 >> 2); // 4  desplazar a la derecha
  print(8 >>> 2); // 2  desplazamiento sin signo (Dart 2.14+)
}
1 7 6 -6 8 4 2

9.6 · Precedencia (resumen práctico)

  1. Paréntesis ( ) — gana siempre; úsalos sin miedo.
  2. Postfijos/prefijos: ++ -- ! -
  3. Multiplicativos: * / ~/%
  4. Aditivos: + -
  5. Comparación relacional e igualdad: < > <= >= == !=
  6. Lógicos: && antes que ||
  7. Asignaciones y ternario, los últimos.

Puntos clave

  • ~/ divide enteros; % obtiene el resto.
  • n++ devuelve antes de incrementar; ++n después.
  • == compara contenido en tipos básicos, identidad en objetos por defecto.
  • is / is! comprueban tipos y activan promoción automática.
  • Ante la duda: paréntesis.

10 · Condicionales: if, else y switch

Básico ~18 min

10.1 · if / else if / else

void main() {
  int temperatura = 31;

  if (temperatura >= 30) {
    print('Hace calor');
  } else if (temperatura >= 15) {
    print('Templado');
  } else {
    print('Frío');
  }
}
Hace calor

Las condiciones exigen bool real (recuerda el capítulo 6). Los bloques de una sola línea pueden omitir llaves, pero en equipo se recomienda mantenerlas.

10.2 · switch tradicional

void main() {
  const dia = 'sabado';

  switch (dia) {
    case 'lunes':
    case 'martes':            // casos vacíos caen juntos: agrupar es gratis
      print('Inicio de semana');
      break;                  // obligatorio: evita la caída accidental entre casos
    case 'sabado':
    case 'domingo':
      print('Fin de semana');
      break;
    default:
      print('Entre semana');
  }
}
Fin de semana

En Dart los casos no atraviesan al siguiente si tienen código: cada rama termina implícitamente. El break es opcional pero recomendable; lo que sí puedes hacer es agrupar casos vacíos como arriba.

10.3 · switch expression: la joya de Dart 3

Desde Dart 3, switch también es una expresión que devuelve un valor. Sintaxis flecha, sin break, con exhaustividad comprobada por el compilador:

String saludo(String idioma) => switch (idioma) {
      'es' => '¡Hola!',
      'en' => 'Hello!',
      'fr' => 'Bonjour !',
      _    => 'Idioma no soportado', // _: comodín por defecto
    };

void main() {
  print(saludo('es'));
  print(saludo('en'));
  print(saludo('jp'));

  // También admite rangos combinando patrones (adelanto del capítulo 30):
  String nota(int puntos) => switch (puntos) {
        >= 90 => 'Sobresaliente',
        >= 60 => 'Aprobado',
        _     => 'Insuficiente',
      };
  print(nota(95));
}
¡Hola! Hello! Idioma no soportado Sobresaliente

10.4 · if-case: condición + patrón en una línea

void main() {
  final respuestas = [42];

  // Comprueba forma Y valor a la vez:
  if (respuestas case [int n] when n == 42) {
    print('La respuesta a todo: $n');
  }
}
La respuesta a todo: 42

Los patrones completos llegan en el capítulo 30; por ahora, quédate con que Dart moderno prefiere switch expressions frente a cascadas de if-else.

Puntos clave

  • if/else para decisiones binarias o encadenadas.
  • switch clásico agrupa casos vacíos y no atraviesa ramas.
  • Preferido hoy: switch expression, concisa, sin break y verificable.

11 · Bucles: repetir sin repetirse

Básico ~18 min

11.1 · for clásico con contador

void main() {
  for (var i = 1; i <= 3; i++) {
    print('Vuelta número $i');
  }

  // Tabla de multiplicar anidada:
  for (var fila = 1; fila <= 2; fila++) {
    var linea = '';
    for (var col = 1; col <= 3; col++) {
      linea += '${fila * col}\t';
    }
    print(linea);
  }
}
Vuelta número 1 Vuelta número 2 Vuelta número 3 1 2 3 2 4 6

11.2 · for-in: recorrer colecciones (el favorito)

void main() {
  var frutas = ['kiwi', 'pera', 'uva'];

  for (var fruta in frutas) {
    print(fruta);
  }

  // Con índice cuando lo necesitas:
  var letras = 'dart'.split('');
  for (var i = 0; i < letras.length; i++) {
    print('$i: ${letras[i]}');
  }
}
kiwi pera uva 0: d 1: a 2: r 3: t

11.3 · while y do-while

void main() {
  var saldo = 100;

  while (saldo >= 30) {       // comprueba ANTES de entrar
    saldo -= 30;
    print('Compra realizada. Saldo: $saldo');
  }
  print('Saldo insuficiente');

  var intentos = 0;
  do {                        // ejecuta AL MENOS una vez
    intentos++;
    print('Intento $intentos');
  } while (intentos < 3);
}
Compra realizada. Saldo: 70 Compra realizada. Saldo: 40 Compra realizada. Saldo: 10 Saldo insuficiente Intento 1 Intento 2 Intento 3

11.4 · break y continue

void main() {
  for (var i = 1; i <= 5; i++) {
    if (i == 4) break;         // abandona el bucle entero
    if (i % 2 == 0) continue;  // salta esta iteración
    print(i);
  }

  // Etiquetas para saltar en bucles anidados (uso puntual):
  exterior:
  for (var f = 1; f <= 3; f++) {
    for (var c = 1; c <= 3; c++) {
      if (f * c == 6) break exterior;
      print('$f-$c');
    }
  }
}
1 3 1-1 1-2 2-1 2-2

11.5 · ¿Qué bucle elijo?

  • Número conocido de vueltasfor clásico.
  • Recorrer una colecciónfor-in.
  • Repetir mientras algo sea ciertowhile.
  • Ejecutar mínimo una vezdo-while.
  • Transformar/filtrar datos → ni bucle: usa map/where (capítulo 15).

Puntos clave

  • for-in es el caballo de batalla diario sobre listas y mapas.
  • break corta el bucle; continue salta a la siguiente vuelta.
  • Cuida siempre la condición de salida: evita bucles infinitos.

12 · Funciones I: los fundamentos

Básico ~15 min

Una función empaqueta lógica reutilizable: recibe parámetros, hace algo y devuelve un valor. En Dart todo se tipa — también lo que devuelve:

// tipoRetorno nombre(parámetros) { cuerpo }
int sumar(int a, int b) {
  return a + b;
}

void main() {
  var total = sumar(2, 3);
  print('Total: $total');

  saludar('Ana');          // llamar sin usar su retorno es válido si devuelve algo útil
}

// void = no devuelve nada; return opcional o vacío
void saludar(String nombre) {
  print('Hola, $nombre');
  // return; // implícito
}
Total: 5 Hola, Ana

12.1 · Funciones flecha (=>)

Cuando el cuerpo entero es una sola expresión que se devuelve, usa la sintaxis flecha. Es la favorita del código Flutter:

int duplicar(int n) => n * 2;

bool esPar(int n) => n % 2 == 0;

String estado(int edad) => edad >= 18 ? 'adulto' : 'menor';

void main() {
  print(duplicar(21));   // 42
  print(esPar(21));      // false
  print(estado(21));     // adulto
}
Error clásico: la flecha devuelve una expresión, no ejecuta sentencias. void f() => print('a'); print('b'); solo encierra el primer print.

12.2 · Parámetros tipados siempre

// Sin tipos compila en modo dinámico antiguo, pero hoy el analizador avisa:
double area(double base, double altura) => base * altura / 2;

void main() {
  print(area(10, 4));   // 20.0
}

Tipar parámetros y retornos cuesta segundos y compra errores detectables al instante por el analizador. Hazlo sin excepción.

Puntos clave

  • Tipo nombre(params) { return valor; } — la estructura universal.
  • => azúcar para «return de una expresión».
  • void significa «no devuelvo nada».

13 · Funciones II: parámetros avanzados

Básico ~20 min

Dart tiene tres tipos de parámetros y saber combinarlos distingue al código profesional:

TipoSintaxisLlamada
Posicional requerido(int a, int b)Por orden: f(1, 2)
Opcional posicional(int a, [int? b, int c = 0])Puede omitirse: f(1)
Nombrado({required int a, int b = 5})Por nombre: f(a: 1, b: 2)

13.1 · Opcionales posicionales [ ]

String ficha(String nombre, [int? edad, String ciudad = 'Desconocida']) {
  var e = edad == null ? '?' : '$edad';
  return '$nombre ($e años) vive en $ciudad';
}

void main() {
  print(ficha('Ana'));
  print(ficha('Ana', 34));
  print(ficha('Ana', 34, 'Madrid'));
}
Ana (? años) vive en Desconocida Ana (34 años) vive en Desconocida Ana (34 años) vive en Madrid

Los opcionales van después de los obligatorios; los que siguen a uno con valor por defecto necesitan también uno propio para ser alcanzables.

13.2 · Nombrados { }: claridad ante todo

Usuario crearUsuario({
  required String nombre,       // required = obligatorio
  int edad = 18,                // con valor por defecto
  bool activo = true,
}) => Usuario(nombre, edad, activo);

class Usuario {
  final String nombre;
  final int edad;
  final bool activo;
  Usuario(this.nombre, this.edad, this.activo);

  @override
  String toString() => 'Usuario($nombre, $edad, activo: $activo)';
}

void main() {
  print(crearUsuario(nombre: 'Luis'));
  print(crearUsuario(nombre: 'Marta', edad: 41, activo: false));

  // El orden ya no importa: se llama por nombre
  print(crearUsuario(activo: false, nombre: 'Beto'));
}
Usuario(Luis, 18, activo: true) Usuario(Marta, 41, activo: false) Usuario(Beto, 18, activo: false)

Este estilo domina las APIs modernas de Dart y Flutter: Text('Hola'), Container(width: 100, height: 50)

13.3 · Mezclar los tres (orden fijo)

void reporte(String titulo, {int nivel = 1, List<String>? notas}) {
  print('$titulo (nivel $nivel)');
  if (notas != null) {
    for (var nota in notas) {
      print(' - $nota');
    }
  }
}

void main() {
  reporte('Ventas');
  reporte('Incidencias', nivel: 3);
  reporte('Auditoría', nivel: 2, notas: ['Sin hallazgos', 'OK']);
}
Ventas (nivel 1) Incidencias (nivel 3) Auditoría (nivel 2) - Sin hallazgos - OK

Regla inquebrantable: primero posicionales, luego [opcionales], luego {nombrados}.

Puntos clave

  • [opcionales] mantienen el orden; {nombrados} dan legibilidad.
  • required marca nombrados obligatorios; el resto necesita valor por defecto o ser anulable.
  • Orden fijo: posicionales → opcionales → nombrados.

14 · Funciones III: closures y funciones como valores

Intermedio ~25 min

En Dart las funciones son objetos de primera clase: se guardan en variables, se pasan como argumentos y se devuelven desde otras funciones. Este capítulo abre la puerta a la programación funcional que usarás constantemente en Flutter.

14.1 · Funciones anónimas

void main() {
  // Sin nombre, asignada a una variable:
  var sumar = (int a, int b) => a + b;
  print(sumar(2, 5)); // 7

  // Cuerpo largo con llaves y return:
  var clasificar = (int edad) {
    if (edad < 13) return 'niño';
    if (edad < 18) return 'adolescente';
    return 'adulto';
  };
  print(clasificar(15)); // adolescente

  // Uso típico: callback dentro de un método de colección
  ['a', 'b', 'c'].forEach((letra) {
    print('Letra: $letra');
  });
}
7 adolescente Letra: a Letra: b Letra: c

14.2 · Pasar funciones como argumentos

void ejecutarSi(bool condicion, void Function() accion) {
  if (condicion) {
    accion();
  }
}

int aplicarOperacion(int x, int Function(int) operacion) {
  return operacion(x);
}

void main() {
  ejecutarSi(10 > 5, () => print('La condición era verdadera'));

  print(aplicarOperacion(6, (n) => n * n));       // 36
  print(aplicarOperacion(6, (n) => n + 100));     // 106
}
La condición era verdadera 36 106

14.3 · Closures: funciones que recuerdan

Una función anónima captura las variables del ámbito donde nació, incluso cuando ese ámbito ya terminó:

// Devuelve una función nueva que multiplica por 'factor'
Function crearMultiplicador(int factor) {
  return (int n) => n * factor;   // recuerda 'factor' para siempre
}

void main() {
  final duplicar = crearMultiplicador(2);
  final triplicar = crearMultiplicador(3);

  print(duplicar(10));   // 20  -> recuerda factor=2
  print(triplicar(10));  // 30  -> recuerda factor=3

  // Contador con estado privado gracias al closure:
  var contador = crearContador();
  print(contador()); // 1
  print(contador()); // 2
  print(contador()); // 3
}

Function crearContador() {
  int cuenta = 0;                 // vive mientras exista la función devuelta
  return () => ++cuenta;
}
20 30 1 2 3

14.4 · typedef: nombres para tipos de función

// Un alias reutilizable y autodocumentado:
typedef Comparador<T> = int Function(T a, T b);
typedef Procesador = String Function(String entrada);

String normalizar(String s) => s.trim().toLowerCase();

void main() {
  final Procesador p = normalizar;
  print(p('   HOLA MUNDO   '));

  List<int> edades = [40, 12, 25];
  edades.sort((a, b) => a.compareTo(b)); // Comparador<int> en línea
  print(edades);
}
hola mundo [12, 25, 40]
Por qué importa en Flutter: los botones reciben callbacks (onPressed: () { … }), los constructores capturan estado… Los closures son el pegamento de toda app Flutter.

Puntos clave

  • Las funciones son objetos: variables, argumentos y retornos.
  • Un closure captura variables del entorno donde se creó.
  • typedef documenta tipos de función complejos.

15 · Colecciones I: List e Iterable

Intermedio ~30 min

La lista es la estructura reina. En Flutter construirás interfaces recorriendo listas de datos una y otra vez.

15.1 · Crear y manipular

void main() {
  var tareas = ['comprar', 'estudiar'];   // List<String> por inferencia
  List<int> primos = [2, 3, 5, 7];

  tareas.add('descansar');       // añade al final
  tareas.insert(0, 'despertar'); // inserta en posición
  print(tareas);

  print(tareas.length);          // 4
  print(tareas[0]);              // despertar (acceso por índice)
  tareas[0] = 'levantarse';      // reemplazar

  print(tareas.contains('estudiar')); // true
  print(tareas.indexOf('descansar')); // 3

  tareas.removeAt(0);            // elimina por índice y lo devuelve
  tareas.remove('estudiar');     // elimina el primer igual
  print(tareas);

  print(primos.reversed);        // (7, 5, 3, 2) vista invertida
  print(primos.sublist(1, 3));   // [3, 5]
  print(primos.take(2));         // (2, 3) primeros dos
  print(primos.skip(1).take(2)); // (3, 5) salta uno y toma dos

  var vacia = <String>[];        // lista vacía tipada explícita
  print(vacia.isEmpty);
}
[despertar, comprar, estudiar, descansar] 4 despertar true 3 [comprar, descansar] (7, 5, 3, 2) [3, 5] (2, 3) (3, 5) true

15.2 · Ordenar

void main() {
  var numeros = [40, 12, 25, 8];
  numeros.sort();                       // ascendente in situ
  print(numeros);

  numeros.sort((a, b) => b.compareTo(a)); // descendente con comparador
  print(numeros);

  var nombres = ['carla', 'Ana', 'bruno'];
  nombres.sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
  print(nombres);                       // orden alfabético insensible a mayúsculas
}
[8, 12, 25, 40] [40, 25, 12, 8] [Ana, bruno, carla]

15.3 · Programación funcional: where, map y amigos

void main() {
  var edades = [12, 17, 34, 41, 9];

  var adultos = edades.where((e) => e >= 18);     // filtra
  print(adultos.toList());

  var enAnios = edades.map((e) => '$e años');     // transforma cada elemento
  print(enAnios.join(', '));

  print(edades.any((e) => e > 40));   // true  ¿alguno cumple?
  print(edades.every((e) => e > 5));  // true  ¿cumplen todos?
  print(edades.firstWhere((e) => e >= 18, orElse: () => -1)); // primer adulto

  var suma = edades.reduce((a, b) => a + b);      // acumula sin valor inicial
  var total = edades.fold<int>(0, (a, b) => a + b); // con valor inicial y tipo
  print('$suma $total');

  var pares = edades.expand((e) => [e, e * 10]);  // aplana resultados
  print(pares);
}
[34, 41] 12 años, 17 años, 34 años, 41 años, 9 años true true 34 113 113 [12, 120, 17, 170, 34, 340, 41, 410, 9, 90]
Pereza inteligente: where/map devuelven un Iterable perezoso: no calculan nada hasta que lo recorres. Llama a .toList() cuando necesites materializarlo o guardarlo.

15.4 · Spread y collection-if / collection-for

void main() {
  var base = [1, 2];
  var completa = [...base, 3, 4];         // ... expande otra colección
  print(completa);

  List<int>? posibleNull;
  var segura = [...?posibleNull, 99];     // ...? ignora si es null
  print(segura);

  bool modoAdmin = true;
  final menu = [
    'Inicio',
    if (modoAdmin) 'Panel de administración',   // elemento condicional
    for (var i = 1; i <= 3; i++) 'Sección $i',  // elementos generados
  ];
  print(menu);
}
[1, 2, 3, 4] [99] [Inicio, Panel de administración, Sección 1, Sección 2, Sección 3]

Esta sintaxis dentro de literales es idiomática de Dart moderno: verás listas construidas así en cada pantalla de Flutter.

Puntos clave

  • add/insert/remove/removeAt/length/[] cubren el día a día.
  • where/map/reduce/fold/any/every = lógica sin bucles manuales.
  • ...spread, if y for dentro de literales son oro puro.
  • Los iterables son perezosos: usa toList() para materializar.

16 · Colecciones II: Set y Map

Intermedio ~25 min

16.1 · Set: valores únicos

void main() {
  var visitados = <String>{'Madrid', 'Lima'};
  visitados.add('Bogotá');
  visitados.add('Madrid');        // duplicado: ignorado silenciosamente
  print(visitados);
  print(visitados.length);        // 3

  print(visitados.contains('Lima')); // búsqueda O(1), muy rápida

  var europeas = {'Madrid', 'París'};
  print(visitados.union(europeas));
  print(visitados.intersection(europeas));
  print(visitados.difference(europeas));

  // Truco clásico: eliminar duplicados de una lista
  var conRepetidos = ['a', 'b', 'a', 'c', 'b'];
  var unicos = conRepetidos.toSet().toList();
  print(unicos);
}
{Madrid, Lima, Bogotá} 3 true {Madrid, París, Lima, Bogotá} {Madrid} {Lima, Bogotá} [a, b, c]

16.2 · Map: pares clave → valor

void main() {
  var capitales = {
    'España': 'Madrid',
    'Perú': 'Lima',
  };
  capitales['Colombia'] = 'Bogotá';   // añadir o actualizar

  print(capitales['Perú']);           // Lima
  print(capitales['Francia']);        // null (clave inexistente)
  print(capitales['Francia'] ?? 'Sin dato');

  print(capitales.containsKey('España'));
  print(capitales.containsValue('Lima'));
  print(capitales.length);

  capitales.putIfAbsent('Chile', () => 'Santiago');
  capitales.update('España', (capital) => capital.toUpperCase());
  print(capitales);

  capitales.remove('Chile');
  print(capitales.keys);
  print(capitales.values);
}
Lima null Sin dato true true 3 {España: MADRID, Perú: Lima, Colombia: Bogotá, Chile: Santiago} (España, Perú, Colombia) (MADRID, Lima, Bogotá)

16.3 · Recorrer un Map

void main() {
  var inventario = {'manzanas': 12, 'peras': 5};

  inventario.forEach((producto, cantidad) {
    print('$producto: $cantidad uds.');
  });

  for (final entrada in inventario.entries) {
    print('${entrada.key} => ${entrada.value}');
  }

  // Transformar un mapa en otro:
  final agotados = inventario.map((k, v) => MapEntry(k, v < 6 ? 'agotándose' : 'ok'));
  print(agotados);

  // Mapas con claves dinámicas (JSON crudo):
  dynamic respuesta = {'id': 7, 'nombre': 'Ada'};
  print(respuesta['nombre']);
}
manzanas: 12 uds. peras: 5 uds. manzanas => 12 peras => 5 {manzanas: ok, peras: agotándose} Ada

16.4 · Spread también funciona aquí

void main() {
  const baseConfig = {'tema': 'claro', 'idioma': 'es'};
  final configUsuario = {
    ...baseConfig,
    'notificaciones': true,
    if (!baseConfig.containsKey('fuente')) 'fuente': 'Roboto',
  };
  print(configUsuario);
}
{tema: claro, idioma: es, notificaciones: true, fuente: Roboto}
¿Cuál uso? Lista: orden importa y hay duplicados. Set: unicidad y búsquedas rápidas. Map: acceso por clave (índice de datos). El capítulo 35 muestra cómo convertir JSON real en estas estructuras.

Puntos clave

  • Set garantiza unicidad y busca en O(1).
  • Map accede por clave; las claves ausentes devuelven null.
  • entries, forEach((k,v)) y spreads completan el kit.

17 · Null Safety: el sistema anti-nulos

Intermedio ~30 min

El error «null pointer» ha hundido más programas que cualquier otro. Desde Dart 2.12, el compilador distingue entre tipos que pueden contener null y tipos que nunca lo harán. El resultado: esa familia de bugs desaparece en tiempo de compilación.

17.1 · Tipos anulables: la interrogación

void main() {
  String nombre = 'Ada';    // NO puede ser null jamás
  // nombre = null;         // ERROR de compilación

  String? apodo;            // String? SÍ puede ser null
  print(apodo);             // null
  print(apodo?.length);     // null: llamada segura

  apodo = 'la primera';
  print(apodo.length);      // ahora sin ? : el compilador sabe que no es null aquí
}
null null 10

17.2 · Los operadores null-aware

String? obtenerApodo(String usuario) =>
    usuario == 'ada' ? 'la primera' : null;

void main() {
  var apodo = obtenerApodo('ada');

  print(apodo ?? 'sin apodo');     // si es null, usa el valor de la derecha
  print(obtenerApodo('bob') ?? 'sin apodo');

  int visitas = 0;
  visitas ??= 1;                   // asigna SOLO si era null
  print(visitas);                  // 0 (no era null)

  int? otra;
  otra ??= 7;
  print(otra);                     // 7

  // Encadenado seguro: corta toda la cadena al primer null
  Map<String, dynamic>? respuesta;
  print(respuesta?['usuario']?['email']);
}
la primera sin apodo 0 7 null

17.3 · El operador ! : «confía en mí»

void main() {
  String? texto = '42';

  // Convierte un String? en String asumiendo que no es null:
  int numero = int.parse(texto!);
  print(numero + 8);   // 50

  // ¡CUIDADO! Si fuera null lanza excepción en tiempo de ejecución:
  String? vacio;
  // print(vacio!.length); // Null check operator used on a null value
}
Regla: usa ! solo cuando tengas certeza absoluta y preferiblemente con un comentario que la justifique. Casi siempre existe una alternativa con ?? o comprobación previa.

17.4 · Análisis de flujo: promoción automática

void main() {
  String? nombre;

  if (nombre != null) {
    // Dentro del if, nombre se PROMOCIONA a String:
    print(nombre.toUpperCase());
  }

  nombre = 'Ana';
  // Tras la asignación, también está promovido:
  print(nombre.length);
}

17.5 · late: prometo inicializarlo

class Configuracion {
  // Se declara sin valor pero se garantiza su asignación antes de usarse:
  late final String apiKey;

  void cargar() {
    apiKey = 'sk-1234';   // única asignación permitida por final
  }

  void usar() {
    cargar();
    print(apiKey);        // seguro: ya fue cargada
  }
}

// late también permite inicialización perezosa a nivel local:
late String costoso = calcularCostoso();
String calcularCostoso() {
  print('Calculando…');       // solo se ejecuta en el PRIMER acceso
  return 'resultado';
}

void main() {
  final c = Configuracion()..usar();  // cascada: capítulo 20
  print(c.runtimeType);
  print(costoso);
}
sk-1234 Configuracion Calculando… resultado

17.6 · Nulabilidad anidada: ¿qué significa cada firma?

TipoLectura correcta
List<int>Lista obligatoria de enteros obligatorios.
List<int>?Puede no haber lista; si hay, todos sus elementos son enteros.
List<int?>Siempre hay lista, pero puede contener nulos entre los enteros.
Map<String, int?>Claves siempre presentes; valores opcionales.

Puntos clave

  • Por defecto ningún tipo acepta null; añade ? solo cuando tenga sentido.
  • Caja de herramientas: ?? ??= . ! + promoción por análisis de flujo.
  • late aplaza la inicialización manteniendo la seguridad.
  • ! es una apuesta: si fallas, excepción en runtime.

18 · Clases y objetos: primeros pasos

Intermedio ~25 min

Una clase es el plano; un objeto es cada casa construida con él. La clase agrupa datos (campos) y comportamiento (métodos):

class Coche {
  // Campos (estado)
  String marca;
  String modelo;
  int anio;

  // Constructor: inicializa el estado
  Coche(this.marca, this.modelo, this.anio);

  // Método (comportamiento)
  String descripcion() => '$marca $modelo ($anio)';

  int edad() => DateTime.now().year - anio;
}

void main() {
  var miCoche = Coche('Toyota', 'Corolla', 2021);  // instancia 1
  var otroCoche = Coche('Seat', 'Ibiza', 2024);    // instancia 2 independiente

  print(miCoche.descripcion());
  print('Edad del Toyota: ${miCoche.edad()} años');
  print(otroCoche.descripcion());

  // Los campos son accesibles y modificables:
  miCoche.anio = 2020;
  print(miCoche.descripcion());
}
Toyota Corolla (2021) Edad del Toyota: 5 años Seat Ibiza (2024) Toyota Corolla (2020)

18.1 · La abreviatura this.x

class Punto {
  double x;
  double y;

  // Forma larga:
  // Punto(double x, double y) { this.x = x; this.y = y; }

  // Forma idiomática (initializing formal): this.x asigna directo
  Punto(this.x, this.y);

  double distanciaAlOrigen() {
    var dx = -x;   // dentro de la clase no hace falta this.
    var dy = -y;
    return sqrt(dx * dx + dy * dy);
  }
}

// Necesitamos dart:math para sqrt — así se importa una librería:
import 'dart:math';

void main() {
  final p = Punto(3, 4);
  print(p.distanciaAlOrigen()); // 5.0 (¡el famoso triángulo!)
}
5.0

Regla de estilo: usa this. solo cuando haya ambigüedad entre campo y parámetro.

18.2 · Representar objetos como texto: toString

class Libro {
  String titulo;
  String autor;

  Libro(this.titulo, this.autor);

  @override                       // indica que reemplazamos un método heredado
  String toString() => '"$titulo" de $autor';
}

void main() {
  var l = Libro('1984', 'Orwell');
  print(l);            // print llama a toString automáticamente
  print('${l}');       // igual en interpolación
}
"1984" de Orwell "1984" de Orwell
Sobrescribe siempre toString() en tus modelos de datos: mejora logs, depuración y tests desde el primer día.

Puntos clave

  • new no existe: MiClase(...) ya crea instancias.
  • this.campo en el constructor asigna sin boilerplate.
  • Cada instancia tiene su propio estado independiente.

19 · Constructores a fondo

Intermedio ~30 min

Dart permite varios constructores por clase, cada uno con un propósito distinto. Este es uno de los capítulos más rentables para Flutter.

19.1 · Constructores con parámetros normales y nombrados

class Tarea {
  final String titulo;
  final String prioridad;
  final bool completada;

  Tarea(
    this.titulo, {
    this.prioridad = 'media',     // nombrados con valor por defecto
    this.completada = false,
  });

  @override
  String toString() => '[$prioridad] $titulo ${completada ? "OK" : ""}';
}

void main() {
  print(Tarea('Pagar alquiler'));
  print(Tarea('Estudiar Dart', prioridad: 'alta'));
  print(Tarea('Comprar pan', prioridad: 'baja', completada: true));
}
[media] Pagar alquiler [alta] Estudiar Dart [baja] Comprar pan OK

19.2 · Lista de inicializadores (: )

class Rectangulo {
  final double ancho;
  final double alto;
  late final double area;   // calculada una sola vez

  Rectangulo(this.ancho, this.alto)
      : area = ancho * alto {         // ANTES del cuerpo: campos derivados/finales
    print('Rectángulo creado');
  }
}

void main() => print(Rectangulo(3, 4).area);
Rectángulo creado 12.0

19.3 · Constructores con nombre

Dart no admite sobrecarga de constructores; en su lugar, nombres descriptivos:

import 'dart:convert';

class Punto {
  final double x, y;
  Punto(this.x, this.y);

  // Constructor alternativo: origen fijo
  Punto.origen()
      : x = 0,
        y = 0;

  // Desde JSON:
  Punto.desdeJson(String json)
      : this._desdeMapa(jsonDecode(json));

  Punto.polar(double radio, double angulo)
      : x = radio * cos(angulo),
        y = radio * sin(angulo);

  Punto._desdeMapa(Map<String, dynamic> m)
      : x = m['x'],
        y = m['y'];

  @override
  String toString() => '($x, $y)';
}

void main() {
  print(Punto(1, 2));
  print(Punto.origen());
  print(Punto.polar(2, 0));                 // (2.0, ~0.0)
  print(Punto.desdeJson('{"x": 5, "y": 7}'));
}
(1.0, 2.0) (0.0, 0.0) (2.0, 1.2246467991473532e-16) (5.0, 7.0)

19.4 · Redirección : this.otro(...)

class Pizza {
  String tamano;
  Pizza(this.tamano);
  Pizza.grande() : this('grande');       // delega en el principal
  Pizza.familiar() : this('familiar + borde');
}

void main() {
  print(Pizza.familiar().tamano);
}

19.5 · factory: construcción con lógica

Un constructor factory puede devolver instancias ya creadas, subtipos o decidir cómo construir según la entrada:

class Logger {
  static final Map<String, Logger> _cache = {};

  final String nombre;
  Logger._interno(this.nombre);

  factory Logger(String nombre) {
    return _cache.putIfAbsent(nombre, () => Logger._interno(nombre));
  }
}

void main() {
  var a = Logger('api');
  var b = Logger('api');
  print(identical(a, b));   // true: mismo objeto devuelto (patrón Singleton)
}
true

19.6 · Constructores const

class Etiqueta {
  final String texto;
  const Etiqueta(this.texto);
}

void main() {
  const e1 = Etiqueta('FRAGIL');
  const e2 = Etiqueta('FRAGIL');
  var e3 = Etiqueta('FRAGIL');

  print(identical(e1, e2));  // true: compiladas como un único objeto
  print(identical(e1, e3));  // false: e3 no fue declarada const
}

Requisitos: todos los campos final y sin cuerpos que muten estado. En Flutter esto habilita const Text('...'), que evita reconstrucciones costosas.

Puntos clave

  • Nombrados Clase.nombre(...) sustituyen a la sobrecarga.
  • La lista tras : inicializa finales/derivados antes del cuerpo.
  • factory decide qué devolver (caches, singletons, subtipos).
  • const en constructores = objetos canónicos y rendimiento.

20 · Miembros especiales: get/set, static y operadores

Intermedio ~30 min

20.1 · Getters y setters: propiedades calculadas

class CuentaBancaria {
  double _saldo = 0;          // _privado a nivel de biblioteca

  // Getter: se lee como propiedad, no como método
  double get saldo => _saldo;

  // Setter con validación
  set saldo(double valor) {
    if (valor < 0) {
      throw ArgumentError('El saldo no puede ser negativo');
    }
    _saldo = valor;
  }

  void depositar(double monto) => _saldo += monto;
}

void main() {
  final c = CuentaBancaria();
  c.depositar(100);
  print(c.saldo);      // sin paréntesis: parece un campo…
  c.saldo = 150;       // …pero pasa por el setter validado
  print(c.saldo);
}
100.0 150.0

20.2 · static: de la clase, no del objeto

class Matematica {
  static const double pi = 3.14159;

  static int cuadrado(int n) => n * n;

  static int _instancias = 0;
  static int get instancias => _instancias;
}

class Producto {
  static int totalCreados = 0;   // contador compartido por TODAS las instancias
  Producto() {
    totalCreados++;
  }
}

void main() {
  print(Matematica.pi);          // sin crear objetos
  print(Matematica.cuadrado(9));

  Producto(); Producto(); Producto();
  print(Producto.totalCreados);
}
3.14159 81 3

20.3 · Sobrecarga de operadores

class Dinero {
  final double monto;
  const Dinero(this.monto);

  // Permite escribir dineroA + dineroB
  Dinero operator +(Dinero otro) => Dinero(monto + otro.monto);

  @override
  bool operator ==(Object other) =>
      other is Dinero && other.monto == monto;

  @override
  int get hashCode => monto.hashCode;

  @override
  String toString() => '${monto.toStringAsFixed(2)} €';
}

void main() {
  const cafe = Dinero(1.5);
  const croissant = Dinero(2.25);
  print(cafe + croissant);

  const a = Dinero(10), b = Dinero(10);
  print(a == b);          // true: definimos igualdad por valor
  print(a.hashCode == b.hashCode); // coherente con ==
}
3.75 € true true

Regla de oro: si redefines ==, redefine también hashCode o los Sets/Maps dejarán de comportarse.

20.4 · Clases invocables: call()

class Corrector {
  final double tolerancia;
  Corrector(this.tolerancia);

  // El objeto ahora «se llama» como una función:
  bool call(double esperado, double obtenido) =>
      (esperado - obtenido).abs() <= tolerancia;
}

void main() {
  final corrige = Corrector(0.01);
  print(corrige(10, 9.995));   // true — ¡un objeto usado como función!
  print(corrige(10, 9.8));     // false
}

Puntos clave

  • get/set exponen lógica con apariencia de atributo.
  • static = miembros de la clase: constantes, utilidades, contadores.
  • operator == exige acompañarse de hashCode.
  • call() convierte instancias en invocables.

21 · Herencia: extends y super

Intermedio ~30 min

La herencia modela relaciones «es un». Una clase hija hereda campos y métodos de su padre y puede añadir o redefinir comportamiento.

class Animal {
  String nombre;
  Animal(this.nombre);

  void hacerSonido() => print('$nombre hace un sonido genérico');
  void dormir() => print('$nombre duerme… zzz');
}

class Perro extends Animal {
  Perro(String nombre) : super(nombre);   // llama al constructor del padre

  @override
  void hacerSonido() => print('$nombre dice ¡guau!');

  void traerPalo() => print('$nombre trae el palo');
}

void main() {
  var animal = Animal('Criatura');
  var perro = Perro('Rex');

  animal.hacerSonido();
  perro.hacerSonido();   // versión redefinida (polimorfismo)
  perro.dormir();        // heredado intacto
  perro.traerPalo();     // propio de Perro
}
Criatura hace un sonido genérico Rex dice ¡guau! Rex duerme… zzz Rex trae el palo

21.1 · Polimorfismo: una variable, muchas formas

class Gato extends Animal {
  Gato(String nombre) : super(nombre);
  @override
  void hacerSonido() => print('$nombre dice miau');
}

void main() {
  List<Animal> granja = [Perro('Rex'), Gato('Michi'), Animal('Oveja')];

  for (final a in granja) {
    a.hacerSonido();       // cada uno responde a SU manera
  }
}
Rex dice ¡guau! Michi dice miau Oveja hace un sonido genérico

21.2 · Reutilizar el padre dentro del override

class Empleado {
  String nombre;
  double salarioBase;
  Empleado(this.nombre, this.salarioBase);

  double salarioTotal() => salarioBase;
  String ficha() => 'Empleado: $nombre';
}

class Gerente extends Empleado {
  double bono;
  Gerente(String nombre, double base, this.bono) : super(nombre, base);

  @override
  double salarioTotal() => super.salarioTotal() + bono;  // extiende, no sustituye

  @override
  String ficha() => '${super.ficha()} (Gerente)';
}

void main() {
  var g = Gerente('Ana', 2000, 500);
  print(g.salarioTotal());
  print(g.ficha());
}
2500.0 Empleado: Ana (Gerente)

21.3 · Constructores en la cadena de herencia

class Vehiculo {
  final String ruedas;
  Vehiculo(this.ruedas) { print('Vehículo($ruedas)'); }
}

class Moto extends Vehiculo {
  Moto() : super('2 ruedas') { print('Moto lista'); }
}

void main() => Moto();
// El constructor del PADRE siempre se ejecuta primero.
Vehículo(2 ruedas) Moto lista
Cuando NO heredar: si solo quieres reutilizar código sin relación «es-un», usa composición (objetos dentro de objetos) o mixins (capítulo 23). La herencia profunda acopla clases y dificulta el mantenimiento.

Puntos clave

  • extends hereda; super(...) inicializa al padre.
  • @override + redefinición = polimorfismo.
  • super.metodo() permite extender en vez de reemplazar.

22 · Clases abstractas e interfaces

Intermedio ~25 min

Una clase abstracta define un contrato con implementación parcial: no se puede instanciar y obliga a sus hijas a completar los métodos pendientes.

abstract class Figura {
  String nombre;

  Figura(this.nombre);

  double area();              // método abstracto: SIN cuerpo

  void describir() {          // implementación compartida
    print('$nombre tiene un área de ${area().toStringAsFixed(2)}');
  }
}

class Circulo extends Figura {
  double radio;
  Circulo(this.radio) : super('Círculo');

  @override
  double area() => 3.14159 * radio * radio;
}

class Cuadrado extends Figura {
  double lado;
  Cuadrado(this.lado) : super('Cuadrado');

  @override
  double area() => lado * lado;
}

void main() {
  // var f = Figura('x');  // ERROR: las clases abstractas no se instancian
  List<Figura> figuras = [Circulo(1), Cuadrado(3)];
  for (final f in figuras) {
    f.describir();
  }
}
Círculo tiene un área de 3.14 Cuadrado tiene un área de 9.00

22.1 · Toda clase es una interfaz implícita

En Dart no existe la palabra interface: cada clase define automáticamente una interfaz. Con implements heredas el contrato, pero debes escribir todos los métodos desde cero:

class Repositorio {
  String nombre = 'base';
  void guardar(String dato) => print('[$nombre] guardando $dato');
}

class RepositorioMemoria implements Repositorio {
  @override
  String nombre = 'memoria';

  final _datos = <String>[];

  @override
  void guardar(String dato) => _datos.add(dato);   // reimplementado al 100%

  List<String> get datos => List.unmodifiable(_datos);
}

void main() {
  var repo = RepositorioMemoria();
  repo.guardar('tarea 1');
  print(repo.datos);
}
[tarea 1]

22.2 · extends frente a implements

extendsimplements
HerenciaUna sola superclaseVarias interfaces a la vez
Código heredadoSí, reutiliza implementaciónNo: solo firmas; todo se reescribe
Uso idealJerarquías «es-un»Contratos puros, desacoplamiento
// Contrato clásico estilo interfaz:
abstract class Notificable {
  void notificar(String mensaje);
}

class EmailService implements Notificable {
  @override
  void notificar(String mensaje) => print('Email: $mensaje');
}

class SmsService implements Notificable {
  @override
  void notificar(String mensaje) => print('SMS: $mensaje');
}

// Programamos contra el CONTRATO, no contra clases concretas:
void avisar(Notificable canal, String msg) => canal.notificar(msg);

void main() {
  avisar(EmailService(), 'Tu pedido salió');
  avisar(SmsService(), 'Código: 123456');
}
Email: Tu pedido salió SMS: Código: 123456

Puntos clave

  • abstract = contrato + implementación parcial; no instanciable.
  • Toda clase es interfaz: implements obliga a reescribir todo.
  • Programa contra contratos (Notificable) para desacoplar tu código.

23 · Mixins: composición flexible

Avanzado ~30 min

Un mixin es un paquete reutilizable de métodos y campos que se «mezcla» dentro de una clase. Resuelve el problema de querer capacidades múltiples sin crear torres de herencia.

mixin Volador {
  void volar() => print('Volando…');
}

mixin Nadador {
  void nadar() => print('Nadando…');
}

class Pato extends Animal with Volador, Nadador {
  Pato(String nombre) : super(nombre);
}

class Pez extends Animal with Nadador {
  Pez(String nombre) : super(nombre);
}

class Animal {
  String nombre;
  Animal(this.nombre);
  void hacerSonido() => print('$nombre hace sonido');
}

void main() {
  var pato = Pato('Donald');
  pato.hacerSonido();   // del padre
  pato.volar();         // del mixin Volador
  pato.nadar();         // del mixin Nadador

  Pez('Nemo').nadar();
  // Nemo.volar();      // ERROR: Pez no mezcla Volador
}
Donald hace sonido Volando… Nadando… Nadando…

23.1 · Mixins con estado y super en cadena

mixin Logger {
  void log(String msg) => print('[LOG] $msg');
}

mixin Timestamped {
  DateTime get creadoEn => DateTime.now();
}

class Pedido with Logger, Timestamped {
  String producto;
  Pedido(this.producto);

  void confirmar() {
    log('Pedido "$producto" confirmado a las ${creadoEn.toIso8601String()}');
  }
}

void main() => Pedido('Teclado mecánico').confirmar();
[LOG] Pedido "Teclado mecánico" confirmado a las 2026-08-22T09:41:00.000

23.2 · Orden de mezcla: derecha gana

mixin Caminante {
  void moverse() => print('Camino');
}

mixin Corredor {
  void moverse() => print('Corro');
}

class Persona with Caminante, Corredor {}   // ¡el último gana!

void main() => Persona().moverse();         // Corro
Corro

23.3 · Restricción on: solo para ciertas jerarquías

abstract class Vehiculo {
  int km = 0;
  void rodar() => km += 10;
}

// Solo puede mezclarse EN clases Vehiculo o hijas:
mixin Turbo on Vehiculo {
  void activarTurbo() {
    rodar();            // garantizado: existe porque venimos de Vehiculo
    km += 90;
    print('¡Turbo! Total km: $km');
  }
}

class CocheDeportivo extends Vehiculo with Turbo {}

void main() => CocheDeportivo().activarTurbo();
¡Turbo! Total km: 100

23.4 · mixin class: lo mejor de ambos (Dart 3)

// Puede usarse como mixin (with) Y como clase padre (extends):
mixin class Utilidades {
  int doble(int n) => n * 2;
}

class Calculadora with Utilidades {}
void main() => print(Calculadora().doble(21)); // 42
En Flutter verás mixins por todas partes: SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin… Ahora sabes exactamente qué son.

Puntos clave

  • with mezcla varios mixins: composición sin acoplar jerarquías.
  • Ante conflictos, el mixin más a la derecha prevalece.
  • on Tipo restringe dónde puede aplicarse y habilita sus miembros.

24 · Enums potenciados

Intermedio ~20 min

24.1 · Enums básicos

enum Estado { pendiente, enviando, entregado }

void main() {
  var e = Estado.enviando;

  print(e.name);          // 'enviando'
  print(e.index);         // 1
  print(Estado.values);   // lista con todos los casos
  print(Estado.values.length);

  final porNombre = Estado.values.byName('entregado');
  print(porNombre);
}
enviando 1 [Estado.pendiente, Estado.enviando, Estado.entregado] 3 Estado.entregado

24.2 · Switch exhaustivo: el compilador te protege

String iconoDe(Estado estado) => switch (estado) {
      Estado.pendiente => 'reloj',
      Estado.enviando => 'camion',
      Estado.entregado => 'check',
    };
// Si mañana añades un caso al enum y olvidas esta función,
// el COMPILADOR marca el error antes de ejecutar nada.

void main() {
  for (final e in Estado.values) {
    print('${e.name} -> ${iconoDe(e)}');
  }
}
pendiente -> reloj enviando -> camion entregado -> check

24.3 · Enums mejorados: campos y métodos

// Cada caso puede llevar datos y comportamiento propio:
enum Planeta {
  mercurio(0.39),
  tierra(1.00),
  marte(1.52);

  // Campos y constructor CONSTANTES obligatorios:
  final double distanciaSolUA;
  const Planeta(this.distanciaSolUA);

  double anosLuzARecorrer(int anios) => distanciaSolUA * anios;

  bool get esInterior => distanciaSolUA < 1;
}

void main() {
  print(Planeta.tierra.distanciaSolUA);   // 1.0
  print(Planeta.marte.esInterior);        // false
  print(Planeta.mercurio.anosLuzARecorrar ?? '');
}
// Nota: la última línea contiene una errata a propósito —
// corrígela a anosLuzARecorrer(10) para ver: 3.9
Ejercicio: la línea final del ejemplo anterior tiene un nombre mal escrito. Ejecútalo, lee el error del analizador y arréglalo. Aprender a leer errores es parte del oficio.
// Enum que implementa una interfaz:
abstract interface class Sonido {
  String get onomatopeya;
}

enum Animalito implements Sonido {
  gato('miau'),
  perro('guau');

  @override
  final String onomatopeya;
  const Animalito(this.onomatopeya);
}

void main() => print(Animalito.gato.onomatopeya); // miau

Puntos clave

  • Enums = conjuntos finitos de constantes seguras.
  • switch sobre enums es exhaustivo: cero casos olvidados.
  • Los enums mejorados llevan campos, getters y métodos.

25 · Extensiones y extension types

Avanzado ~25 min

Una extensión añade miembros nuevos a tipos existentes — incluso de los que no puedes modificar como int o String — sin herencia ni envoltorios.

extension StringUtil on String {
  bool get esEmail => contains('@') && endsWith('.com');

  String capitalizar() =>
      isEmpty ? this : this[0].toUpperCase() + substring(1);

  String get invertida => split('').reversed.join();
}

extension IntUtil on int {
  int get enBytes => this * 1024 * 1024;   // MB a bytes
  bool get esPrimo {
    if (this < 2) return false;
    for (var i = 2; i * i <= this; i++) {
      if (this % i == 0) return false;
    }
    return true;
  }
}

void main() {
  print('hola@correo.com'.esEmail);
  print('ana'.capitalizar());
  print('dart'.invertida);
  print(500.enBytes);
  print([2, 4, 7].where((n) => n.esPrimo));
}
true Ana trad 524288000 (2, 7)

25.1 · Extensiones genéricas

extension ListaX<T> on List<T> {
  T? segundoOrNull() => length >= 2 ? this[1] : null;

  List<T> sinDuplicados() => toSet().toList();

  void intercambiar(int i, int j) {
    if (i.isNegative || j.isNegative || j >= length || i >= length) return;
    final temp = this[i];
    this[i] = this[j];
    this[j] = temp;
  }
}

void main() {
  print(['a'].segundoOrNull());          // null, sin excepción
  print([1, 2, 2, 3].sinDuplicados());
}
null [1, 2, 3]

25.2 · Conflictos: elegir extensión explícitamente

extension A on String {
  String get saludo => 'Hola desde A';
}

extension B on String {
  String get saludo => 'Hola desde B';
}

void main() {
  var s = '';
  // print(s.saludo);           // AMBIGUO: error de compilación
  print(A(s).saludo);           // resuelto nombrando la extensión
  print(B(s).saludo);
}
Hola desde A Hola desde B

25.3 · Extension types: envoltorios gratis (Dart 3)

A diferencia de las extensiones (que añaden métodos), los extension types crean tipos nuevos que en compilación son distintos pero en ejecución son el valor original: cero coste de rendimiento.

extension type Metros(double valor) {
  Metros operator +(Metros otro) => Metros(valor + otro.valor);
  String get descripcion => '${valor}m';
}

extension type Pies(double valor) {}

void main() {
  var alto = Metros(1.8) + Metros(0.2);
  print(alto.descripcion);   // 2.0m
  print(alto.valor);         // acceso al representación interna

  // Metros m = Pies(6);      // ERROR: ¡tipos incompatibles en compilación!
  // Metros m = 1.8;          // ERROR: no se confunde con double
}
2.0m 2.0

Ideal para IDs, unidades de medida o claves: ganas seguridad de tipos sin crear objetos extra en memoria.

Puntos clave

  • extension X on Tipo: métodos/getters nuevos para cualquier tipo.
  • Son estáticos: no hay polimorfismo ni acceso a privados.
  • extension type crea alias fuertes sin coste en runtime.

26 · Metadatos y anotaciones

Avanzado ~15 min

Las anotaciones (@algo) son metadatos: información para el compilador, el analizador y las herramientas de generación de código.

26.1 · Las que usarás siempre

class Padre {
  void saludar() => print('Hola');
}

class Hijo extends Padre {
  @override                       // verifica que SÍ existe en el padre
  void saludar() => print('¡Hola!');

  // @override sobre un método inexistente = ERROR del analizador:
  // @override
  // void saludarr() {}            // The method doesn't override...
}

void main() {
  @Deprecated('Usa nuevaApi() en su lugar')
  void viejaApi() => print('vieja');

  viejaApi();   // el IDE tacha la llamada y muestra el mensaje
}

26.2 · Crear tus propias anotaciones

// Una anotación es una clase const normal:
class Ruta {
  final String path;
  final String metodo;
  const Ruta(this.path, {this.metodo = 'GET'});
}

enum Rol { admin, usuario }

class RequiereRol {
  final Rol rol;
  const RequiereRol(this.rol);
}

@Ruta('/usuarios', metodo: 'POST')
@RequiereRol(Rol.admin)
void crearUsuario() => print('Usuario creado');

void main() {
  crearUsuario();
  // Las anotaciones se leen con herramientas de análisis/generación de código,
  // no con reflexión (los mirrors no existen en Flutter por rendimiento).
}
Usuario creado
¿Quién lee estas anotaciones? Paquetes como json_serializable, freezed o generadores de rutas usan build_runner para generar código a partir de ellas. En Flutter las verás constantemente.

Puntos clave

  • @override y @Deprecated son tu red de seguridad diaria.
  • Tu propia anotación = clase con constructor const.
  • Sin reflexión en Flutter: las herramientas generan código en tiempo de compilación.

27 · Genéricos: tipos parametrizados

Avanzado ~25 min

Los genéricos escriben código una vez y lo hacen seguro para muchos tipos: List<int>, Map<String, Usuario>, Future<List<Producto>>… todo son genéricos.

27.1 · Clase genérica

class Caja<T> {
  T contenido;
  Caja(this.contenido);

  T abrir() => contenido;

  Caja<R> mapear<R>(R Function(T) transformar) => Caja(transformar(contenido));
}

void main() {
  final cajaTexto = Caja('regalo');
  print(cajaTexto.abrir().toUpperCase());   // sabe que es String

  final cajaNum = Caja(42);
  print(cajaNum.abrir() + 8);               // sabe que es int

  final cajaDoble = cajaNum.mapear((n) => n * 2);
  print(cajaDoble.abrir());
}
REGALO 50 84

27.2 · Restricciones con extends

// T debe ser num o un subtipo: podemos sumar sin miedo
T sumaDeLista<T extends num>(List<T> valores) {
  T acumulado = valores.first;
  for (var i = 1; i < valores.length; i++) {
    acumulado = acumulado + valores[i] as T;
  }
  return acumulado;
}

void main() {
  print(sumaDeLista([1, 2, 3]));       // 6
  print(sumaDeLista([1.5, 2.5]));      // 4.0
  // print(sumaDeLista(['a']));        // ERROR: String no es num
}

27.3 · Genéricos reificados: existen en runtime

void main() {
  var lista = <int>[1, 2, 3];

  // A diferencia de Java, aquí el tipo SÍ viaja hasta la ejecución:
  print(lista is List<int>);      // true
  print(lista is List<String>);   // false
  print(lista.runtimeType);       // List<int>

  // Por eso esto falla en runtime, no solo en análisis:
  List dinamicos = [1, 'dos'];
  if (dinamicos is List<String>) print(dinamicos.first.toUpperCase());
}

27.4 · Un genérico práctico: Resultado tipo Either

// Patrón muy usado en arquitecturas limpias y paquetes como fpdart:
sealed class Resultado<T> {}

class Exito<T> extends Resultado<T> {
  final T valor;
  Exito(this.valor);
}

class Fallo<T> extends Resultado<T> {
  final String error;
  Fallo(this.error);
}

Resultado<double> dividir(double a, double b) {
  if (b == 0) return Fallo('División por cero');
  return Exito(a / b);
}

void main() {
  for (final caso in [dividir(10, 2), dividir(10, 0)]) {
    switch (caso) {
      case Exito<double>(:final valor):
        print('OK: $valor');
      case Fallo<double>(:final error):
        print('ERROR: $error');
    }
  }
}
OK: 5.0 ERROR: División por cero

(El sealed y los patrones :final se explican a fondo en el capítulo 30.)

Puntos clave

  • <T> parametriza clases y métodos; adiós casts manuales.
  • T extends Tipo restringe y habilita operaciones.
  • Dart reifica genéricos: is List<int> funciona en runtime.

28 · Excepciones y manejo de errores

Avanzado ~30 min

Cuando algo sale mal, Dart «lanza» (throw) un objeto que interrumpe el flujo normal. Tu trabajo: capturar lo esperable y dejar que lo inesperado crashee con información útil.

TipoSemánticaEjemplos
ExceptionFallo esperable: datos de entrada, red, formato…FormatException, IOException
ErrorBug del programa: no se captura, se corrigeRangeError, StateError, TypeError

28.1 · try / on / catch / finally

void main() {
  try {
    int.parse('no soy un número');
  } on FormatException {                 // captura UN tipo concreto, sin variable
    print('Formato inválido');
  }

  try {
    var lista = <int>[];
    lista.first;
  } on StateError catch (e) {            // tipo + objeto de error
    print('Error controlado: ${e.message}');
  }

  try {
    throw Exception('algo raro');
  } catch (e) {                          // captura CUALQUIER cosa lanzada
    print('Capturado: $e');
  } finally {
    print('finally SIEMPRE corre (cierres, logs…)');
  }
}
Formato inválido Error controlado: No element Capturado: Exception: algo raro finally SIEMPRE corre (cierres, logs…)

28.2 · StackTrace: el mapa del crimen

void nivelProfundo() => throw ArgumentError('valor prohibido');

void main() {
  try {
    nivelProfundo();
  } on ArgumentError catch (e, s) {   // segundo parámetro: StackTrace
    print('Mensaje: $e');
    print('Origen:');
    print(s.toString().split('\n').take(3).join('\n'));
  }
}
Mensaje: Invalid argument(s): valor prohibido Origen: #0 nivelProfundo (file:///…main.dart:1:26) #1 main (file:///…main.dart:5:5)

28.3 · rethrow: registrar sin tragar el error

Future<String> descargar() async => throw Exception('red caída');

void main() async {
  try {
    await descargar();
  } catch (e) {
    print('Registrando en telemetría: $e');
    rethrow;   // vuelve a lanzar el MISMO error conservando el stack original
  }
}
Registrando en telemetría: Exception: red caída Unhandled exception: Exception: red caída …

28.4 · Excepciones personalizadas

class SaldoInsuficienteException implements Exception {
  final double faltante;
  SaldoInsuficienteException(this.faltante);

  @override
  String toString() => 'SaldoInsuficiente: faltan \$${faltante.toStringAsFixed(2)}';
}

void retirar(double saldo, double monto) {
  if (monto > saldo) {
    throw SaldoInsuficienteException(monto - saldo);
  }
  print('Retiro OK');
}

void main() {
  for (final monto in [50.0, 500.0]) {
    try {
      retirar(100, monto);
    } on SaldoInsuficienteException catch (e) {
      print(e);
    }
  }
}
Retiro OK SaldoInsuficiente: faltan $400.00

28.5 · assert: guardas solo para desarrollo

double aplicarDescuento(double precio, double porcentaje) {
  assert(precio >= 0, 'El precio nunca puede ser negativo');
  assert(porcentaje >= 0 && porcentaje <= 100);
  return precio * (1 - porcentaje / 100);
}

void main() => print(aplicarDescuento(100, 15)); // 85.0
// En producción (dart compile) los asserts desaparecen: coste cero.

Puntos clave

  • Lanza Exception para fallos esperables; deja que los Error rompan.
  • on Tipo catch (e, s): específico primero; catch (e) genérico después.
  • rethrow preserva el stack; throw e lo reinicia.
  • assert documenta invariantes gratis en desarrollo.

29 · Records: tuplas estructuradas (Dart 3)

Avanzado ~25 min

Un record agrupa varios valores en un solo objeto inmutable, con tipo exacto y sin crear una clase ad-hoc. Perfecto para devolver varios resultados de golpe.

29.1 · Posicionales: (valor, valor)

void main() {
  var par = (42, 'respuesta');          // Record de 2 campos
  print(par.$1);                        // 42   (posiciones desde $1)
  print(par.$2);                        // respuesta

  (int, String, bool) triple = (7, 'siete', true);  // tipado explícito
  print(triple);

  // Igualdad ESTRUCTURAL: por contenido, no por identidad
  var a = (1, 'x');
  var b = (1, 'x');
  print(a == b);            // true
  print(identical(a, b));   // false
}
42 respuesta (7, siete, true) true false

29.2 · Campos nombrados

(String, {int edad}) persona(String nombre, int anios) =>
    (nombre, edad: anios);

void main() {
  final ana = persona('Ana', 34);
  print(ana.$1);      // campo posicional
  print(ana.edad);    // campo nombrado

  // Mezcla permitida: posicionales primero, luego {nombrados}
  ({String titulo, List<double> notas}) boletin =
      (titulo: 'Matemáticas', notas: [4.5, 9.0]);
  print('${boletin.titulo}: ${boletin.notas}');
}
Ana 34 Matemáticas: [4.5, 9.0]

29.3 · El caso de uso estrella: múltiples retornos

// Antes: clases contenedoras o parámetros mutables. Ahora:
(double minima, double maxima, double promedio) resumen(List<double> datos) {
  final ordenados = [...datos]..sort();
  return (
    ordenados.first,
    ordenados.last,
    datos.reduce((a, b) => a + b) / datos.length,
  );
}

void main() {
  final stats = resumen([3.5, 8.0, 5.5]);
  print('min ${stats.$1} | max ${stats.$2} | media ${stats.$3.toStringAsFixed(2)}');

  // Con destructuring directo a variables:
  final (min, max, promedio) = resumen([10, 20]);
  print('min $min max $max media $promedio');
}
min 3.5 | max 8.0 | media 5.67 min 10.0 max 20.0 media 15.0
¿Record o clase? Records para valores efímeros y locales (resultados intermedios, claves compuestas). Clases para conceptos del dominio con comportamiento y validación.

Puntos clave

  • (a, b) posicional; (x: 1) nombrado; se pueden mezclar.
  • Inmutables e iguales por estructura: útiles como claves de Map o en Sets.
  • final (a, b) = f(); desestructura el retorno al instante.

30 · Patterns y sealed classes (Dart 3)

Avanzado ~35 min

Dart 3 introdujo el pattern matching: comprobar la forma de un dato, extraer sus partes y decidir — todo en una sola expresión legible.

30.1 · Tipos de patrones básicos

String describir(Object valor) => switch (valor) {
      null                    => 'es nulo',              // patrón null
      0                       => 'cero',                 // literal
      int n when n < 0        => 'entero negativo: $n',  // variable + guardia when
      int()                   => 'entero positivo',      // object pattern vacío
      String s                => 'texto: $s',
      [_, _, ...]             => 'lista con 2+ elementos', // list pattern
      {'rol': String rol}     => 'mapa con rol=$rol',      // map pattern
      _                       => 'otra cosa',            // comodín
    };

void main() {
  for (final v in [null, 0, -5, 99, 'hola', ['a', 'b'], {'rol': 'admin'}, 3.14]) {
    print(describir(v));
  }
}
es nulo cero entero negativo: -5 entero positivo texto: hola lista con 2+ elementos mapa con rol=admin otra cosa

30.2 · Object patterns: desmontar objetos

class Punto {
  final double x, y;
  const Punto(this.x, this.y);
}

String cuadrante(Punto p) => switch (p) {
      Punto(x: >= 0, y: >= 0) => 'I',      // patrones relacionales dentro del objeto
      Punto(x: < 0, y: >= 0)  => 'II',
      Punto(x: < 0, y: < 0)   => 'III',
      Punto(x: >= 0, y: < 0)  => 'IV',
      _                       => '?',
    };

void main() {
  for (final p in [Punto(1, 1), Punto(-2, 5), Punto(-1, -1), Punto(4, -9)]) {
    print(cuadrante(p));
  }

  // Extraer variables directamente:
  final Punto(x: px, y: py) = Punto(3, 4);   // destructuring declarativo
  print('px=$px py=$py');
}
I II III IV px=3.0 py=4.0

30.3 · List, map y record patterns

void main() {
  // Lista exacta / prefijo / resto variable
  final [a, b] = [1, 2];
  final [primero, ...resto] = [1, 2, 3, 4];
  print('$a $b | primero=$primero resto=$resto');

  // Mapa: comprueba claves y captura valores
  final json = {'nombre': 'Ada', 'rol': 'dev'};
  final {'nombre': String nombre, 'rol': String rol} = json;
  print('$nombre es $rol');

  // Record anidado
  final (id, (nombre2, activo)) = (7, ('Luis', true));
  print('id=$id nombre=$nombre2 activo=$activo');
}
1 2 | primero=1 resto=[2, 3, 4] Ada es dev id=7 nombre=Luis activo=true

30.4 · Sealed classes: jerarquías cerradas

sealed declara que TODAS las subclases viven en la misma biblioteca. El compilador conoce la jerarquía completa y puede exigir exhaustividad:

sealed class EstadoCarga {}

class Cargando extends EstadoCarga {}
class Exito extends EstadoCarga {
  final List<String> items;
  Exito(this.items);
}
class ErrorDeRed extends EstadoCarga {
  final String mensaje;
  ErrorDeRed(this.mensaje);
}

// SIN default: si mañana añades un estado nuevo, el compilador
// te obligará a manejarlo aquí. Eso es un superpoder.
String vista(EstadoCarga estado) => switch (estado) {
      Cargando()           => 'Mostrando spinner…',
      Exito(items: var it) => 'Lista: ${it.join(", ")}',
      ErrorDeRed(:final mensaje) => 'Fallo: $mensaje',
    };

void main() {
  for (final s in [Cargando(), Exito(['A', 'B']), ErrorDeRed('timeout')]) {
    print(vista(s));
  }
}
Mostrando spinner… Lista: A, B Fallo: timeout
sealed vs abstract: abstract permite subclases en cualquier archivo (jerarquía abierta). sealed las cierra al archivo actual y compra exhaustividad en los switches. Úsalos juntos cuando tenga sentido.

30.5 · if-case y guards when

void main() {
  final respuestas = [200, 'OK'];

  if (respuestas case [int codigo, String texto] when codigo == 200) {
    print('HTTP $codigo ($texto)');
  }

  Object dato = 42;
  if (dato case int n when n.isEven) {
    print('par: $n');
  }
}
HTTP 200 (OK) par: 42

Puntos clave

  • Patterns combinan comprobación + extracción en una sola línea segura.
  • when añade condiciones a cualquier caso.
  • sealed convierte jerarquías cerradas en switches exhaustivos.
  • Esta sintaxis domina el código Dart moderno (incluido Flutter 3.x+).

31 · Futures, async y await

Avanzado ~35 min

Las operaciones reales tardan: red, disco, GPS… Un Future<T> representa un valor de tipo T que estará disponible más adelante. Con async/await lo esperas escribiendo código tan claro como el síncrono.

31.1 · El problema y la solución moderna

// Simulamos una llamada lenta:
Future<String> pedirNombreUsuario() async {
  await Future.delayed(const Duration(seconds: 1));   // espera sin bloquear
  return 'Ada';
}

// Estilo antiguo con callbacks (.then):
void main() {
  print('A');
  pedirNombreUsuario().then((nombre) => print('B: $nombre'));
  print('C');
}
A C B: Ada

¡Ojo al orden! El programa no se detiene en B: continúa con C mientras llega la respuesta.

Future<String> pedirNombreUsuario() async {
  await Future.delayed(const Duration(seconds: 1));
  return 'Ada';
}

// Estilo moderno: await pausa SOLO esta función, no toda la app
Future<void> main() async {
  print('A');
  final nombre = await pedirNombreUsuario();
  print('B: $nombre');
  print('C');
}
A B: Ada C

31.2 · Secuencial frente a paralelo

Future<int> descargar(String archivo) async {
  await Future.delayed(const Duration(seconds: 1));
  return archivo.length;
}

Future<void> main() async {
  var inicio = DateTime.now();

  // SECUENCIAL: uno tras otro (~2 s)
  final a = await descargar('a.json');
  final b = await descargar('b.json');

  // PARALELO: ambos a la vez (~1 s)
  final resultados = await Future.wait([
    descargar('c.json'),
    descargar('d.json'),
  ]);

  final total = DateTime.now().difference(inicio).inMilliseconds;
  print('$a $b ${resultados} en ~${total}ms');
}
6 6 [7, 7] en ~2005ms

31.3 · Errores asíncronos: try/catch funciona igual

Future<double> cotizacion(String moneda) async {
  await Future.delayed(const Duration(milliseconds: 300));
  if (moneda == 'BTC') throw Exception('cotización no disponible');
  return 1.08;
}

Future<void> main() async {
  try {
    final eur = await cotizacion('EUR');
    print('EUR: $eur');
    final btc = await cotizacion('BTC');
    print(btc);
  } catch (e) {
    print('Fallo: $e');
  } finally {
    print('Consulta terminada');
  }

  // Alternativa encadenada para APIs estilo callback:
  await cotizacion('BTC')
      .then((v) => print(v))
      .catchError((e) => print('catchError: $e'))
      .whenComplete(() => print('siempre'));
}
EUR: 1.08 Fallo: Exception: cotización no disponible Consulta terminada catchError: Exception: cotización no disponible siempre

31.4 · Timeouts: no esperar para siempre

Future<void> main() async {
  try {
    final respuesta = await servicioLento().timeout(
      const Duration(seconds: 2),
      onTimeout: () => 'respuesta por defecto',
    );
    print(respuesta);
  } catch (e) {
    print(e);
  }
}

Future<String> servicioLento() async {
  await Future.delayed(const Duration(seconds: 5));
  return 'demasiado tarde';
}
respuesta por defecto

31.5 · Reglas de oro del async

  • Toda función que use await debe marcarse async (y devolver Future<T> o Stream<T>).
  • await solo espera ese futuro; el resto del programa sigue vivo. En Flutter la UI nunca se congela.
  • Lanza errores dentro de funciones async: viajan por el Future hasta quien haga await.
  • Pide varias cosas independientes en paralelo con Future.wait: los usuarios odian las pantallas lentas.
  • Nunca uses await sin capturar errores donde importe: un Future sin manejar es un crash silencioso.

Puntos clave

  • Future<T> = valor futuro; async/await = su sintaxis cómoda.
  • try/catch/finally funcionan idéntico en código async.
  • Future.wait paraleliza; timeout evita esperas eternas.

32 · Streams y generadores

Avanzado ~35 min

Si un Future es un valor único futuro, un Stream<T> es una secuencia de valores que llegan con el tiempo: pulsaciones del usuario, posiciones GPS, descargas por trozos, mensajes de chat.

32.1 · Escuchar un stream

Stream<int> segundos() async* {          // generator: capítulo siguiente
  for (var i = 1; i <= 3; i++) {
    await Future.delayed(const Duration(seconds: 1));
    yield i;
  }
}

void main() {
  final sub = segundos().listen(
    (valor) => print('Tick $valor'),      // cada dato recibido
    onDone: () => print('Stream terminado'),
    onError: (e) => print('Error: $e'),
  );

  // sub.cancel(); // detiene la escucha cuando quieras
}
(un tick por segundo) Tick 1 Tick 2 Tick 3 Stream terminado

32.2 · await for: consumir como una lista

Future<void> main() async {
  var suma = 0;
  await for (final n in segundos()) {   // pausa hasta que llega CADA valor
    suma += n;
    print('Acumulado: $suma');
  }
  print('Total: $suma');
}
Acumulado: 1 Acumulado: 3 Acumulado: 6 Total: 6

32.3 async*: generar streams con yield

Una función async* devuelve un stream. Cada yield emite un valor y la ejecución se suspende hasta que el consumidor lo pide:

Stream<String> descargarPorPartes(List<String> partes) async* {
  for (var i = 0; i < partes.length; i++) {
    await Future.delayed(const Duration(milliseconds: 200)); // trabajo lento
    yield 'parte ${i + 1}/${partes.length}: ${partes[i]}';
  }
}

Future<void> main() async {
  await for (final progreso in descargarPorPartes(['intro', 'cuerpo', 'fin'])) {
    print(progreso);
  }
}

// Delegación con yield*:
Stream<int> cuentaHasta(int n) async* {
  if (n >= 0) {
    yield n;
    yield* cuentaHasta(n - 1);   // reenvía todo el stream recursivo
  }
}
parte 1/3: intro parte 2/3: cuerpo parte 3/3: fin

32.4 sync*: iterables perezosos con yield

La prima síncrona genera Iterables infinitos o costosos sin materializarlos:

Iterable<int> naturales() sync* {
  var i = 1;
  while (true) {
    yield i++;                    // infinito… pero solo se calcula lo pedido
  }
}

void main() {
  final primeros5 = naturales().take(5).toList();
  print(primeros5);               // [1, 2, 3, 4, 5]

  Iterable<String> fibonaccis() sync* {
    int a = 0, b = 1;
    while (true) {
      yield '$a';
      [a, b] = [b, a + b];        // destructuring de records (capítulo 29)
    }
  }
  print(fibonaccis().take(8).toList());
}
[1, 2, 3, 4, 5] [0, 1, 1, 2, 3, 5, 8, 13]

32.5 · StreamController: crear streams a medida

import 'dart:async';

class Termometro {
  final _controlador = StreamController<double>();

  Stream<double> get temperaturas => _controlador.stream;

  void medir(double celsius) => _controlador.add(celsius);

  void cerrar() => _controlador.close();
}

void main() async {
  final t = Termometro();

  t.temperaturas
      .where((temp) => temp > 30)
      .map((temp) => 'ALERTA: ${temp}°C')
      .listen(print);

  t.medir(25);   // no pasa el filtro: silencio
  t.medir(33);
  await Future.delayed(Duration.zero);
  t.cerrar();
}
ALERTA: 33.0°C
Broadcast: por defecto un stream admite UN oyente. Con StreamController.broadcast() varios oyentes reciben lo mismo — así funcionan los «providers» de estado en Flutter.

Puntos clave

  • Stream = muchos valores futuros; Future = uno.
  • async*/yield crea streams; sync*/yield crea iterables perezosos.
  • await for, listen() y operadores como where/map/take procesan el flujo.

33 · Event loop e isolates

Avanzado ~30 min

Cada programa Dart corre en uno o más isolates: unidades de ejecución con su propia memoria y su propio bucle de eventos. Dentro de un isolate todo es de un solo hilo, lo que elimina las condiciones de carrera clásicas… si sabes delegar el trabajo pesado.

33.1 · El event loop: dos colas

  1. Cola de microtasks: tareas urgentes internas (scheduleMicrotask, continuaciones de async). Se vacía SIEMPRE antes.
  2. Cola de eventos: timers, I/O, toques del usuario, mensajes entre isolates.
  3. Cuando no queda código síncrono ni microtasks, se procesa un evento… y vuelta a empezar.
import 'dart:async';

void main() {
  print('1. síncrono');

  scheduleMicrotask(() => print('3. microtask'));

  Future(() => print('4. evento (Future)'));
  Future.delayed(Duration.zero, () => print('5. evento retrasado'));

  print('2. fin del síncrono');
}
1. síncrono 2. fin del síncrono 3. microtask 4. evento (Future) 5. evento retrasado
Consecuencia: un cálculo largo SIN await (un bucle de millones de iteraciones) bloquea el event loop: la UI se congela. Los futures no crean magia paralela — solo organizan esperas.

33.2 · Isolate.run: paralelismo en una línea

// Cálculo CPU-intensivo que congelaría una UI:
int fibonacciLento(int n) {
  if (n < 2) return n;
  return fibonacciLento(n - 1) + fibonacciLento(n - 2);
}

Future<void> main() async {
  print('UI sigue fluida mientras calculo…');

  final resultado = await Isolate.run(() => fibonacciLento(30));

  print('fib(30) = $resultado');
}
UI sigue fluida mientras calculo… fib(30) = 832040

La función viaja a otro isolate con copia de los datos necesarios; al terminar, el resultado vuelve al principal.

33.3 · Isolate.spawn y comunicación por puertos

import 'dart:async';
import 'dart:isolate';

// Cada isolate recibe UN mensaje inicial: usaremos un SendPort para dialogar.
void trabajador(SendPort canalHaciaMain) {
  final puerto = ReceivePort();
  canalHaciaMain.send(puerto.sendPort);   // entrego mi buzón

  puerto.listen((mensaje) {
    final numeros = mensaje as List<int>;
    final suma = numeros.reduce((a, b) => a + b);
    canalHaciaMain.send(suma);            // respondo
    puerto.close();                       // y termino
  });
}

Future<void> main() async {
  final canalDesdeMain = ReceivePort();
  await Isolate.spawn(trabajador, canalDesdeMain.sendPort);

  // Primer mensaje: el SendPort del trabajador
  final SendPort haciaTrabajador = await canalDesdeMain.first;

  // Segundo uso: sus respuestas. Redirijo con un StreamQueue manual simple:
  final respuestas = StreamIterator(canalDesdeMain);
  haciaTrabajador.send([1000000, 2000000, 3000000]);

  await respuestas.moveNext();
  print('Suma calculada en otro isolate: ${respuestas.current}');

  respuestas.cancel();
}
Suma calculada en otro isolate: 6000000

33.4 · Reglas prácticas

  • < ~16 ms de trabajo: hazlo inline; el event loop ni se enterará.
  • Esperas (I/O): usa async/await, NO isolates.
  • Cálculo pesado (parsear JSON enorme, procesar imágenes, cifrar): Isolate.run.
  • En Flutter existe compute(), un envoltorio de este mismo mecanismo.
  • Los mensajes entre isolates deben ser «enviables»: datos simples o inmutables (desde Dart 2.15+ casi cualquier objeto puede viajar).

Puntos clave

  • Dart es single-thread por isolate; async organiza esperas, no cálculo.
  • Microtasks antes que eventos: orden predecible si entiendes las dos colas.
  • Isolate.run mueve el trabajo CPU-pesado fuera del hilo de UI.

34 · Librerías, paquetes y pub.dev

Avanzado ~25 min

34.1 · Las tres formas de importar

import 'dart:math';                    // SDK oficial (dart:)
import 'package:http/http.dart';       // paquete externo de pub.dev
import '../herramientas/utiles.dart';  // archivo propio relativo

// Control fino:
import 'dart:math' show max;           // solo trae max
import 'dart:convert' hide ascii;      // todo menos ascii
import 'package:intl/intl.dart' as intl; // prefijo para evitar choques

void main() {
  print(max(3, 7));
  print(intl.NumberFormat.currency(locale: 'es').format(1234.5));
}
Privacidad = guion bajo: en Dart lo que empieza por _ es privado a nivel de biblioteca, no de clase. Dos archivos no se ven los privados mutuamente aunque estén en la misma carpeta.

34.2 · pubspec.yaml: el corazón del proyecto

name: mi_app
description: Una app de ejemplo.
version: 1.2.3+4        # versión semántica +build
environment:            # versiones mínimas del SDK
  sdk: ^3.5.0

dependencies:           # para producción
  http: ^1.2.0
  intl: any

dev_dependencies:       # solo desarrollo y pruebas
  lints: ^4.0.0
  test: ^1.24.0

Cómo leer las restricciones:

  • ^1.2.0 — «cualquier 1.x.x desde 1.2.0» (compatible). La más usada.
  • 1.2.0 — exacta: congela la dependencia.
  • >=1.0.0 <2.0.0 — rango explícito.
  • any — déjame elegir (evítalo en producción).

34.3 · Comandos de pub

dart pub add http # añade dependencia Y la descarga dart pub get # descarga según pubspec.lock dart pub upgrade # actualiza dentro de las restricciones dart pub outdated # ¿qué quedó atrás? dart pub deps # árbol de dependencias

34.4 · Organizar tu propio código en librerías

lib/ ├── mi_app.dart # barril: exporta la API pública └── src/ # implementación privada por convención ├── usuario.dart └── repositorio.dart
// lib/mi_app.dart — el único archivo que otros deben importar:
export 'src/usuario.dart';
export 'src/repositorio.dart';

// En cualquier consumidor:
// import 'package:mi_app/mi_app.dart';   // limpio y estable

34.5 · Reglas de estilo con analysis_options.yaml

include: package:lints/recommended.yaml

analyzer:
  language:
    strict-casts: true
  errors:
    invalid_assignment: error

linter:
  rules:
    - prefer_final_locals
    - avoid_print        # avisa si imprimes sin querer en lib/
    - always_declare_return_types

Puntos clave

  • Tres orígenes: dart:, package:, rutas relativas.
  • ^ expresa compatibilidad; el lock file fija versiones exactas.
  • lib/src privado + archivo barril = API pública ordenada.

35 · Utilidades esenciales: DateTime, RegExp, JSON e I/O

Avanzado ~30 min

35.1 · Fechas con DateTime y Duration

void main() {
  final ahora = DateTime.now();
  final lanzamientoFlutter = DateTime(2015, 12, 4);

  print(lanzamientoFlutter.weekday);         // 5 (viernes)
  print(lanzamientoFlutter.isBefore(ahora)); // true

  final futuro = ahora.add(const Duration(days: 30));
  final diferencia = futuro.difference(ahora);

  print(diferencia.inDays);
  print(futuro.year);

  // Formato ISO 8601 (el estándar de las APIs):
  final iso = DateTime.parse('2026-08-22T10:30:00');
  print(iso.add(const Duration(hours: 8)).toIso8601String());
}
5 true 30 2026 2026-08-22 18:30:00.000

35.2 · Expresiones regulares

void main() {
  final telefono = RegExp(r'^\d{3}-\d{4}$');

  print(telefono.hasMatch('555-1234'));   // true
  print(telefono.hasMatch('abc'));        // false

  final palabras = RegExp(r'\w+');
  print(palabras.allMatches('Dart mola mucho').length);   // 3
  print('Hola mundo'.replaceAllMapped(
      RegExp(r'o'), (m) => '0'));                          // H0la mund0
}

35.3 · JSON: el puente hacia cualquier API

import 'dart:convert';

class Producto {
  final int id;
  final String nombre;
  final double precio;

  Producto({required this.id, required this.nombre, required this.precio});

  // Deserialización manual: mapa -> objeto
  factory Producto.desdeJson(Map<String, dynamic> json) => Producto(
        id: json['id'] as int,
        nombre: json['nombre'] as String,
        precio: (json['precio'] as num).toDouble(),
      );

  // Serialización: objeto -> mapa
  Map<String, dynamic> aJson() =>
      {'id': id, 'nombre': nombre, 'precio': precio};

  @override
  String toString() => '$nombre ($precio€)';
}

void main() {
  const crudo = '{"id": 1, "nombre": "Teclado", "precio": 49.9}';

  final decodificado = jsonDecode(crudo) as Map<String, dynamic>;
  final producto = Producto.desdeJson(decodificado);
  print(producto);

  final listaJson = jsonEncode([producto.aJson()]);
  print(listaJson);
}
Teclado (49.9€) [{"id":1,"nombre":"Teclado","precio":49.9}]

35.4 · Archivos y consola con dart:io

import 'dart:io';

Future<void> main(List<String> args) async {
  // Escribir y leer archivos:
  final notas = File('notas.txt');
  await notas.writeAsString('Primera línea\nSegunda línea\n');
  print(await notas.readAsString());

  // Añadir sin sobreescribir:
  await notas.writeAsString('Tercera\n', mode: FileMode.append);

  // Leer entrada del usuario (apps CLI):
  stdout.write('¿Tu nombre? ');
  final nombre = stdin.readLineSync();
  stdout.writeln('¡Hola, $nombre!');

  // Variables de entorno:
  print(Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']);

  // Listar un directorio:
  await for (final entidad in Directory('.').list()) {
    if (entidad is File) print(entidad.path);
  }
}
Primera línea Segunda línea ¡Hola, Ana! /home/palmeyda ./bin/mi_app.dart …

Puntos clave

  • DateTime es inmutable: add/difference devuelven nuevos valores.
  • jsonDecode/Encode + métodos desdeJson/aJson manuales bastan para empezar.
  • File/stdin/stdout/Directory cubren apps de consola completas.

36 · Pruebas unitarias con package:test

Avanzado ~30 min

El código sin pruebas es código heredado desde el día uno. Dart trae su framework oficial: package:test.

dart pub add --dev test

36.1 · Tu primera prueba

Código a probar en lib/calculadora.dart:

int sumar(int a, int b) => a + b;

double dividir(int a, int b) {
  if (b == 0) throw ArgumentError('El divisor no puede ser cero');
  return a / b;
}

Prueba en test/calculadora_test.dart:

import 'package:test/test.dart';
import 'package:mi_app/calculadora.dart';

void main() {
  test('sumar devuelve la suma de dos enteros', () {
    final resultado = sumar(2, 3);
    expect(resultado, equals(5));
  });

  test('dividir lanza ArgumentError con divisor cero', () {
    expect(() => dividir(10, 0), throwsArgumentError);
  });

  test('dividir calcula el cociente real', () {
    expect(dividir(10, 4), closeTo(2.5, 0.0001));   // para doubles
  });
}
dart test
00:00 +3: All tests passed!

36.2 · Matchers más útiles

MatcherComprueba que…
equals(x)igualdad profunda de valor
isA<Tipo>()es instancia del tipo
greaterThan(n), lessOrEqual(n)comparaciones numéricas
contains(x)la colección contiene el elemento
hasLength(n)longitud exacta
startsWith/endsWith/containssobre strings
throwsA(isA<FormatException>())lanza ese tipo concreto
completion(equals(x))un Future resuelve ese valor
emitsInOrder([a, b])un Stream emite en orden

36.3 · group, setUp y tearDown

class Carrito {
  final items = <String>[];
  void agregar(String p) => items.add(p);
  void vaciar() => items.clear();
}

void main() {
  group('Carrito', () {
    late Carrito carrito;

    setUp(() => carrito = Carrito());       // antes de CADA test

    test('empieza vacío', () => expect(carrito.items, isEmpty));

    test('acumula productos', () {
      carrito.agregar('café');
      carrito.agregar('té');
      expect(carrito.items, ['café', 'té']);
    });

    tearDown(() => carrito.vaciar());       // después de cada uno
  });
}

36.4 · Probar código asíncrono y streams

import 'dart:async';
import 'package:test/test.dart';

Future<String> cargarDato() async {
  await Future.delayed(const Duration(milliseconds: 50));
  return 'dato';
}

void main() {
  test('cargarDato resuelve el valor esperado', () async {
    final dato = await cargarDato();
    expect(dato, 'dato');
  });

  test('un stream emite en orden', () {
    Stream<int> numeros() async* { yield 1; yield 2; }
    expect(numeros(), emitsInOrder([1, 2, emitsDone]));
  });
}

Puntos clave

  • Un test = una afirmación clara sobre un comportamiento.
  • setUp/tearDown aíslan cada prueba entre sí.
  • Los matchers cubren sync, Futures y Streams por igual.
  • Ejecuta dart test tras cada cambio: feedback en segundos.

37 · Buenas prácticas y Effective Dart

Avanzado ~25 min

37.1 · Convenciones de nombres oficiales

ElementoEstiloEjemplo
Variables, funciones, parámetroslowerCamelCasetotalConIva()
Clases, enums, typedefs, extensionesUpperCamelCaseCarritoCompra
Constantes y enums valueslowerCamelCase (sí, también)const maxIntentos = 3;
Archivos y paquetessnake_casecarrito_compra.dart
Privadoprefijo __cacheInterno
Nombrados de libreríados puntoslibrary mi.app.core;

37.2 · Los hábitos que separan al profesional

  • Inmutabilidad por defecto: campos final, constructores const. Menos bugs, mejor rendimiento en Flutter.
  • Nada de dynamic innecesario: si el tipo se conoce, escríbelo o deja inferirlo.
  • Null safety como aliado: evita ! salvo certeza demostrable; prefiere ?? y promoción.
  • Funciones pequeñas y nombradas: si necesitas comentario para explicar qué hace un bloque, extrae una función con buen nombre.
  • Errores explícitos: lanza excepciones específicas o devuelve tipos Result; nunca strings vacíos ni null «porque sí».
  • Documentación ///: para API pública; comentarios // solo cuando el «porqué» no es obvio.
  • Formato automático: dart format antes de cada commit. Sin debates.

37.3 · Patrón copyWith: objetos inmutables cómodos

class Configuracion {
  final String tema;
  final String idioma;
  const Configuracion({this.tema = 'claro', this.idioma = 'es'});

  // Copia cambiando SOLO lo indicado:
  Configuracion copiarCon({String? tema, String? idioma}) => Configuracion(
        tema: tema ?? this.tema,
        idioma: idioma ?? this.idioma,
      );

  @override
  String toString() => '$tema/$idioma';
}

void main() {
  const base = Configuracion();
  print(base);
  print(base.copiarCon(tema: 'oscuro'));
  print(base);   // la original sigue intacta
}
claro/es oscuro/es claro/es

37.4 · Checklist antes de subir código

  1. dart analyze: cero errores y avisos.
  2. dart format . aplicado.
  3. dart test: todo en verde.
  4. Sin print olvidados (usa logging).
  5. Nombres nuevos coherentes con las convenciones de arriba.
La referencia definitiva es Effective Dart en dart.dev/effective-dart. Léelo entero cuando termines este curso: son 30 minutos que valen años.

Puntos clave

  • Convenciones fijas: lowerCamelCase para casi todo, UpperCamelCase para tipos.
  • final + const + null safety = menos superficie de error.
  • analyze + format + test: el ritual mínimo antes de cada entrega.

38 · Rumbo a Flutter: tu siguiente paso

Meta ~20 min

Si llegaste aquí practicando cada capítulo, ya dominas Dart. Ahora veamos cómo TODO lo aprendido encaja en Flutter — porque en Flutter no aprenderás un lenguaje nuevo, solo nuevas clases escritas en el que ya conoces.

38.1 · Cada concepto tiene su traducción

Lo que aprendisteCómo aparece en Flutter
Clases y constructores con nombreTodo widget es una clase; los parámetros son nombrados
final / constCampos de widgets finales; const Text(...) optimiza renders
Listas + map/where + spreadConstruir columnas de widgets dinámicamente
async/await y FuturesLlamadas HTTP, FutureBuilder
StreamsDatos en tiempo real, StreamBuilder, BLoC
MixinsSingleTickerProviderStateMixin y amigos
Null safetyTipos anulables en toda la API de widgets
Patterns y sealed classesEstados de UI exhaustivos sin bugs
Isolatescompute() para trabajo pesado sin jank

38.2 · Un widget real: ¿ves todo el Dart dentro?

// Un widget de Flutter NO ES MAGIA: es una clase final con constructor
// nombrado, campos tipados y un método que devuelve otro objeto.
class TarjetaUsuario extends StatelessWidget {
  final String nombre;
  final int edad;

  const TarjetaUsuario({super.key, required this.nombre, required this.edad});

  @override
  Widget build(BuildContext context) => Container(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Text(nombre, style: const TextStyle(fontSize: 18)),
            if (edad >= 18) const Icon(Icons.verified),
          ],
        ),
      );
}
  • Clase + herencia: extends StatelessWidget.
  • Constructor const con nombrados y required: capítulos 19 y 13.
  • Colección-if: capítulo 15.
  • Función flecha: capítulo 12.

38.3 · Estado y asíncronía en acción

class Contador extends StatefulWidget {
  const Contador({super.key});

  @override
  State<Contador> createState() => _EstadoContador();
}

class _EstadoContador extends State<Contador> {
  int _veces = 0;

  void _incrementar() => setState(() => _veces++);   // closure sobre this

  @override
  Widget build(BuildContext context) => FilledButton(
        onPressed: _incrementar,
        child: Text('Pulsado $_veces veces'),
      );
}

// Y para datos asíncronos, FutureBuilder pinta según el estado del Future:
FutureBuilder<List<String>>(
  future: cargarUsuarios(),                 // tu función async del capítulo 31
  builder: (context, snapshot) => switch (snapshot.connectionState) {
    ConnectionState.waiting => const CircularProgressIndicator(),
    _ when snapshot.hasError => Text('Error: ${snapshot.error}'),
    _ => Text('Usuarios: ${snapshot.data?.length ?? 0}'),
  },                                        // patterns del capítulo 30
)

38.4 · Tu ruta recomendada ahora

  1. Instala Flutter (incluye su propio Dart): sigue docs.flutter.dev/get-started/install.
  2. Ejecuta flutter doctor y resuelve lo que falte (Android Studio o Xcode).
  3. Crea tu primera app: flutter create mi_primera_app && cd mi_primera_app && flutter run.
  4. Lee el codelab oficial "Write your first Flutter app": reconocerás el 90 % del código.
  5. Reconstruye este curso en práctica: una app con lista de productos (colecciones), formulario (null safety), API real (async) y tema claro/oscuro (enums + patterns).
  6. Sigue con Effective Dart y luego los patrones de arquitectura (provider, riverpod o bloc).
Curso completado. Pasaste de instalar el SDK a manejar null safety, POO completa, patrones de Dart 3 y concurrencia con isolates. El siguiente flutter create ya no te asusta: el lenguaje es tuyo. Nos vemos compilando apps.