Operaciones asíncronas en Dart con Futures

Dart es un lenguaje de programación que se ejecuta en un único hilo. Si un bloque de código bloquea dicho hilo (por ejemplo haciendo una operación de entrada-salida de larga duración), la aplicación quedará congelada. Las operaciones asíncronas permiten al programador crear operaciones sin bloquear la aplicación entera. Dart usa la clase Future para realizar estas operaciones asíncronas.

En el tutorial sobre Futures podrás encontrar información acerca de cómo usar esta API de Dart.

Introducción

El siguiente código haría que la aplicación quedara congelada:

import 'dart:io';

void printDailyNewsDigest() {
  File file = new File("dailyNewsDigest.txt");
  print(file.readAsStringSync());
}

void main() {
  printDailyNewsDigest();
  printWinningLotteryNumbers();
  printWeatherForecast();
  printBaseballScore();
}

El problema de este código es que readAsStringSync() no retorna hasta que no termina, con lo que las llamadas al resto de métodos serán retrasadas.

Para lograr hacer que esa llamada no bloquee la ejecución del resto, Dart proporciona los Futures.

¿Qué es un Future?

Es simplemente un medio para obtener un valor en algún momento en el futuro.

La forma en la que funciona es la siguiente: una función que necesita hacer una acción muy costosa en el tiempo, encola dicha acción como un trabajo para hacer y devuelve un objeto Future inmediatamente, de esta forma se consigue no bloquear el hilo de ejecución de Dart. Más adelante, cuando el trabajo ha terminado, y el valor está disponible, se dice que el Future se ha completado con dicho valor.

Para obtener el valor que representa Future, se usa el método then(), pasándole como parámetro una función de callback que será llamada con el valor del Future.

¿Cómo se usa?

El método bloqueante anterior quedaría así usando Future:

import 'dart:io';
import 'dart:async';

void printDailyNewsDigest() {
  File file = new File("dailyNewsDigest.txt");
  Future future = file.readAsString();
  future.then((content) {
    print(content);
  });
}

Esta vez, para leer el fichero se usa el método readAsString(), el cual no bloquea el hilo y retorna un Future. Después, se registra un callback a través del método then(). Este callback recibe como parámetro el valor que esperamos que retorne readAsString(), que es el contenido del fichero. Es en este callback donde se imprime el contenido del fichero leído.

El propio método then() devuelve también un Future, por lo cual se pueden encadenar distintas llamadas then().

Gestión de errores

La gestión de errores con Future es muy sencilla, ya que la clase proporciona el método catchError() que será llamado si se produce un error a la hora de conseguir el valor que representa el Future.

El método anterior se puede reescribir así:

void printDailyNewsDigest() {
  File file = new File("dailyNewsDigest.txt");
  Future future = file.readAsString();
  future.then((content) => doSomethingWith(content))
        .catchError((e) => handleError(e));
}

De esta forma, si readAsString() produce un error, la variable future se completará con error (en lugar de completarse con el valor del contenido del fichero), lo que provocará que el Future devuelto por then() se complete también con error, lo que hará que se llame al método catchError(), el cual gestionará el error.

Encadenando múltiples llamadas a then()

Supongamos que existen tres funciones: expensiveA(), expensiveB() y expensiveC(). Todas ellas devolviendo Futures, de forma que se pueden encadenar llamadas a then() de esta forma:

expensiveA().then((aValue) => expensiveB()) 
            .then((bValue) => expensiveC()) 
            .then((cValue) => doSomethingWith(cValue));

También existe otra posibilidad, y es esperar a que termine la ejecución de las tres, para realizar alguna acción solamente cuando los tres Futures se hayan completado. Para ello, el API proporciona el método wait().

Future.wait([expensiveA(), expensiveB(), expensiveC()])
      .then((List responses) => chooseBestResponse(responses));

Aquí, wait() devuelve un Future cuyo valor es una lista con los valores de todos los Futures pasados como parámetros.

Creando tus propios Future

De acuerdo, entendido cómo se usan. Pero, ¿y si lo que quiero es ser yo quien comienza la cadena de Futures? ¿Cómo se crea el primero de ellos?

Future methodReturningAFuture() {
    return new Future.value('foo bar');
}

El método del código anterior devuelve un valor, un Future que se resuelve al valor foo bar. Si quisiéramos imprimir esa cadena por consola, haríamos lo siguiente:

methodReturningAFuture()
    .then((message) => print(message));

Conclusión

Si así en papel no te ha quedado lo suficientemente claro, te recomiendo que te pases por el tutorial sobre Futures o que le eches un ojo al código completo del tutorial.