Programación asincrónica en Dart: Futures
Una de las APIs más básicas que Dart tiene para programación asincrónica son los futures — objetos de tipo Future. En su mayor parte, Dart…
Una de las APIs más básicas que Dart tiene para programación asíncrona son los futures — objetos de tipo
Future. En su mayor parte, los futures de Dart son muy similares a las
APIs de future o promise que se encuentran en otros lenguajes.
Este artículo discute los conceptos detrás de los futures de Dart y te explica cómo usar la API de
Future.
También discute el widget FutureBuilder de Flutter, que te ayuda a actualizar una UI de Flutter de forma asíncrona, basándose en el estado de un future.
Gracias a características del lenguaje Dart como async-await, puede que nunca necesites usar la
API de Future directamente. Pero es casi seguro que encontrarás futures en tu código Dart. Y puede que quieras
crear futures o leer código que usa la API de Future.
Este artículo es el segundo basado en la serie de videos Flutter in Focus Asynchronous Programming in Dart. El primer artículo, Isolates and event loops, cubrió los fundamentos del soporte de Dart para trabajo en segundo plano. Si prefieres aprender viendo o escuchando, todo lo que se trata en este artículo está cubierto en el siguiente video*.*
Puedes pensar en los futures como pequeñas cajas de regalo para datos. Alguien te entrega una de estas cajas de regalo, que empieza cerrada. Un poco después la caja se abre, y dentro hay un valor o un error.
Así que un future puede estar en uno de 3 estados:
No completado: La caja de regalo está cerrada*.*
Completado con un valor: La caja está abierta, y tu regalo (datos) está listo.
Completado con un error: La caja está abierta, pero algo salió mal.
La mayor parte del código que vas a ver a continuación gira en torno a lidiar con estos tres estados. Recibes un future, y necesitas decidir qué hacer hasta que la caja se abra, qué hacer cuando se abre con un valor, y qué hacer si hay un error. Verás ese patrón 1–2–3 mucho.
Puede que recuerdes el event loop (ilustrado abajo) de nuestro artículo sobre el event loop de Dart. Algo bueno saber sobre los futures es que en realidad son solo una API construida para facilitar el uso del event loop.
El código de Dart que escribes es ejecutado por un único hilo. Durante todo el tiempo que tu app está en ejecución, ese pequeño hilo simplemente sigue dando vueltas y vueltas, recogiendo eventos de la cola de eventos y procesándolos.
Supongamos que tienes algo de código para un botón de descarga (implementado abajo como un RaisedButton). El usuario toca, y tu botón empieza a descargar una imagen.
RaisedButton(
onPressed: () {
final myFuture = http.get('https://my.image.url');
myFuture.then((resp) {
setImage(resp);
});
},
child: Text('Click me!'),
)
Primero ocurre el evento del toque. El event loop recibe el evento, y llama a tu manejador del toque (que configuraste usando el parámetro
onPressed en el constructor de RaisedButton). Tu manejador usa la librería http para hacer una petición (http.get()), y obtiene un future a cambio (myFuture).
Así que ahora tienes tu pequeña caja, myFuture. Empieza cerrada. Para registrar un callback para cuando se abra, usas
then().
Una vez que tienes tu caja de regalo, esperas. Puede que lleguen otros eventos, el usuario haga algunas cosas, y tu pequeña caja simplemente se queda ahí mientras el event loop sigue dando vueltas.
Eventualmente, los datos de la imagen se descargan, y la librería http dice, “¡Genial! Tengo este future justo aquí.” Pone los datos en la caja y la abre, lo cual dispara tu callback.
Ahora ese pequeño fragmento de código que le pasaste a then() se ejecuta, y muestra la imagen.
Durante todo ese proceso, tu código nunca tuvo que tocar el event loop directamente. No importaba qué más estuviera ocurriendo, ni qué otros eventos llegaran. Todo lo que necesitabas hacer era obtener el future de la librería http, y luego decir qué hacer cuando el future se completara.
En código real, también tendrías que ocuparte de los errores. Te mostraremos cómo hacerlo un poco más adelante.
Echemos un vistazo más detallado a la API de Future, parte de la cual acabas de ver en uso.
Vale, primera pregunta: ¿cómo obtienes una instancia de un Future? La mayoría de las veces, no creas futures directamente. Eso es porque muchas de las tareas comunes de programación asíncrona ya tienen librerías que generan futures por ti.
Por ejemplo, la comunicación de red devuelve un future:
final myFuture = http.get('http://example.com');
Obtener acceso a las preferencias compartidas también devuelve un future:
final myFuture = SharedPreferences.getInstance();
Pero también puedes usar los constructores de Future para crear futures.
Constructores de Future
#El constructor más simple es Future(), que toma una función y devuelve un future que coincide con el tipo de retorno de la función. Luego la función se ejecuta de forma asíncrona, y el future se completa con el valor de retorno de la función. Aquí hay un ejemplo de uso de
Future():
void main() {
final myFuture = Future(() {
return 12;
});
}
Añadamos un par de sentencias print para que la parte asíncrona quede clara:
void main() {
final myFuture = Future(() {
print('Creating the future.'); // Prints second.
return 12;
});
print('Done with main().'); // Prints first.
}
Si ejecutas ese código en DartPad (dartpad.dev), toda la función main termina antes que la función pasada al constructor
Future(). Eso es porque el constructor Future() al principio solo devuelve un future no completado. Dice: “Aquí tienes esta caja. Guardala por ahora, y más tarde iré a ejecutar tu función y pondré algunos datos ahí para ti.” Aquí está la salida del código anterior:
Done with main().
Creating the future.
Otro constructor, Future.value(), es útil cuando ya conoces el valor del future. Este constructor es útil cuando estás construyendo servicios que usan caché. A veces ya tienes el valor que necesitas, así que puedes meterlo directamente ahí:
final myFuture = Future.value(12);
El constructor Future.value() tiene una contraparte para completar con un error. Se llama
Future.error(), y funciona esencialmente de la misma manera, pero toma un objeto de error y un stacktrace opcional:
final myFuture = Future.error(ArgumentError.notNull('input'));
El constructor de future más práctico es probablemente Future.delayed(). Funciona igual que
Future(), excepto que espera una duración de tiempo especificada antes de ejecutar la función y completar el future.
Una forma de usar Future.delayed() es cuando estás creando servicios de red simulados para testing. Si necesitas asegurarte de que tu spinner de carga se muestra correctamente, un future retrasado es tu amigo.
final myFuture = Future.delayed(
const Duration(seconds: 5),
() => 12,
);
Uso de futures
#Ahora que sabes de dónde vienen los futures, hablemos de cómo usarlos. Como mencionamos antes, usar un future trata principalmente de contemplar los tres estados en los que puede estar: no completado, completado con un valor, o completado con un error.
El siguiente código usa Future.delayed() para crear un future que se completa después de 3 segundos con un valor de 100.
void main() {
Future.delayed(
const Duration(seconds: 3),
() => 100,
);
print('Waiting for a value...');
}
Cuando este código se ejecuta, main() se ejecuta de arriba a abajo, creando el future e imprimiendo “Waiting for a value…” Todo ese tiempo, el future está sin completar. No se completa hasta dentro de 3 segundos.
Para usar el valor completado, puedes usar then(). Es un método de instancia en cada future que puedes usar para registrar un callback para cuando el future se completa con un valor. Le pasas una función que toma un único parámetro que coincide con el tipo del future. Una vez que el future se completa con un valor, tu función es llamada con ese valor.
void main() {
Future.delayed(
const Duration(seconds: 3),
() => 100,
).then((value) {
print('The value is $value.'); // Prints later, after 3 seconds.
});
print('Waiting for a value...'); // Prints first.
}
Aquí está la salida del código anterior:
Waiting for a value... *(3 seconds pass until callback executes)*
The value is 100.
Además de ejecutar tu código, then() devuelve su propio future, coincidiendo con el valor de retorno de cualquier función que le pases. Así que si necesitas hacer un par de llamadas asíncronas, puedes encadenarlas incluso si tienen diferentes tipos de retorno.
_fetchNameForId(12)
.then((name) => _fetchCountForName(name))
.then((count) => print('The count is $count.'));
Volviendo a nuestro primer ejemplo, ¿qué pasa si ese future inicial no se completa con un valor — qué pasa si se completa con un error? El método
then() espera un valor. Necesitas una forma de registrar otro callback en caso de error.
La respuesta es usar catchError(). Funciona igual que
then(), excepto que toma un error en lugar de un valor, y se ejecuta si el future se completa con un error. Al igual que
then(), el método catchError() devuelve su propio future, así que puedes construir toda una cadena de
métodos then() y catchError() que se esperan entre sí.
Nota: No necesitas llamar a
then()ni acatchError()si usas la característica del lenguaje async-await. En su lugar, esperas el valor completado con await, y usas try-catch-finally para manejar los errores. Para más detalles, consulta la sección de soporte de asincronía del tour del lenguaje Dart.
Aquí hay un ejemplo de uso de catchError() para manejar el caso en que un future se completa con un error:
void main() {
Future.delayed(
Duration(seconds: 3),
() => throw 'Error!', // Complete with an error.
).then((value) {
print(value);
}).catchError((err) {
print('Caught $err'); // Handle the error.
});
print('Waiting for a value...');
}
Incluso puedes darle a catchError() una función de test para verificar el error antes de invocar el callback. Puedes tener múltiples
funciones catchError() de esta manera, cada una verificando un tipo diferente de error. Aquí hay un ejemplo de cómo especificar una función de test, usando el parámetro opcional
test de catchError():
void main() {
Future.delayed(
Duration(seconds: 3),
() => throw 'Error!',
).then((value) {
print(value);
}).catchError((err) {
print('Caught $err');
}, test: (err) { // Optional test parameter.
return err is String;
});
print('Waiting for a value...');
}
Ahora que has llegado hasta aquí, esperemos que puedas ver cómo los tres estados de un future a menudo se reflejan en la estructura del código. Hay tres bloques en el ejemplo anterior:
El primer bloque crea un future no completado.
Luego hay una función que llamar si el future se completa con un valor.
Luego hay otra función que llamar si el future se completa con un error.
Hay un método más que podrías querer usar: whenComplete(). Puedes usarlo para ejecutar una función cuando el future se ha completado, sin importar si es con un valor o con un error.
Es algo así como el bloque finally en un try-catch-finally. Hay código que se ejecuta si todo va bien, código para un error, y código que se ejecuta pase lo que pase.
Uso de futures en Flutter
#Así es como se crean los futures, y un poco sobre cómo puedes usar sus valores. Ahora hablemos de ponerlos a trabajar en Flutter.
Supongamos que tienes un servicio de red que va a devolver algunos datos JSON, y quieres mostrar esos datos. Podrías crear un
StatefulWidget
que cree el future, verifique si se completó o si hay error, llame a setState(), y en general maneje toda la conexión manualmente.
O puedes usar FutureBuilder. Es un widget que viene con el SDK de Flutter. Le das un future y una función builder, y automáticamente reconstruye sus hijos cuando el future se completa.
El widget FutureBuilder funciona llamando a su función builder, que toma un context y un snapshot del estado actual del future.
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Use a FutureBuilder.
return FutureBuilder<String>(
future: _fetchNetworkData(),
builder: (context, snapshot) {},
);
}
}
Puedes verificar el snapshot para ver si el future se completó con un error:
return FutureBuilder<String>(
future: _fetchNetworkData(5),
builder: (context, snapshot) {
if (snapshot.hasError) {
// Future completed with an error.
return Text(
'There was an error',
);
}
throw UnimplementedError("Case not handled yet");
},
);
De lo contrario puedes verificar la propiedad hasData para ver si se completó con un valor:
return FutureBuilder<String>(
future: _fetchNetworkData(5),
builder: (context, snapshot) {
if (snapshot.hasError) {
// Future completed with an error.
return Text(
'There was an error',
);
} else if (snapshot.hasData) {
// Future completed with a value.
return Text(
json.decode(snapshot.data)['field'],
);
}
throw UnimplementedError("Case not handled yet");
},
);
Si ni hasError ni hasData son true, entonces sabes que todavía estás esperando, y puedes mostrar algo para eso también.
return FutureBuilder<String>(
future: _fetchNetworkData(5),
builder: (context, snapshot) {
if (snapshot.hasError) {
// Future completed with an error.
return Text(
'There was an error',
);
} else if (snapshot.hasData) {
// Future completed with a value.
return Text(
json.decode(snapshot.data)['field'],
);
} else {
// Uncompleted.
return Text(
'No value yet!',
);
}
},
);
Incluso en código de Flutter, puedes ver cómo esos tres estados siguen apareciendo: no completado, completado con valor, y completado con error.
Resumen
#Este artículo habló sobre qué representan los futures y cómo puedes usar las APIs de
Future y FutureBuilder
para crear futures y usar sus valores completados.
Si te gustaría aprender más sobre el uso de futures — con la opción de usar ejemplos ejecutables y ejercicios interactivos para probar tu comprensión — consulta el codelab asíncrono sobre futures, async, y await.
O continúa con el siguiente video de la serie Asynchronous Programming in Dart. Habla sobre los streams, que son muy parecidos a los futures en que pueden proporcionar valores o errores. Pero mientras que los futures solo te dan un resultado y se detienen, los streams simplemente siguen y siguen.
Muchas gracias a Andrew Brogdon, quien creó el video en el que se basa este artículo.
Más de Dart
Historia del interop con JS en Dart
El soporte para Wasm acaba de llegar al beta actual de Flutter, gracias a un emocionante hito de interop con JavaScript alcanzado en Dart 3.3. Para…
Dart DevTools: Analizando el rendimiento de la aplicación con el CPU Profiler
Ya seas un desarrollador de backend escribiendo una herramienta de línea de comandos con Dart, o un ingeniero de UX construyendo aplicaciones con Flutter, el rendimiento del programa es crítico para el éxito de tu proyecto. Las herramientas de línea de comandos deberían minimizar la latencia y las apps deberían ser responsivas y ágiles sin fotogramas perdidos. Como desarrolladores, hacemos todo lo posible para escribir código performante pero a veces no está claro por qué nuestro código no funciona tan bien como esperaríamos.