Saltar al contenido principal

Programación asíncrona: futures, async, await

¡Aprende y practica escribiendo código asíncrono en DartPad!

Este tutorial te enseña cómo escribir código asíncrono usando futures y las palabras clave async y await. Usando editores DartPad incrustados, puedes probar tus conocimientos ejecutando código de ejemplo y completando ejercicios.

Para aprovechar al máximo este tutorial, deberías tener lo siguiente:

Este tutorial cubre el siguiente material:

  • Cómo y cuándo usar las palabras clave async y await.
  • Cómo afecta el uso de async y await al orden de ejecución.
  • Cómo manejar errores de una llamada asíncrona usando expresiones try-catch en funciones async.

Tiempo estimado para completar este tutorial: 40-60 minutos.

Los ejercicios de este tutorial tienen fragmentos de código parcialmente completados. Puedes usar DartPad para probar tus conocimientos completando el código y haciendo clic en el botón Run. No edites el código de prueba en la función main o debajo.

Si necesitas ayuda, expande el menú desplegable Pista o Solución después de cada ejercicio.

Por qué es importante el código asíncrono

#

Las operaciones asíncronas permiten que tu programa complete el trabajo mientras espera que termine otra operación. Aquí tienes algunas operaciones asíncronas comunes:

  • Obtener datos a través de una red.
  • Escribir en una base de datos.
  • Leer datos de un archivo.

Estas operaciones asíncronas usualmente proporcionan su resultado como un Future o, si el resultado tiene múltiples partes, como un Stream. Estos cálculos introducen asincronía en un programa. Para acomodar esa asincronía inicial, otras funciones normales de Dart también necesitan volverse asíncronas.

Para interactuar con estos resultados asíncronos, puedes usar las palabras clave async y await. La mayoría de las funciones asíncronas son simplemente funciones de Dart asíncronas que dependen, posiblemente en el fondo, de un cálculo intrínsecamente asíncrono.

Ejemplo: Uso incorrecto de una función asíncrona

#

El siguiente ejemplo muestra la forma incorrecta de usar una función asíncrona (fetchUserOrder()). Más adelante corregirás el ejemplo usando async y await. Antes de ejecutar este ejemplo, intenta detectar el problema: ¿cuál crees que será la salida?

He aquí por qué el ejemplo no logra imprimir el valor que fetchUserOrder() eventualmente produce:

  • fetchUserOrder() es una función asíncrona que, después de un retraso, proporciona una cadena que describe el pedido del usuario: un "Large Latte".
  • Para obtener el pedido del usuario, createOrderMessage() debería llamar a fetchUserOrder() y esperar a que termine. Como createOrderMessage() no espera a que fetchUserOrder() termine, createOrderMessage() no logra obtener el valor de cadena que fetchUserOrder() eventualmente proporciona.
  • En su lugar, createOrderMessage() obtiene una representación del trabajo pendiente por hacer: un future incompleto. Aprenderás más sobre los futures en la siguiente sección.
  • Debido a que createOrderMessage() no logra obtener el valor que describe el pedido del usuario, el ejemplo no imprime "Large Latte" en la consola, y en su lugar imprime "Your order is: Instance of '_Future<String>'".

En las siguientes secciones aprenderás sobre los futures y sobre cómo trabajar con ellos (usando async y await) para que puedas escribir el código necesario para hacer que fetchUserOrder() imprima el valor deseado ("Large Latte") en la consola.

¿Qué es un future?

#

Un future (con "f" minúscula) es una instancia de la clase Future (con "F" mayúscula). Un future representa el resultado de una operación asíncrona y puede tener dos estados: incompleto o completado.

Incompleto (Uncompleted)

#

Cuando llamas a una función asíncrona, esta devuelve un future incompleto. Ese future está esperando a que termine la operación asíncrona de la función o a que lance un error.

Completado (Completed)

#

Si la operación asíncrona tiene éxito, el future se completa con un valor. De lo contrario, se completa con un error.

Completar con un valor

#

Un future de tipo Future<T> se completa con un valor de tipo T. Por ejemplo, un future de tipo Future<String> produce un valor de cadena. Si un future no produce un valor utilizable, entonces el tipo del future es Future<void>.

Completar con un error

#

Si la operación asíncrona realizada por la función falla por cualquier motivo, el future se completa con un error.

Ejemplo: Presentando los futures

#

En el siguiente ejemplo, fetchUserOrder() devuelve un future que se completa después de imprimir en la consola. Debido a que no devuelve un valor utilizable, fetchUserOrder() tiene el tipo Future<void>. Antes de ejecutar el ejemplo, intenta predecir qué se imprimirá primero: "Large Latte" o "Fetching user order...".

En el ejemplo anterior, a pesar de que fetchUserOrder() se ejecuta antes de la llamada a print() en la línea 8, la consola muestra la salida de la línea 8 ("Fetching user order...") antes de la salida de fetchUserOrder() ("Large Latte"). Esto se debe a que fetchUserOrder() tiene un retraso antes de imprimir "Large Latte".

Ejemplo: Completar con un error

#

Ejecuta el siguiente ejemplo para ver cómo se completa un future con un error. Un poco más adelante aprenderás a manejar el error.

En este ejemplo, fetchUserOrder() se completa con un error que indica que el ID de usuario no es válido.

Has aprendido sobre los futures y cómo se completan, pero ¿cómo usas los resultados de las funciones asíncronas? En la siguiente sección aprenderás cómo obtener resultados con las palabras clave async y await.

Trabajar con futures: async y await

#

Las palabras clave async y await proporcionan una forma declarativa de definir funciones asíncronas y usar sus resultados. Recuerda estas dos pautas básicas cuando uses async y await:

  • Para definir una función async, añade async antes del cuerpo de la función:
  • The await keyword works only in async functions.

Aquí hay un ejemplo que convierte main() de una función síncrona a una asíncrona.

Primero, añade la palabra clave async antes del cuerpo de la función:

dart
void main() async { ··· }

Si la función tiene un tipo de retorno declarado, entonces actualiza el tipo para que sea Future<T>, where T es el tipo del valor que devuelve la función. Si la función no devuelve explícitamente un valor, entonces el tipo de retorno es Future<void>:

dart
Future<void> main() async { ··· }

Ahora que tienes una función async, puedes usar la palabra clave await para esperar a que se complete un future:

dart
print(await createOrderMessage());

Como muestran los siguientes dos ejemplos, las palabras clave async y await dan como resultado un código asíncrono que se parece mucho al código síncrono. Las únicas diferencias se destacan en el ejemplo asíncrono, que, si tu ventana es lo suficientemente ancha, está a la derecha del ejemplo síncrono.

Ejemplo: funciones síncronas

#
dart
String createOrderMessage() {
  var order = fetchUserOrder();
  return 'Your order is: $order';
}

Future<String> fetchUserOrder() =>
    // Imagine that this function is
    // more complex and slow.
    Future.delayed(const Duration(seconds: 2), () => 'Large Latte');

void main() {
  print('Fetching user order...');
  print(createOrderMessage());
}
Fetching user order...
Your order is: Instance of 'Future<String>'

Como se muestra en los siguientes dos ejemplos, funciona como código síncrono.

Ejemplo: funciones asíncronas

#
dart
Future<String> createOrderMessage() async {
  var order = await fetchUserOrder();
  return 'Your order is: $order';
}

Future<String> fetchUserOrder() =>
    // Imagine that this function is
    // more complex and slow.
    Future.delayed(const Duration(seconds: 2), () => 'Large Latte');

Future<void> main() async {
  print('Fetching user order...');
  print(await createOrderMessage());
}
Fetching user order...
Your order is: Large Latte

El ejemplo asíncrono es diferente en tres aspectos:

  • El tipo de retorno de createOrderMessage() cambia de String a Future<String>.
  • La palabra clave async aparece antes del cuerpo de las funciones para createOrderMessage() y main().
  • La palabra clave await aparece antes de llamar a las funciones asíncronas fetchUserOrder() y createOrderMessage().

Flujo de ejecución con async y await

#

Una función async se ejecuta sincrónicamente hasta la primera palabra clave await. Esto significa que dentro del cuerpo de una función async, todo el código síncrono antes de la primera palabra clave await se ejecuta inmediatamente.

Ejemplo: Ejecución dentro de funciones async

#

Ejecuta el siguiente ejemplo para ver cómo procede la ejecución dentro del cuerpo de una función async. ¿Cuál crees que será la salida?

Después de ejecutar el código en el ejemplo anterior, intenta invertir las líneas 2 y 3:

dart
var order = await fetchUserOrder();
print('Awaiting user order...');

Observa que el orden temporal de la salida cambia, ahora que print('Awaiting user order') aparece después de la primera palabra clave await en printOrderMessage().

Ejercicio: Practicar el uso de async y await

#

El siguiente ejercicio es una prueba unitaria fallida que contiene fragmentos de código parcialmente completados. Tu tarea es completar el ejercicio escribiendo código para que las pruebas pasen. No necesitas implementar main().

Para simular operaciones asíncronas, llama a las siguientes funciones, que se te proporcionan:

FunctionFirma de tipoDescripción
fetchRole() Future<String> fetchRole() Obtiene una breve descripción del rol del usuario.
fetchLoginAmount() Future<int> fetchLoginAmount() Obtiene el número de veces que un usuario ha iniciado sesión.

Parte 1: reportUserRole()

#

Agrega código a la función reportUserRole() para que haga lo siguiente:

  • Returns a future that completes with the following string: "User role: <user role>"
    • Nota: Debes usar el valor real devuelto por fetchRole(); copiar y pegar el valor de retorno de ejemplo no hará que la prueba pase.
    • Valor de retorno de ejemplo: "User role: tester"
  • Obtiene el rol del usuario llamando a la función proporcionada fetchRole().

Parte 2: reportLogins()

#

Implementa una función async reportLogins() para que haga lo siguiente:

  • Returns the string "Total number of logins: <# of logins>".
    • Nota: Debes usar el valor real devuelto por fetchLoginAmount(); copiar y pegar el valor de retorno de ejemplo no hará que la prueba pase.
    • Valor de retorno de ejemplo de reportLogins(): "Total number of logins: 57"
  • Obtiene el número de inicios de sesión llamando a la función proporcionada fetchLoginAmount().
Pista

¿Te acordaste de agregar la palabra clave async a la función reportUserRole?

¿Te acordaste de usar la palabra clave await antes de invocar a fetchRole()?

Recuerda: reportUserRole necesita devolver un Future.

Solución
dart
Future<String> reportUserRole() async {
  final username = await fetchRole();
  return 'User role: $username';
}

Future<String> reportLogins() async {
  final logins = await fetchLoginAmount();
  return 'Total number of logins: $logins';
}

Manejo de errores

#

Para manejar errores en una función async, usa try-catch:

dart
try {
  print('Awaiting user order...');
  var order = await fetchUserOrder();
} catch (err) {
  print('Caught error: $err');
}

Dentro de una función async, puedes escribir cláusulas try-catch de la misma manera que lo harías en el código síncrono.

Ejemplo: async y await con try-catch

#

Ejecuta el siguiente ejemplo para ver cómo manejar un error de una función asíncrona. ¿Cuál crees que será la salida?

Ejercicio: Practicar el manejo de errores

#

El siguiente ejercicio proporciona práctica para manejar errores con código asíncrono, utilizando el enfoque descrito en la sección anterior. Para simular operaciones asíncronas, tu código llamará a la siguiente función, que se te proporciona:

FunctionFirma de tipoDescripción
fetchNewUsername() Future<String> fetchNewUsername() Devuelve el nuevo nombre de usuario que puedes usar para reemplazar uno antiguo.

Usa async y await para implementar una función asíncrona changeUsername() función que haga lo siguiente:

  • Calls the provided asynchronous function fetchNewUsername() and returns its result.
    • Valor de retorno de ejemplo de changeUsername(): "jane_smith_92"
  • Catches any error that occurs and returns the string value of the error.
Pista

Implementa changeUsername para devolver la cadena de fetchNewUsername o, si eso falla, el valor de cadena de cualquier error que ocurra.

Recuerda: Puedes usar una sentencia try-catch para capturar y manejar errores.

Solución
dart
Future<String> changeUsername() async {
  try {
    return await fetchNewUsername();
  } catch (err) {
    return err.toString();
  }
}

Ejercicio: Poniéndolo todo junto

#

Es hora de practicar lo que has aprendido en un último ejercicio. Para simular operaciones asíncronas, este ejercicio proporciona las funciones asíncronas fetchUsername() y logoutUser():

FunctionFirma de tipoDescripción
fetchUsername() Future<String> fetchUsername() Devuelve el nombre asociado con el usuario actual.
logoutUser() Future<String> logoutUser() Realiza el cierre de sesión del usuario actual y devuelve el nombre de usuario que cerró la sesión.

Escribe lo siguiente:

Parte 1: addHello()

#
  • Escribe una función addHello() que tome un único argumento String.
  • addHello() devuelve su argumento String precedido por 'Hello '.
    Ejemplo: addHello('Jon') devuelve 'Hello Jon'.

Parte 2: greetUser()

#
  • Escribe una función greetUser() que no tome argumentos.
  • Para obtener el nombre de usuario, greetUser() llama a la función asíncrona proporcionada fetchUsername().
  • greetUser() crea un saludo para el usuario llamando a addHello(), pasándole el nombre de usuario y devolviendo el resultado.
    Ejemplo: Si fetchUsername() devuelve 'Jenny', entonces greetUser() devuelve 'Hello Jenny'.

Parte 3: sayGoodbye()

#
  • Write a function sayGoodbye() that does the following:
    • No toma argumentos.
    • Captura cualquier error.
    • Llama a la función asíncrona proporcionada logoutUser().
  • Si logoutUser() falla, sayGoodbye() devuelve cualquier cadena que desees.
  • Si logoutUser() tiene éxito, sayGoodbye() devuelve la cadena '<result> Thanks, see you next time', donde <result> es el valor de cadena devuelto al llamar a logoutUser().
Pista

Las funciones greetUser y sayGoodbye deben ser asíncronas, mientras que addHello debe ser una función normal y síncrona.

Recuerda: Puedes usar una sentencia try-catch para capturar y manejar errores.

Solución
dart
String addHello(String user) => 'Hello $user';

Future<String> greetUser() async {
  final username = await fetchUsername();
  return addHello(username);
}

Future<String> sayGoodbye() async {
  try {
    final result = await logoutUser();
    return '$result Thanks, see you next time';
  } catch (e) {
    return 'Failed to logout user: $e';
  }
}

¿Qué linter rules funcionan para futures?

#

Para detectar errores comunes que surgen al trabajar con async y futures, habilita las siguientes linter rules (lints):

¿Qué sigue?

#

¡Felicitaciones, has terminado el tutorial! Si deseas obtener más información, aquí hay algunas sugerencias sobre a dónde ir a continuación: