Dominando Future y la asincronía en Dart

Un Future<T> es un objeto que representa un valor de tipo T que aún no está disponible, pero que estará en algún momento. No es el valor en sí, sino una promesa de que, tras una operación asíncrona, obtendremos un resultado o un error. En el corazón de esto está el Event Loop de Dart: como Dart es un lenguaje de un solo hilo (single-threaded), no puede detener la ejecución para esperar a que un archivo se lea o un servidor responda. En su lugar, delega esa tarea al sistema operativo, libera el hilo para seguir procesando otros eventos y, cuando la tarea termina, el resultado se encola para ser procesado.

Existen tres estados posibles para un Future:
1. Incompleto (Pending): La operación sigue en curso.
2. Completado con valor (Fulfilled): La operación terminó con éxito.
3. Completado con error (Rejected): La operación falló.

Usamos Future para cualquier operación que no sea puramente computacional y síncrona, como peticiones HTTP, lectura de archivos o temporizadores. Si intentas manejar algo asíncrono como si fuera síncrono (por ejemplo, asignando el resultado de una función que devuelve un Future directamente a una variable sin usar await), terminarás con el objeto Future en lugar del dato esperado, rompiendo la lógica de tu aplicación.

Para crear un Future de forma manual, puedes usar Future.value(v) si el dato ya lo tienes, o Future.error(e) si sabes que la operación fallará. Sin embargo, la forma más común de encolar trabajo es mediante el constructor Future(() => computation()), que asegura que la computación se ejecute en el siguiente ciclo del event loop. Para casos más complejos, como integrar una API antigua basada en callbacks, usamos un Completer<T>, que nos permite controlar manualmente cuándo un Future debe completarse.

import 'dart:async';

class Session {
  final String user;
  final List<String> permissions;
  Session(this.user, this.permissions);
}

// Simula una llamada a una API con un retraso
Future<Session> fetchSession(int userId) async {
  await Future.delayed(const Duration(milliseconds: 500));
  if (userId == 0) {
    throw Exception("Usuario no encontrado");
  }
  return Session("AdminUser", ["read", "write", "execute"]);
}

// Simula una operación que depende de un evento externo (ej. un socket o un botón)
Future<String> waitForManualOverride(Completer<String> completer) {
  print("Esperando autorización manual...");
  // Simulamos que la autorización llega tras 2 segundos
  Timer(const Duration(seconds: 2), () {
    completer.complete("Autorización concedida por el sistema");
  });
  return completer.future;
}

void main() async {
  print("--- Iniciando sistema ---");

  // 1. Uso de Completer para controlar un evento externo
  final overrideCompleter = Completer<String>();

  // 2. Ejecución en paralelo con Future.wait
  // Lanzamos varias operaciones simultáneas para optimizar el tiempo de espera
  print("Cargando recursos en paralelo...");
  try {
    final results = await Future.wait([
      fetchSession(1),
      Future.delayed(const Duration(milliseconds: 800), "Configuración cargada"),
      waitForManualOverride(overrideCompleter),
    ]);

    // Los resultados mantienen el orden en que se pasaron a Future.wait
    final session = results[0] as Session;
    final config = results[1] as String;
    final override = results[2] as String;

    print("Sesión: ${session.user} (${session.permissions.length} permisos)");
    print("Config: $config");
    print("Status: $override");

  } on Object catch (e) {
    print("Error crítico en el arranque: $e");
  }

  // 3. Uso de Future.any para una estrategia de "competencia"
  // Nos quedamos con el que responda primero
  print("\n--- Verificando latencia de servidor ---");
  try {
    final fastestResponse = await Future.any([
      Future.delayed(const Duration(milliseconds: 300), "Servidor A (Rápido)"),
      Future.delayed(const Duration(milliseconds: 1000), "Servidor B (Lento)"),
    ]);
    print("Respuesta ganadora: $fastestResponse");
  } catch (e) {
    print("Uno de los servidores falló");
  }

  // 4. Diferencia entre Future.value y el flujo asíncrono
  print("\n--- Demostración de Microtasks ---");
  print("1. Antes del Future.value");
  Future.value("Dato inmediato").then((val) => print("3. Valor de Future.value: $val"));
  print("2. Después de Future.value");
  
  print("--- Fin de la ejecución ---");
}

Análisis del código

En el main, observamos cómo Future.wait es la herramienta clave para la concurrencia. En lugar de esperar a que fetchSession termine para luego empezar waitForManualOverride, lanzamos ambos procesos simultáneamente. El runtime de Dart gestiona estas tareas en el event loop y Future.wait suspende la ejecución de main hasta que la lista de Futures se haya completado. Es vital entender que, aunque se lanzan “en paralelo”, en Dart siguen corriendo sobre un mismo hilo; lo que se paralleliza es la espera de la entrada/salida (I/O).

El Completer<String> es el puente entre el mundo imperativo y el asíncrono. Cuando llamamos a overrideCompleter.future, obtenemos un Future que está en estado pending. Solo cuando el Timer ejecuta completer.complete(...), ese Future cambia su estado y desbloquea el await que lo estaba esperando.

Un detalle de bajo nivel importante: fíjate en el orden de impresión del punto 1 al 3. Aunque Future.value ya tiene su valor, su callback (.then) no se ejecuta inmediatamente. Se encola como una microtask. Las microtasks tienen prioridad sobre los eventos del event loop (como timers o I/O), pero se ejecutan después de que el código síncrono actual haya terminado. Por eso, el “2. Después de…” se imprime antes que el “3. Valor de…”.

El error frecuente

Un error común al usar Future.wait es asumir que si uno de los elementos falla, los demás se cancelan automáticamente. En realidad, Future.wait tiene un comportamiento de “todo o nada” por defecto: si uno de los Futures lanza una excepción, Future.wait se completa inmediatamente con ese error, pero las otras operaciones que ya estaban en curso siguen corriendo en segundo plano hasta que terminen o fallen.

// ERROR: El error de 'fetchSession' detiene el await, 
// pero la otra tarea sigue "viva" en el event loop.
try {
  await Future.wait([
    fetchSession(0), // Esto lanza Exception
    Future.delayed(const Duration(seconds: 10), () => print("Esto se imprimirá tarde")),
  ]);
} catch (e) {
  print("Error capturado: $e");
}
// El print del Timer se ejecutará 10 segundos después, 
// causando un efecto secundario inesperado.

Si necesitas que una falla no rompa el grupo, utiliza Future.wait con error.orElse o maneja el error internamente en cada Future antes de pasarlo al wait.

65

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio