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.
1 · ¿Qué es Dart?
Básico ~10 minDart 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
Future, Stream, async/await integrados en el lenguaje.Dart frente a otros lenguajes
| Lenguaje | Semejanza con Dart |
|---|---|
Java / C# | Sintaxis muy familiar: clases, interfaces, genéricos. Dart añade inferencia y null safety moderno. |
Kotlin | Muy parecidos en filosofía: concisos, null-safe, con expresiones when/switch potentes. |
JavaScript / TS | Funciones de primera clase, closures y async/await idénticos; pero con tipos estáticos reales. |
Python | List 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');
}
}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 minEl 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:
- Abre PowerShell como administrador.
- Ejecuta:
- Descarga el zip del SDK desde dart.dev/get-dart/archive.
- Descomprímelo, por ejemplo en
C:\dev\dart-sdk. - Pulsa Windows, escribe «variables de entorno» y abre Editar las variables de entorno del sistema → Variables de entorno…
- En la variable
Path(de tu usuario) pulsa Editar → Nuevo y añadeC:\dev\dart-sdk\bin. - 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 dartCualquier 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/binwhich dart (Linux/macOS) o where dart (Windows).2.3 · Verifica la instalación
En cualquier sistema, abre una terminal nueva:
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
- Instala VS Code.
- Abre la pestaña de extensiones (Ctrl+Shift+X).
- Busca e instala «Dart» (editorial Dart Code). Para Flutter instalarás también «Flutter», pero aún no hace falta.
- La extensión detecta el SDK automáticamente. Aporta autocompletado, análisis en vivo, depurador, refactorizaciones y formateo.
- Opcional muy útil: en ajustes activa Editor: Format On Save.
Puntos clave
- Instala el Dart SDK con tu gestor favorito o manualmente añadiendo
binal PATH. - Verifica siempre con
dart --versiontras instalar. - VS Code + extensión Dart = experiencia completa gratis (análisis, debug, formato).
3 · Tu primer programa y el entorno
Básico ~15 min3.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:
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:
La estructura generada es el estándar profesional:
bin/: lo que se ejecuta (dart runbusca 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');
}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
| Comando | Para qué sirve |
|---|---|
dart run | Ejecuta un archivo o el proyecto actual. |
dart analyze | Revisa errores y avisos sin ejecutar nada. |
dart format . | Da formato automático al código (estilo uniforme). |
dart fix --apply | Corrige automáticamente problemas simples. |
dart pub get | Descarga las dependencias declaradas. |
dart compile exe | Crea un ejecutable nativo independiente. |
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(); usadart run archivo.dart. dart creategenera la estructura estándar: bin, lib, test y pubspec.dart analyze/format/fix/pubson 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:
| Regla | Ejemplo |
|---|---|
Cada sentencia termina en ; | print('hola'); |
Los bloques se delimitan con llaves { } | void main() { ... } |
| Distingue mayúsculas y minúsculas | edad, 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
}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(); usadart formatsiempre.
5 · Variables: var, dynamic y tipos
Básico ~15 min- Objetivos: declarar variables con tipo explícito o inferido; entender qué son
var,dynamicy 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');
}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:
precioFinalantes quepfox.
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
}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
varcuando 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 tampocovar 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.dynamicdesactiva la comprobación: úsalo con moderación.- Nombres en lowerCamelCase, claros y descriptivos.
6 · Números y booleanos con detalle
Básico ~15 minDart 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');
}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
}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
}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');
}6.4 · El tipo bool: sin medias tintas
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,doubley su supertipo comúnnum; todo son objetos./devuelve siempre double; usa~/para división entera.tryParseconvierte sin lanzar excepciones (devuelve null).boolestricto: nada de truthy/falsy al estilo JS.
7 · Cadenas de texto (String) a fondo
Básico ~20 minLas 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);
}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
}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, '-')); // '*----'
}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
}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
$variabley${expresión}. r'...'evita escapes;'''...'''para bloques multilínea.==compara contenido;trim/split/replaceAll/padLeftson tus cuchillos suizos.
8 · Constantes: final frente a const
Básico ~12 minDart ofrece dos formas de declarar valores que no cambian. Parecen gemelos, pero la diferencia es cuándo se calcula el valor:
final | const | |
|---|---|---|
| Cuándo se fija el valor | En tiempo de ejecución, una sola vez | En tiempo de compilación |
| Puede depender de datos externos | Sí (DateTime.now(), APIs…) | No, solo literales y otras constantes |
| Inmutabilidad de colecciones | La referencia es final; la lista puede cambiar | Profunda: lista completamente inmutable |
| Uso típico | Valores calculados una vez | Tablas, 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);
}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)
}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 exigefinal.
9 · Operadores y expresiones
Básico ~20 min9.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)
}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
}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);
}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());
}
}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+)
}9.6 · Precedencia (resumen práctico)
- Paréntesis
( )— gana siempre; úsalos sin miedo. - Postfijos/prefijos:
++ -- ! - - Multiplicativos:
* / ~/% - Aditivos:
+ - - Comparación relacional e igualdad:
< > <= >= == != - Lógicos:
&&antes que|| - Asignaciones y ternario, los últimos.
Puntos clave
~/divide enteros;%obtiene el resto.n++devuelve antes de incrementar;++ndespué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 min10.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');
}
}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');
}
}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));
}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');
}
}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/elsepara decisiones binarias o encadenadas.switchclá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 min11.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);
}
}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]}');
}
}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);
}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');
}
}
}11.5 · ¿Qué bucle elijo?
- Número conocido de vueltas →
forclásico. - Recorrer una colección →
for-in. - Repetir mientras algo sea cierto →
while. - Ejecutar mínimo una vez →
do-while. - Transformar/filtrar datos → ni bucle: usa
map/where(capítulo 15).
Puntos clave
for-ines el caballo de batalla diario sobre listas y mapas.breakcorta el bucle;continuesalta a la siguiente vuelta.- Cuida siempre la condición de salida: evita bucles infinitos.
12 · Funciones I: los fundamentos
Básico ~15 minUna 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
}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
}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».voidsignifica «no devuelvo nada».
13 · Funciones II: parámetros avanzados
Básico ~20 minDart tiene tres tipos de parámetros y saber combinarlos distingue al código profesional:
| Tipo | Sintaxis | Llamada |
|---|---|---|
| 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'));
}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'));
}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']);
}Regla inquebrantable: primero posicionales, luego [opcionales],
luego {nombrados}.
Puntos clave
[opcionales]mantienen el orden;{nombrados}dan legibilidad.requiredmarca 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 minEn 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');
});
}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
}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;
}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);
}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ó.
typedefdocumenta tipos de función complejos.
15 · Colecciones I: List e Iterable
Intermedio ~30 minLa 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);
}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
}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);
}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);
}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,ifyfordentro de literales son oro puro.- Los iterables son perezosos: usa
toList()para materializar.
16 · Colecciones II: Set y Map
Intermedio ~25 min16.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);
}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);
}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']);
}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);
}Puntos clave
Setgarantiza unicidad y busca en O(1).Mapaccede 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 minEl 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í
}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']);
}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
}! 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);
}17.6 · Nulabilidad anidada: ¿qué significa cada firma?
| Tipo | Lectura 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. lateaplaza la inicialización manteniendo la seguridad.!es una apuesta: si fallas, excepción en runtime.
18 · Clases y objetos: primeros pasos
Intermedio ~25 minUna 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());
}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!)
}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
}toString() en tus modelos de datos:
mejora logs, depuración y tests desde el primer día.Puntos clave
newno existe:MiClase(...)ya crea instancias.this.campoen el constructor asigna sin boilerplate.- Cada instancia tiene su propio estado independiente.
19 · Constructores a fondo
Intermedio ~30 minDart 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));
}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);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}'));
}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)
}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. factorydecide qué devolver (caches, singletons, subtipos).consten constructores = objetos canónicos y rendimiento.
20 · Miembros especiales: get/set, static y operadores
Intermedio ~30 min20.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);
}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);
}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 ==
}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/setexponen lógica con apariencia de atributo.static= miembros de la clase: constantes, utilidades, contadores.operator ==exige acompañarse dehashCode.call()convierte instancias en invocables.
21 · Herencia: extends y super
Intermedio ~30 minLa 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
}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
}
}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());
}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.Puntos clave
extendshereda;super(...)inicializa al padre.@override+ redefinición = polimorfismo.super.metodo()permite extender en vez de reemplazar.
22 · Clases abstractas e interfaces
Intermedio ~25 minUna 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();
}
}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);
}22.2 · extends frente a implements
extends | implements | |
|---|---|---|
| Herencia | Una sola superclase | Varias interfaces a la vez |
| Código heredado | Sí, reutiliza implementación | No: solo firmas; todo se reescribe |
| Uso ideal | Jerarquí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');
}Puntos clave
abstract= contrato + implementación parcial; no instanciable.- Toda clase es interfaz:
implementsobliga a reescribir todo. - Programa contra contratos (
Notificable) para desacoplar tu código.
23 · Mixins: composición flexible
Avanzado ~30 minUn 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
}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();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(); // Corro23.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();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)); // 42SingleTickerProviderStateMixin,
AutomaticKeepAliveClientMixin…
Ahora sabes exactamente qué son.Puntos clave
withmezcla varios mixins: composición sin acoplar jerarquías.- Ante conflictos, el mixin más a la derecha prevalece.
on Tiporestringe dónde puede aplicarse y habilita sus miembros.
24 · Enums potenciados
Intermedio ~20 min24.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);
}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)}');
}
}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// 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); // miauPuntos clave
- Enums = conjuntos finitos de constantes seguras.
switchsobre enums es exhaustivo: cero casos olvidados.- Los enums mejorados llevan campos, getters y métodos.
25 · Extensiones y extension types
Avanzado ~25 minUna 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));
}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());
}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);
}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
}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 typecrea alias fuertes sin coste en runtime.
26 · Metadatos y anotaciones
Avanzado ~15 minLas 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).
}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
@overridey@Deprecatedson 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 minLos 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());
}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');
}
}
}(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 Tiporestringe y habilita operaciones.- Dart reifica genéricos:
is List<int>funciona en runtime.
28 · Excepciones y manejo de errores
Avanzado ~30 minCuando 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.
| Tipo | Semántica | Ejemplos |
|---|---|---|
Exception | Fallo esperable: datos de entrada, red, formato… | FormatException, IOException |
Error | Bug del programa: no se captura, se corrige | RangeError, 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…)');
}
}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'));
}
}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
}
}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);
}
}
}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
Exceptionpara fallos esperables; deja que losErrorrompan. on Tipo catch (e, s): específico primero;catch (e)genérico después.rethrowpreserva el stack;throw elo reinicia.assertdocumenta invariantes gratis en desarrollo.
29 · Records: tuplas estructuradas (Dart 3)
Avanzado ~25 minUn 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
}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}');
}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');
}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 minDart 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));
}
}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');
}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');
}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));
}
}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');
}
}Puntos clave
- Patterns combinan comprobación + extracción en una sola línea segura.
whenañade condiciones a cualquier caso.sealedconvierte 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 minLas 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');
}¡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');
}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');
}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'));
}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';
}31.5 · Reglas de oro del async
- Toda función que use
awaitdebe marcarseasync(y devolverFuture<T>oStream<T>). awaitsolo 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
Futurehasta quien hagaawait. - Pide varias cosas independientes en paralelo con
Future.wait: los usuarios odian las pantallas lentas. - Nunca uses
awaitsin 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/finallyfuncionan idéntico en código async.Future.waitparaleliza;timeoutevita esperas eternas.
32 · Streams y generadores
Avanzado ~35 minSi 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
}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');
}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
}
}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());
}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();
}StreamController.broadcast() varios oyentes reciben lo mismo —
así funcionan los «providers» de estado en Flutter.Puntos clave
Stream= muchos valores futuros;Future= uno.async*/yieldcrea streams;sync*/yieldcrea iterables perezosos.await for,listen()y operadores comowhere/map/takeprocesan el flujo.
33 · Event loop e isolates
Avanzado ~30 minCada 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
- Cola de microtasks: tareas urgentes internas (
scheduleMicrotask, continuaciones de async). Se vacía SIEMPRE antes. - Cola de eventos: timers, I/O, toques del usuario, mensajes entre isolates.
- 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');
}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');
}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();
}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.runmueve el trabajo CPU-pesado fuera del hilo de UI.
34 · Librerías, paquetes y pub.dev
Avanzado ~25 min34.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));
}_ 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.0Có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
34.4 · Organizar tu propio código en librerías
// 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 estable34.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_typesPuntos clave
- Tres orígenes:
dart:,package:, rutas relativas. ^expresa compatibilidad; el lock file fija versiones exactas.lib/srcprivado + archivo barril = API pública ordenada.
35 · Utilidades esenciales: DateTime, RegExp, JSON e I/O
Avanzado ~30 min35.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());
}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);
}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);
}
}Puntos clave
DateTimees inmutable: add/difference devuelven nuevos valores.jsonDecode/Encode+ métodosdesdeJson/aJsonmanuales bastan para empezar.File/stdin/stdout/Directorycubren apps de consola completas.
36 · Pruebas unitarias con package:test
Avanzado ~30 minEl código sin pruebas es código heredado desde el día uno. Dart trae su framework oficial:
package: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
});
}36.2 · Matchers más útiles
| Matcher | Comprueba 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/contains | sobre 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/tearDownaíslan cada prueba entre sí.- Los matchers cubren sync, Futures y Streams por igual.
- Ejecuta
dart testtras cada cambio: feedback en segundos.
37 · Buenas prácticas y Effective Dart
Avanzado ~25 min37.1 · Convenciones de nombres oficiales
| Elemento | Estilo | Ejemplo |
|---|---|---|
| Variables, funciones, parámetros | lowerCamelCase | totalConIva() |
| Clases, enums, typedefs, extensiones | UpperCamelCase | CarritoCompra |
| Constantes y enums values | lowerCamelCase (sí, también) | const maxIntentos = 3; |
| Archivos y paquetes | snake_case | carrito_compra.dart |
| Privado | prefijo _ | _cacheInterno |
| Nombrados de librería | dos puntos | library mi.app.core; |
37.2 · Los hábitos que separan al profesional
- Inmutabilidad por defecto: campos
final, constructoresconst. Menos bugs, mejor rendimiento en Flutter. - Nada de
dynamicinnecesario: 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 formatantes 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
}37.4 · Checklist antes de subir código
dart analyze: cero errores y avisos.dart format .aplicado.dart test: todo en verde.- Sin
printolvidados (usa logging). - Nombres nuevos coherentes con las convenciones de arriba.
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 minSi 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 aprendiste | Cómo aparece en Flutter |
|---|---|
| Clases y constructores con nombre | Todo widget es una clase; los parámetros son nombrados |
| final / const | Campos de widgets finales; const Text(...) optimiza renders |
| Listas + map/where + spread | Construir columnas de widgets dinámicamente |
| async/await y Futures | Llamadas HTTP, FutureBuilder |
| Streams | Datos en tiempo real, StreamBuilder, BLoC |
| Mixins | SingleTickerProviderStateMixin y amigos |
| Null safety | Tipos anulables en toda la API de widgets |
| Patterns y sealed classes | Estados de UI exhaustivos sin bugs |
| Isolates | compute() 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
- Instala Flutter (incluye su propio Dart): sigue docs.flutter.dev/get-started/install.
- Ejecuta
flutter doctory resuelve lo que falte (Android Studio o Xcode). - Crea tu primera app:
flutter create mi_primera_app && cd mi_primera_app && flutter run. - Lee el codelab oficial "Write your first Flutter app": reconocerás el 90 % del código.
- 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).
- Sigue con Effective Dart y luego los patrones de arquitectura (provider, riverpod o bloc).
flutter create ya no te asusta: el lenguaje
es tuyo. Nos vemos compilando apps.