DevTools

Cheatsheet Dart

Linguagem da Google, base do Flutter, para apps mobile, web e servidor

Volver a los lenguajes
Dart
122 tarjetas encontradas
Categorías:
Versiones:

Básico e Variáveis


12 cards
Función main
void main() {
  print("¡Hola, Dart!");
}

// Con argumentos (CLI)
void main(List<String> args) {
  print(args);
}

main() es el punto de entrada de todo programa Dart. En scripts CLI puede recibir args como List<String>.

Interpolación de strings
String name = "Ana";
int age = 30;

print("Hola, $name");
print("Total: ${price * 2}");
print("Mayúsculas: ${name.toUpperCase()}");

Usa $variable para valores simples y ${expresion} para expresiones o llamadas a método dentro de la string.

Operadores de comparación
5 == 5;   // true  igual
5 != 3;   // true  distinto
5 > 3;    // true  mayor
5 <= 5;   // true  menor o igual

// idéntico (mismo valor y tipo)
1 == 1.0;   // true (== compara valor)

== compara valores. Los operadores >, <, >= y <= comparan orden. != es la negación de igualdad.

Variables
var name = "Ana";      // tipo inferido (String)
String city = "Porto";
int age = 30;
double price = 9.99;
bool active = true;

Usa var para que Dart infiera el tipo, o declara el tipo explícitamente. Una variable tipada no acepta valores de otro tipo.

Strings multilínea y raw
var text = """
Primera línea
Segunda línea
""";

var raw = r"Sin \n escape aquí";
var path = r"C:\carpeta\archivo";

Tres comillas """ crean strings multilínea. El prefijo r crea una string raw, ignorando escapes como \n.

Operadores lógicos
true && false;  // false (Y)
true || false;  // true  (O)
!true;          // false (negación)

// cortocircuito
if (a != null && a.length > 0) {
  // solo evalúa a.length si a no es null
}

&& (Y) y || (O) son de cortocircuito: el segundo operando solo se evalúa si es necesario. ! niega un bool.

final y const
final name = "Ana";  // una asignación (runtime)
const pi = 3.14;     // constante de compilación

final list = [1, 2];
list.add(3);         // OK: el contenido cambia
// list = [];        // ERROR: reasignación

final permite una única asignación en runtime. const es inmutable desde la compilación y exige un valor conocido estáticamente.

Comentarios
// comentario de una línea

/* comentario
   de varias líneas */

/// Documentación (genera docs con dartdoc)
/// [name] es el parámetro.
void func() {}

Usa // para una línea, /* */ para un bloque y /// para documentación. Los comentarios doc generan API docs con la herramienta dartdoc.

Cascade (..)
var list = []
  ..add(1)
  ..add(2)
  ..add(3)
  ..sort();

var sb = StringBuffer()
  ..write("Hola ")
  ..write("Dart");

El operador .. encadena varias operaciones sobre el mismo objeto sin repetir su nombre. Ideal para configurar objetos de forma compacta.

dynamic y Object
dynamic x = "texto";
x = 42;            // OK: cualquier tipo
x.metodoInexistente(); // compila, falla en runtime

Object o = 10;
// o.toUpperCase(); // ERROR: Object no lo tiene

dynamic desactiva la verificación de tipos (riesgo en runtime). Object es la raíz de todos los tipos, pero mantiene la seguridad estática.

Operadores aritméticos
int a = 10, b = 3;
a + b;   // 13  suma
a - b;   // 7   resta
a * b;   // 30  multiplicación
a / b;   // 3.33 división (double)
a ~/ b;  // 3   división entera
a % b;   // 1   resto

La división / devuelve siempre double. Usa ~/ para el cociente entero y % para el resto de la división.

Incremento y asignación
int x = 5;
x++;     // 6 (post-incremento)
++x;     // 7 (pre-incremento)
x += 3;  // x = x + 3
x *= 2;  // x = x * 2
x ??= 0; // asigna solo si es null

Los operadores compuestos +=, -=, *= combinan operación y asignación. ??= asigna solo si la variable es null.

Tipos de Dados


12 cards
int y double
int x = 10;        // entero
double y = 3.14;   // decimal
num z = 5;         // int o double
num w = 2.5;       // también válido

int hex = 0xFF;    // hexadecimal
int bin = 0b1010;  // binario (10)

int es entero y double decimal. num es la superclase de ambos. Soporta notación hexadecimal 0x y binaria 0b.

String: split y replace
String s = "a,b,c,d";
s.split(",");            // ["a","b","c","d"]
s.replaceAll(",", "-");  // "a-b-c-d"
s.replaceFirst(",", ";"); // "a;b,c,d"
s.substring(0, 3);       // "a,b"
s.indexOf(",");          // 1

split() devuelve una List<String>. replaceAll() reemplaza todas las ocurrencias y substring() extrae parte de la string.

List (lista)
List<int> nums = [1, 2, 3];
nums.add(4);
nums[0];          // 1
nums.length;      // 4
nums.first;       // 1
nums.last;        // 4
List empty = [];

La List es ordenada y dinámica. El tipo genérico List<int> restringe los elementos. first y last acceden a los extremos.

Métodos numéricos
double d = 9.876;
d.round();             // 10
d.floor();             // 9
d.ceil();              // 10
d.toStringAsFixed(2);  // "9.88"
d.abs();               // valor absoluto
d.truncate();          // 9 (corta decimales)

round(), floor() y ceil() redondean de formas distintas. toStringAsFixed() formatea los decimales como string.

Conversión de tipos
int n = int.parse("42");
double d = double.parse("3.14");
String s = n.toString();
String f = 3.14.toStringAsFixed(1); // "3.1"

int? error = int.tryParse("abc"); // null

int.parse() y double.parse() convierten strings en números y lanzan una excepción si son inválidas. tryParse() devuelve null en vez de fallar.

Map (mapa)
Map<String, int> ages = {
  "Ana": 30,
  "Ray": 25,
};
ages["Ana"];        // 30
ages["Ze"] = 40;    // añade
ages.keys;          // (Ana, Ray, Ze)
ages.values;        // (30, 25, 40)

El Map guarda pares clave-valor. Acceder a una clave inexistente devuelve null. keys y values devuelven iterables.

Propiedades numéricas
int n = 10;
n.isEven;      // true (par)
n.isOdd;       // false (impar)
n.isNegative;  // false

double d = 3.14;
d.isFinite;    // true
d.isNaN;       // false

Las propiedades isEven/isOdd verifican paridad. isFinite y isNaN son útiles para validar resultados de cálculos.

Booleanos
bool active = true;
bool empty = list.isEmpty;
bool older = age >= 18;

// Dart exige un bool real (sin truthy/falsy)
if (active) { }
// if (1) { }  // ERROR: int no es bool

Dart es estricto: las condiciones exigen un bool real, al contrario que JavaScript. No hay valores "truthy" — if (1) es error de compilación.

Set (conjunto)
Set<int> unique = {1, 2, 2, 3};
// {1, 2, 3} — duplicados eliminados

unique.add(3);        // ignorado
unique.contains(1);   // true
unique.length;        // 3

El Set guarda valores únicos sin orden garantizado. Añadir un duplicado no tiene efecto. Útil para eliminar repeticiones de una lista.

Métodos de String
String s = "  Hola Mundo  ";
s.trim();             // "Hola Mundo"
s.toLowerCase();      // minúsculas
s.toUpperCase();      // mayúsculas
s.length;             // longitud
s.contains("Mundo");  // true
s.startsWith("Hola");  // false (espacios)

Las String son inmutables: cada método devuelve una nueva string. trim() quita espacios en los extremos y contains() verifica substrings.

Runes y caracteres
String s = "Hola";
s.codeUnits;        // códigos UTF-16
s.runes;            // code points Unicode

var emoji = "\u{1F600}"; // 😀
print(emoji);

runes da acceso a los code points Unicode (importante para emojis). \u{...} inserta un carácter Unicode por su código.

Verificación de tipo (is / as)
Object x = "texto";
if (x is String) {
  print(x.length); // x se promueve a String
}

num n = 5;
int i = n as int;  // cast explícito

is verifica el tipo y promueve la variable dentro del bloque. as hace un cast explícito y lanza una excepción si el tipo no coincide.

Controlo de Fluxo


12 cards
if / else
if (age >= 18) {
  print("Adulto");
} else if (age > 12) {
  print("Joven");
} else {
  print("Niño");
}

La estructura if/else if/else evalúa condiciones en orden y ejecuta el primer bloque verdadero. Los paréntesis son obligatorios.

for clásico
for (int i = 0; i < 10; i++) {
  print(i);
}

for (int i = 10; i > 0; i--) {
  print(i); // cuenta regresiva
}

El ciclo for tiene inicialización, condición e incremento. Es ideal cuando necesitas el índice o controlar el contador manualmente.

break y continue
for (int i = 0; i < 10; i++) {
  if (i == 3) continue; // salta el 3
  if (i == 7) break;    // para en el 7
  print(i);
}
// imprime 0,1,2,4,5,6

continue salta a la siguiente iteración y break termina el ciclo inmediatamente. Ambos funcionan en for, while y switch.

Operador ternario
int age = 20;
String r = age >= 18 ? "Adulto" : "Menor";

// Encadenado (usar con moderación)
String level = grade >= 18 ? "A"
             : grade >= 14 ? "B"
             : "C";

El ternario condición ? valorSiTrue : valorSiFalse es un if-else compacto en una expresión. Evita encadenar muchos para mantener la legibilidad.

for-in
var fruits = ["manzana", "pera", "uva"];

for (var fruit in fruits) {
  print(fruit);
}

for (var key in map.keys) {
  print("$key: ${map[key]}");
}

El for-in recorre cada elemento de una colección sin índice. Es la forma más legible de iterar sobre una List, Set o Map.keys.

Labels en ciclos
outer:
for (int i = 0; i < 3; i++) {
  for (int j = 0; j < 3; j++) {
    if (i == 1 && j == 1) {
      break outer; // sale del ciclo externo
    }
  }
}

Un label (un nombre seguido de :) permite que break o continue actúen sobre un ciclo externo en loops anidados.

switch / case
switch (day) {
  case 1:
    print("Lunes");
    break;
  case 2:
    print("Martes");
    break;
  default:
    print("Otro");
}

El switch compara un valor con varios case. Cada caso necesita un break (o return) para no continuar al siguiente.

while
int x = 5;
while (x > 0) {
  print(x);
  x--;
}
// x termina en 0

El while repite mientras la condición sea verdadera. Verifica la condición antes de cada iteración, pudiendo no ejecutar nunca el bloque.

Pattern matching (Dart 3)
var (year, month, day) = (2024, 7, 24);
print("$day/$month/$year");

// Desestructurar lista
var [first, second, ...rest] = [1, 2, 3, 4];

// Guard en switch
var r = switch (value) {
  int n when n > 0 => "positivo",
  _ => "otro",
};

Dart 3 introduce patterns: desestructuración de records y listas, y when para guardas condicionales en el switch.

Switch expression (Dart 3)
String name = switch (day) {
  1 => "Lunes",
  2 => "Martes",
  3 => "Miércoles",
  _ => "Otro",
};

En Dart 3 el switch puede ser una expresión que devuelve un valor. _ es el caso por defecto (wildcard) y reemplaza a default.

do-while
int x = 0;
do {
  print(x);
  x++;
} while (x < 5);
// se ejecuta al menos una vez

El do-while ejecuta el bloque antes de verificar la condición, garantizando al menos una ejecución. Útil para menús y validaciones.

if-case (Dart 3)
var json = {"type": "circle", "radius": 5};

if (json case {"type": "circle", "radius": var r}) {
  print("Círculo con radio $r");
}

// Patrones en declaraciones
var [a, b] = [1, 2];

El if-case combina una condición con pattern matching, extrayendo valores de mapas y objetos de forma declarativa y segura.

Funções


12 cards
Función simple
int add(int a, int b) {
  return a + b;
}

void greeting(String name) {
  print("Hola, $name");
}

print(add(2, 3)); // 5

Declara el tipo de retorno antes del nombre. void indica que no devuelve valor. Los parámetros tipados aumentan la seguridad del código.

Funciones anónimas (lambda)
var list = [1, 2, 3];
list.forEach((n) => print(n));

var double = (int x) => x * 2;
print(double(5)); // 10

var sum = (int a, int b) {
  return a + b;
};

Las funciones sin nombre pueden asignarse a variables o pasarse como argumentos. Son la base de los callbacks y de métodos como map() y where().

Parámetro required
class User {
  final String name;
  final String email;

  User({required this.name,
      required this.email});
}

var u = User(name: "Ana", email: "a@b.com");

required obliga a pasar el argumento nombrado, evitando null inesperados. Esencial en constructores y configuraciones.

Función arrow (=>)
int double(int x) => x * 2;

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

// Equivalente a:
int triple(int x) {
  return x * 3;
}

La sintaxis => expresion es un atajo para funciones de una sola expresión que devuelven ese valor. Reemplaza a { return ...; }.

Función como parámetro
int apply(int x, int Function(int) f) {
  return f(x);
}

print(apply(5, (n) => n * 2)); // 10
print(apply(5, (n) => n + 1)); // 6

Las funciones son objetos de primera clase en Dart. El tipo int Function(int) describe una función que recibe y devuelve un int.

Valores por omisión
void connect({
  String host = "localhost",
  int port = 3306,
  bool ssl = false,
}) {
  print("$host:$port ssl=$ssl");
}

connect(port: 5432);

Los valores por omisión se aplican cuando el argumento no se proporciona. En parámetros nombrados deben ser constantes (const).

Parámetros nombrados
void call({required String number,
    bool speaker = false}) {
  print("$number speaker=$speaker");
}

call(number: "123", speaker: true);
call(number: "456"); // speaker = false

Los parámetros entre { } se pasan por el nombre, en cualquier orden. required los hace obligatorios y puede haber valores por omisión.

Función como retorno (closure)
Function multiplier(int factor) {
  return (int x) => x * factor;
}

var doubler = multiplier(2);
var tripler = multiplier(3);
print(doubler(5));  // 10
print(tripler(5));  // 15

Una función puede devolver otra función, creando un closure que captura variables del ámbito externo (aquí el factor).

Ámbito de variables
String global = "fuera";

void test() {
  String local = "dentro";
  print(global); // accesible
  print(local);
}
// print(local); // ERROR: fuera de ámbito

Las variables declaradas dentro de una función son locales e inaccesibles fuera de ella. Dart tiene ámbito léxico: los bloques { } crean nuevos ámbitos.

Parámetros posicionales opcionales
void greet(String name,
    [String title = "Sr."]) {
  print("Hola, $title $name");
}

greet("Ana");         // Sr. Ana
greet("Ana", "Dra.");  // Dra. Ana

Los parámetros entre [ ] son posicionales y opcionales. Deben ir después de los obligatorios y pueden tener un valor por omisión.

Funciones genéricas
T first<T>(List<T> list) {
  return list.first;
}

print(first<int>([1, 2, 3]));     // 1
print(first<String>(["a", "b"])); // "a"

<T> hace la función genérica, funcionando con cualquier tipo. El tipo puede inferirse o especificarse explícitamente en la llamada.

Tear-off y referencia de método
var list = [1, 2, 3];

// Tear-off: referencia al método
list.forEach(print);

// Equivalente a:
list.forEach((n) => print(n));

var double = [1, 2].map((n) => n * 2);

Un tear-off es pasar un método directamente (ej.: print) en vez de envolverlo en una lambda. Más limpio cuando la firma coincide.

Coleções


12 cards
Operaciones de List
var list = [1, 2, 3];
list.add(4);         // añade al final
list.remove(1);      // elimina el valor 1
list.removeAt(0);    // elimina por índice
list.insert(0, 9);   // inserta en la posición
list.contains(2);    // true
list.indexOf(3);     // posición

La List es dinámica: add() inserta al final, remove() elimina por valor, insert() coloca en una posición específica.

sort() y reversed
var list = [3, 1, 4, 1, 5];
list.sort();              // [1, 1, 3, 4, 5]
list.sort((a, b) => b.compareTo(a)); // desc

var names = ["ana", "rui"];
names.reversed.toList();   // ["rui", "ana"]

sort() ordena la lista en el lugar (muta la original). Pasa un comparador para orden personalizado. reversed devuelve el orden inverso.

forEach e indexed
var fruits = ["manzana", "pera"];
fruits.forEach((f) => print(f));

// Con índice (Dart 3)
for (var (i, f) in fruits.indexed) {
  print("$i: $f");
}

forEach() ejecuta una función para cada elemento. .indexed proporciona pares (índice, valor) para iterar con la posición.

map() y where()
var list = [1, 2, 3, 4, 5];

var double = list.map((e) => e * 2).toList();
// [2, 4, 6, 8, 10]

var evens = list.where((e) => e % 2 == 0).toList();
// [2, 4]

map() transforma cada elemento y where() filtra. Ambos devuelven un Iterable; usa toList() para materializarlo.

Spread operator (...)
var a = [1, 2];
var b = [0, ...a, 3]; // [0, 1, 2, 3]

List<int>? nullable;
var c = [...?nullable, 1]; // spread null-safe

var map = {...otherMap, "x": 1};

... expande los elementos de una colección dentro de otra. ...? evita un error si la colección es null.

Map: transformaciones
var map = {"a": 1, "b": 2};

var doubled = map.map((k, v) => MapEntry(k, v * 2));
// {"a": 2, "b": 4}

map.containsKey("a");  // true
map.remove("b");       // elimina la clave

El map() de un Map devuelve MapEntry para transformar claves y valores. containsKey() verifica la existencia de una clave.

any() y every()
var list = [1, 2, 3, 4];

list.any((e) => e > 3);   // true (alguno)
list.every((e) => e > 0); // true (todos)
list.contains(2);         // true
list.isEmpty;             // false

any() verifica si al menos un elemento cumple la condición y every() si todos la cumplen.

Collection-if y for
bool active = true;
var items = [
  "base",
  if (active) "extra",
  for (var i in [1, 2]) "item$i",
];
// ["base", "extra", "item1", "item2"]

Dentro de literales de colección puedes usar if y for para construir listas dinámicamente, sin lógica externa.

Set: operaciones de conjunto
var a = {1, 2, 3};
var b = {2, 3, 4};

a.union(b);          // {1,2,3,4}
a.intersection(b);   // {2,3}
a.difference(b);     // {1}
a.containsAll([1,2]); // true

El Set ofrece operaciones matemáticas: union() (unión), intersection() (intersección) y difference() (diferencia).

fold() y reduce()
var list = [1, 2, 3, 4];

int sum = list.fold(0, (acc, e) => acc + e);
// 10

int product = list.reduce((acc, e) => acc * e);
// 24

fold() acumula un valor a partir de un valor inicial. reduce() hace lo mismo pero usa el primer elemento como punto de partida.

List.generate()
var squares = List.generate(5, (i) => i * i);
// [0, 1, 4, 9, 16]

var evens = List.generate(10, (i) => i * 2);
// [0, 2, 4, ..., 18]

List.generate() crea una lista a partir de una función que recibe el índice. Ideal para generar secuencias o datos de prueba.

Colecciones inmutables
var fixed = const [1, 2, 3];
// fixed.add(4); // ERROR: inmutable

var immutable = List.unmodifiable([1, 2, 3]);
// immutable[0] = 9; // ERROR en runtime

var readOnly = [1, 2, 3] as List<int>;

Una lista const es inmutable en compilación. List.unmodifiable() crea una vista de solo lectura que lanza un error al intentar modificarla.

Classes e OOP


16 cards
Clase básica
class Person {
  String name;
  int age;

  Person(this.name, this.age);
}

var p = Person("Ana", 30);
print(p.name); // Ana

El constructor Person(this.name, this.age) asigna automáticamente los argumentos a los campos. Es la forma idiomática y compacta de Dart.

Clase abstracta
abstract class Shape {
  double area();
}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);

  @override
  double area() => 3.14 * radius * radius;
}

Una clase abstract no puede instanciarse y define un contrato. Las subclases están obligadas a implementar los métodos abstractos.

Enum avanzado
enum Planet {
  earth(9.8),
  mars(3.7),
  moon(1.6);

  final double gravity;
  const Planet(this.gravity);
}

print(Planet.earth.gravity); // 9.8

Los enums en Dart pueden tener campos, constructores y métodos. Cada valor pasa argumentos al constructor const, permitiendo datos asociados.

Miembros estáticos
class MyMath {
  static const double pi = 3.14159;

  static double circleArea(double r) {
    return pi * r * r;
  }
}

print(MyMath.pi);
print(MyMath.circleArea(2));

Los miembros static pertenecen a la clase y no a la instancia. Se acceden por el nombre de la clase, sin crear un objeto. Útil para utilidades y constantes.

Constructor nombrado
class Point {
  final int x, y;

  const Point(this.x, this.y);
  Point.origin() : this(0, 0);
  Point.axisX(this.x) : y = 0;
}

var p = Point.origin();

Los constructores nombrados como Point.origin() ofrecen formas alternativas de crear objetos. El : this(...) delega en otro constructor.

Interfaz (implements)
class Point {
  int x = 0, y = 0;
}

class Point3D implements Point {
  int x = 0, y = 0, z = 0;
}

void printPoint(Point p) => print(p.x);

En Dart cualquier clase puede ser una interfaz con implements. La clase debe reimplementar todos los miembros de la interfaz.

Extension methods
extension StringExtra on String {
  String get reversed =>
      split("").reversed.join();
  bool get isNumber =>
      int.tryParse(this) != null;
}

print("abc".reversed); // "cba"
print("123".isNumber);  // true

Una extension añade métodos a tipos existentes sin modificarlos. Se acceden como si fueran métodos nativos del tipo.

Factory constructor
class Logger {
  static final Logger _inst = Logger._();
  Logger._();

  factory Logger() => _inst;

  void log(String msg) => print(msg);
}

var a = Logger();
var b = Logger(); // misma instancia

Un constructor factory controla la creación del objeto, pudiendo devolver instancias existentes (singleton) o de subclases.

Getters y setters
class Rectangle {
  double width = 0, height = 0;

  double get area => width * height;

  set area(double v) {
    width = height = v;
  }
}

var r = Rectangle();
r.area = 5;    // setter
print(r.area); // getter

get crea una propiedad computada de solo lectura y set permite la asignación con lógica. Se acceden como campos normales, sin paréntesis.

Mixin (with)
mixin Swimmer {
  void swim() => print("nadando");
}

mixin Flyer {
  void fly() => print("volando");
}

class Duck extends Animal with Swimmer, Flyer {}

Duck().swim();
Duck().fly();

Un mixin añade comportamiento a varias clases sin herencia clásica. Se aplica con with y puede combinar varios mixins.

Clases genéricas
class Box<T> {
  T value;
  Box(this.value);
}

var c = Box<int>(42);
var s = Box<String>("hola");
print(c.value); // 42

<T> hace la clase reutilizable para cualquier tipo. El tipo se fija en la instanciación, garantizando seguridad de tipos.

Clases inmutables
class Point {
  final int x;
  final int y;
  const Point(this.x, this.y);
}

const a = Point(1, 2);
const b = Point(1, 2);
print(identical(a, b)); // true (canonical)

Los campos final + un constructor const crean objetos inmutables. Las instancias const idénticas son canónicas (compartidas en memoria).

Herencia (extends)
class Animal {
  void speak() => print("...");
}

class Dog extends Animal {
  @override
  void speak() => print("Guau guau");
}

Dog().speak(); // Guau guau

extends hereda campos y métodos de la superclase. @override indica que estás reescribiendo un método heredado.

Enum simple
enum Status { active, inactive, pending }

Status s = Status.active;
print(s.name);    // "active"
print(s.index);   // 0

if (s == Status.active) {
  print("active!");
}

El enum define un conjunto fijo de valores. .name devuelve el nombre como string y .index la posición (empieza en 0).

super parameters (Dart 2.17+)
class Animal {
  final String name;
  Animal(this.name);
}

class Dog extends Animal {
  final String breed;
  Dog(super.name, this.breed);
}

var c = Dog("Rex", "Labrador");

super.name en el constructor reenvía automáticamente el argumento a la superclase, evitando escribir : super(name).

Operador == y hashCode
class Point {
  final int x, y;
  const Point(this.x, this.y);

  @override
  bool operator ==(Object o) =>
      o is Point && o.x == x && o.y == y;

  @override
  int get hashCode => Object.hash(x, y);
}

Para comparar objetos por valor, sobrescribe operator == y hashCode. Object.hash() genera un hash consistente de los campos.

Assíncrono


12 cards
async / await
Future<String> fetch() async {
  var r = await http.get(url);
  return r.body;
}

void main() async {
  var data = await fetch();
  print(data);
}

async marca una función asíncrona y await pausa la ejecución hasta que el Future complete, manteniendo el código legible.

Future.wait (paralelo)
var results = await Future.wait([
  fetchA(),
  fetchB(),
  fetchC(),
]);
print(results); // [a, b, c]

El Future.wait() ejecuta varios Future en paralelo y espera todos. Más rápido que el await secuencial cuando son independientes.

async* y yield*
Stream<int> upTo(int n) async* {
  for (var i = 0; i <= n; i++) {
    yield i;
  }
}

Stream<int> doubled() async* {
  yield* upTo(3).map((e) => e * 2);
}

El async* devuelve un Stream y el yield emite valores. El yield* reemite todos los valores de otra stream.

Future
Future<String> task() {
  return Future.delayed(
    Duration(seconds: 2),
    () => "ready",
  );
}

task().then((v) => print(v));

Un Future<T> representa un valor disponible en el futuro. El .then() registra un callback ejecutado cuando el resultado esté listo.

StreamController
final controller = StreamController<int>();

controller.stream.listen((n) => print(n));

controller.add(1);
controller.add(2);
await controller.close();

El StreamController permite emitir valores manualmente a una stream vía .add(). El .listen() se suscribe a los eventos.

sync* (generator)
Iterable<int> fib(int n) sync* {
  int a = 0, b = 1;
  for (var i = 0; i < n; i++) {
    yield a;
    [a, b] = [b, a + b];
  }
}

print(fib(6).toList()); // [0,1,1,2,3,5]

El sync* crea un generador síncrono que devuelve un Iterable. Los valores se producen de forma perezosa (lazy) al iterar.

try / catch async
try {
  var data = await fetch();
  print(data);
} catch (e) {
  print("Error: $e");
} finally {
  print("finalizado");
}

Los errores en código await se capturan con un try/catch normal. El finally se ejecuta siempre, con o sin error.

listen y cancelación
var sub = stream.listen(
  (data) => print(data),
  onError: (e) => print("Error: $e"),
  onDone: () => print("fin"),
);

// Más tarde:
await sub.cancel();

El listen() se suscribe a una stream con callbacks de datos, error y fin. Guarda la StreamSubscription para cancelar con .cancel().

Timeout
try {
  var r = await fetch()
      .timeout(Duration(seconds: 5));
} on TimeoutException {
  print("Tardó demasiado");
}

El timeout() lanza TimeoutException si el Future no completa a tiempo. Esencial para peticiones de red robustas.

Stream
Stream<int> count() async* {
  for (var i = 0; i < 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

await for (var n in count()) {
  print(n);
}

Un Stream emite varios valores a lo largo del tiempo. El async* con yield produce los valores y el await for los consume.

then / catchError
fetch()
  .then((v) => print(v))
  .catchError((e) {
    print("Falló: $e");
    return "fallback";
  })
  .whenComplete(() => print("fin"));

El then() encadena el éxito, catchError() trata errores (pudiendo devolver un fallback) y whenComplete() se ejecuta siempre al final.

Isolates (paralelismo)
import 'dart:isolate';

int heavyCalc(int n) {
  var sum = 0;
  for (var i = 0; i < n; i++) sum += i;
  return sum;
}

// Corre en otro isolate (thread)
var result = await Isolate.run(
  () => heavyCalc(1000000),
);

Dart es single-thread por isolate. Isolate.run() ejecuta trabajo pesado en otro isolate, evitando bloquear la UI. Cada isolate tiene su propia memoria.

Null Safety


10 cards
Tipos nullable (?)
String? name;    // acepta null
name = null;     // OK

String s = "x";
// s = null;     // ERROR: no-nullable

int? number;     // nullable
int age = 0;     // nunca null

Por defecto los tipos no aceptan null. El sufijo ? hace el tipo nullable, permitiendo asignar null con seguridad.

required
class User {
  final String email;
  final String? phone;

  User({required this.email, this.phone});
}

var u = User(email: "a@b.com");
// User(); // ERROR: email obligatorio

El required hace obligatorio un parámetro nombrado, evitando null inesperados. Los parámetros sin él deben ser nullable o tener default.

Null-aware en cascade (..?)
StringBuffer? sb = getBuffer();

sb?..write("Hola")
   ..write(" Dart");
// solo se ejecuta si sb != null

list?..add(1)..sort();

El ?.. combina null-aware con cascade: la cadena solo se ejecuta si el objeto no es null, evitando excepciones en configuraciones opcionales.

Operador ?? (fallback)
String? input;
var name = input ?? "Anónimo";
// "Anónimo" si input es null

int? attempts;
attempts ??= 0; // asigna solo si es null

El ?? devuelve el operando izquierdo si no es null, si no el derecho. El ??= asigna un valor solo si la variable es null.

late (inicialización tardía)
class Service {
  late final ApiClient api;

  Service() {
    api = ApiClient(); // después del constructor
  }
}

late String config = loadConfig();

El late permite declarar una variable non-null inicializada más tarde. Dart solo verifica la asignación en el primer acceso a la variable.

Retorno nullable
int? findIndex(List l, Object v) {
  int i = l.indexOf(v);
  return i >= 0 ? i : null;
}

var idx = findIndex([1, 2], 2);
if (idx != null) print("posición $idx");

Las funciones que pueden fallar devuelven un tipo nullable (int?). El llamador verifica con != null antes de usar el resultado.

Operador ?. (acceso seguro)
String? name;
int? len = name?.length; // null, sin error

// En cadena
var city = user?.address?.city;

// Con método
list?.add(1); // solo llama si list != null

El ?. interrumpe la cadena y devuelve null en vez de lanzar una excepción si el objeto es null. Ideal para accesos encadenados.

Promoción de tipo (flow analysis)
String? name = getName();

if (name != null) {
  // aquí name se promueve a String
  print(name.length); // sin !
}

// También con is
Object x = "texto";
if (x is String) print(x.length);

Tras verificar != null o is, Dart promueve la variable al tipo non-null dentro del bloque, prescindiendo del operador !.

Operador ! (forzar non-null)
String? name = "Ana";
int len = name!.length; // afirma non-null

// CUIDADO: lanza error si es null
String? nullValue;
// nullValue!.length; // Null check operator error

El ! afirma al compilador que el valor no es null. Si está mal, lanza error en runtime. Úsalo solo cuando estés seguro.

Colecciones nullable
List<int>? list;
list?.add(1);            // seguro
var l = list ?? [];      // fallback

Map<String, int?> map = {
  "a": 1,
  "b": null, // valor nullable
};

Una colección puede ser nullable (List?) o tener elementos nullable (List<int?>). Usa ?. y ?? según el caso.

Genéricos, Records e Patterns


14 cards
Genéricos en clases
class Stack<T> {
  final List<T> _items = [];

  void push(T item) => _items.add(item);
  T pop() => _items.removeLast();
  bool get empty => _items.isEmpty;
}

var p = Stack<int>();
p.push(1);

Los genéricos (<T>) crean clases reutilizables con seguridad de tipos. El tipo se fija en la instanciación, evitando casts en runtime.

Desestructuración (destructuring)
var list = [1, 2, 3, 4];
var [a, b, ...rest] = list;
// a=1, b=2, rest=[3,4]

var map = {"x": 10, "y": 20};
var {"x": x, "y": y} = map;

Dart 3 permite desestructurar listas y mapas en variables individuales. El ...rest captura los elementos restantes en una nueva lista.

Parámetros de tipo múltiples
Map<K, V> invert<K, V>(Map<V, K> map) {
  return map.map((k, v) => MapEntry(v, k));
}

var original = {1: "uno", 2: "dos"};
var inverted = invert(original);
// {"uno": 1, "dos": 2}

Las funciones pueden tener varios parámetros de tipo (<K, V>). El compilador los infiere a partir de los argumentos pasados.

Bibliotecas e imports
import 'dart:math';
import 'package:http/http.dart' as http;
import 'utils.dart' show format;
import 'other.dart' hide internal;

export 'model.dart';

El import trae bibliotecas; as crea un prefijo, show/hide filtran símbolos. export reexporta desde otra biblioteca.

Restricciones genéricas (bounds)
T greater<T extends Comparable<T>>(T a, T b) {
  return a.compareTo(b) > 0 ? a : b;
}

print(greater(5, 10));       // 10
print(greater("a", "z"));    // "z"

T extends Comparable<T> restringe el genérico a tipos comparables. El compilador garantiza que compareTo() existe.

Switch con patterns
String describe(Object value) => switch (value) {
  int n => "entero $n",
  String s => "texto $s",
  [1, 2] => "lista [1,2]",
  { "name": var n } => "mapa name=$n",
  _ => "otro",
};

El switch con patterns hace matching por tipo, valor y estructura. Extrae valores directamente a variables (ej.: int n, var n).

Object vs dynamic vs void
Object a = 1;     // cualquier tipo, seguro
dynamic b = 1;    // sin verificación (riesgo)
void c = func();  // no usar el valor

// a.methodX(); // ERROR en compilación
b.methodX();     // compila, falla en runtime

Object es la raíz tipada (segura), dynamic desactiva verificaciones y void indica que el valor no debe usarse.

Partes y extensión de bibliotecas
// lib.dart
library my_lib;
part 'part_a.dart';
part 'part_b.dart';

// part_a.dart
part of 'lib.dart';

void funcA() {}

part y part of dividen una biblioteca en varios archivos que comparten el mismo ámbito. Útil para organizar código extenso.

Records (Dart 3)
(String, int) person = ("Ana", 30);
print(person.$1); // Ana
print(person.$2); // 30

// Con nombres
({String name, int age}) p =
    (name: "Ana", age: 30);
print(p.name);

Los records agrupan varios valores sin crear una clase. Pueden ser posicionales ($1, $2) o nombrados, y son inmutables.

Guardas (when)
String classify(int n) => switch (n) {
  < 0 => "negativo",
  0 => "zero",
  > 0 && < 10 => "pequeño",
  _ => "grande",
};

// Guarda con when
var r = switch (value) {
  int x when x.isEven => "par",
  _ => "impar",
};

Los relational patterns (<, >) y la guarda when permiten condiciones refinadas dentro de los casos del switch.

Constantes en tiempo de compilación
const list = [1, 2, 3];
const map = {"a": 1};
const point = Point(1, 2); // constructor const

// Deep const: todo es inmutable
const nested = [1, [2, 3]];
// nested[1].add(4); // ERROR

El const crea valores inmutables en compilación, incluyendo colecciones anidadas (deep const). Reduce asignaciones y mejora el rendimiento.

Records como retorno
(double, double) divide(int a, int b) {
  return (a / b, a % b.toDouble());
}

var (quotient, remainder) = divide(10, 3);
print("$quotient resto $remainder");

Las funciones pueden devolver varios valores vía records, sin crear una clase. La desestructuración var (a, b) = ... extrae los valores.

typedef (alias de tipos)
typedef Callback = void Function(String);
typedef Comparator<T> = int Function(T, T);

void register(Callback cb) {
  cb("ok");
}

register((msg) => print(msg));

El typedef crea un nombre alternativo para un tipo, útil para firmas de funciones complejas y genéricos reutilizables.

assert
void setAge(int age) {
  assert(age >= 0, "Edad inválida");
  // solo activo en modo debug
}

assert(list.isNotEmpty);
assert(x > 0, "x debe ser positivo");

El assert verifica una condición en modo debug y lanza AssertionError si es falsa. Se ignora en producción (no afecta el rendimiento).

Exceções e Erros


10 cards
try / catch
try {
  int r = 10 ~/ 0;
} catch (e) {
  print("Error: $e");
}

// Capturar tipo específico
try {
  risky();
} on FormatException {
  print("formato inválido");
}

El try/catch captura excepciones en runtime. El on filtra por tipo de excepción, permitiendo un tratamiento específico para cada caso.

Excepción personalizada
class InsufficientBalance implements Exception {
  final double amount;
  InsufficientBalance(this.amount);

  @override
  String toString() =>
      "Saldo insuficiente: $amount";
}

throw InsufficientBalance(100.0);

Crea clases que implementan Exception para errores específicos del dominio. Sobrescribe toString() para mensajes claros.

tryParse (evitar excepciones)
// En vez de try/catch:
int? n = int.tryParse("abc"); // null
if (n != null) {
  print("número: $n");
}

double? d = double.tryParse("3.14");

Los métodos tryParse() devuelven null en vez de lanzar una excepción. Preferible para validación de input, más limpio y eficiente.

finally
try {
  var data = await readFile();
  process(data);
} catch (e) {
  print("Falló: $e");
} finally {
  closeResources(); // se ejecuta siempre
}

El bloque finally se ejecuta siempre, haya o no excepción. Ideal para liberar recursos como archivos, conexiones o subscriptions.

Capturar stack trace
try {
  operation();
} catch (e, stack) {
  print("Error: $e");
  print("Stack: $stack");
}

El segundo parámetro del catch (stack) es la StackTrace, útil para depurar dónde ocurrió la excepción.

Errores vs Excepciones
// Error: bug de programación (no recuperar)
// StateError, ArgumentError, RangeError

// Exception: condición esperada (recuperable)
// FormatException, IOException, HttpException

throw StateError("estado inválido");
throw FormatException("input malo");

El Error indica bugs (no deben capturarse normalmente). La Exception representa condiciones esperadas y recuperables del programa.

throw (lanzar excepción)
void setAge(int age) {
  if (age < 0) {
    throw ArgumentError("Edad inválida: $age");
  }
}

void save(Object? v) {
  if (v == null) throw Exception("vacío");
}

El throw lanza una excepción. Puedes usar tipos nativos como ArgumentError o Exception, o crear los tuyos propios.

Excepciones comunes
int.parse("abc");   // FormatException
[1,2][5];           // RangeError
null!.length;       // TypeError (null check)
10 ~/ 0;            // UnsupportedError
{}["x"]!;           // null check operator

Excepciones frecuentes: FormatException (parse inválido), RangeError (índice fuera), TypeError (tipo/null erróneo).

rethrow
try {
  operation();
} catch (e) {
  print("registrar: $e");
  rethrow; // relanza la misma excepción
}

El rethrow relanza la excepción actual preservando la stack trace original. Útil para registrar el error antes de propagarlo.

runZonedGuarded
import 'dart:async';

runZonedGuarded(() {
  // código que puede lanzar errores
  asyncOperation();
}, (error, stack) {
  print("Error global: $error");
});

El runZonedGuarded() captura errores no tratados dentro de una zona, incluidos los asíncronos. Útil como handler global de errores.