Asincronía en Flutter

sept. 15, 2024

Este artículo forma parte de una serie:

Introducción a la Asincronía en Programación

En el mundo de la programación, uno de los mayores desafíos que enfrentan los desarrolladores es cómo gestionar tareas que tardan tiempo en completarse sin bloquear la ejecución del resto del programa. Este tipo de operaciones incluyen, por ejemplo, el acceso a recursos externos (como bases de datos o servicios web), la lectura de archivos grandes o cualquier operación intensiva en el uso de la CPU.

La asincronía es una técnica de programación que permite a los programas iniciar tareas sin necesidad de esperar a que estas finalicen, permitiendo que otras partes del código sigan ejecutándose. En lugar de detenerse y esperar la respuesta de una tarea, el programa continúa y, una vez que la tarea ha terminado, se notifica al sistema de que la operación ha finalizado.

Asincronía vs Sincronía

Cuando un programa ejecuta operaciones de manera síncrona, debe esperar a que cada tarea se complete antes de seguir adelante. Esto es como hacer cola en una tienda: cada cliente espera su turno hasta que el cliente anterior ha terminado su compra. En el caso de las aplicaciones, esto puede provocar bloqueos si la tarea que estamos esperando tarda mucho en completarse.

Por otro lado, la asincronía permite que las tareas que pueden tardar en completarse, como operaciones de red o lectura de archivos, no detengan la ejecución del programa. En lugar de esperar a que una tarea termine, el programa sigue avanzando y maneja el resultado de la operación asincrónica cuando esté listo. Esto mejora la fluidez y la respuesta del software, especialmente en aplicaciones que necesitan ser altamente interactivas.

Ejemplos de uso de la asincronía

El uso de la asincronía es común en escenarios donde el tiempo de espera puede ser largo o indefinido. Algunos ejemplos típicos incluyen:

  • Operaciones de entrada/salida (I/O): como leer o escribir en archivos, conectarse a bases de datos o interactuar con servicios web externos.
  • Llamadas a APIs: cuando una aplicación necesita hacer una solicitud a un servidor remoto y obtener una respuesta, en lugar de esperar a que la respuesta llegue, el código continúa ejecutándose y maneja la respuesta cuando esté disponible.
  • Interfaces gráficas: en aplicaciones con interfaz de usuario, mantener la interfaz reactiva mientras se realizan tareas en segundo plano es clave para no bloquear la interacción entre la aplicación y el usuario.

Mecanismos para manejar la Asincronía

En el desarrollo moderno, existen varias técnicas y mecanismos para manejar la asincronía. Entre los más comunes encontramos:

  1. Callbacks: Aunque no es específico para la asincronía, un callback es una función que se pasa como argumento a otra función, y que se ejecuta cuando la invoquemos.
  2. Promesas/Futuros: Representan un valor que estará disponible en el futuro. Las promesas permiten encadenar acciones que dependen de la finalización de una tarea.
  3. async/await: Estas palabras clave permiten escribir código asincrónico que parece sincrónico, facilitando su lectura y mantenimiento.

Diferentes lenguajes de programación manejan la asincronía de maneras distintas. Por ejemplo, en JavaScript se utilizan mucho las promesas, mientras que en Python y Dart, async y await se han convertido en los estándares para escribir código asincrónico de manera clara y concisa.

En el siguiente apartado, exploraremos cómo Flutter, con el lenguaje Dart, implementa este concepto y cómo podemos utilizarlo para crear aplicaciones móviles con una experiencia de usuario fluida.

Asincronía en Flutter gracias a Dart

Ahora que entendemos el concepto general de asincronía en programación, es momento de ver cómo se aplica en Flutter. Es importante mencionar que el manejo de la asincronía en Flutter proviene directamente del lenguaje en el que está escrito: Dart. Éste fue diseñado con la asincronía como una de sus características fundamentales, mucho antes de que Flutter existiera.

Dart, el lenguaje detrás de Flutter, tiene soporte nativo para manejar la asincronía de manera eficiente, utilizando operaciones como Futures y las palabras clave async y await. Estos permiten ejecutar tareas en segundo plano mientras la aplicación sigue siendo interactiva.

1. Futuros: La base de la asincronía en Dart

Un Future en Dart representa un elemento que se espera que esté disponible en el futuro. Es una promesa de que en algún momento se completará una operación asincrónica y se devolverá un valor o un error.

    
    Future<String> fetchData() {
      return Future.delayed(Duration(seconds: 2), () => "Data fetched!");
    }
    
    void main() {
      fetchData().then((data) {
        print(data);
      });
    }
    

En este ejemplo, la función fetchData devuelve un Future<String> que se completa después de 2 segundos. Luego, se usa el método then para manejar el valor devuelto por el Future cuando esté disponible. La recepción del resultado puede realizarse utilizando el then o con await en una función marcada como async.


    Future<String> fetchData() {
      return Future.delayed(Duration(seconds: 2), () => "Data fetched!");
    }
    
    void main() async {
      String data = await fetchData();
      print(data);
    }
    

En este caso, la función main está marcada como async, esto es mejor evitarlo aunque en un principio no debería dar problemas, nos permite utilizar await para esperar a que el Future se complete. Veamos un poco más de async y await en el siguiente apartado.

2. async y await: Simplificando el código asincrónico

Escribir código asincrónico con Future puede volverse complejo rápidamente, especialmente si tenemos que manejar múltiples tareas. Para hacer este proceso más sencillo, Dart nos proporciona las palabras clave async y await.

  • async: Se utiliza para declarar una función asincrónica. Esto significa que la función puede contener operaciones asincrónicas y que puede esperar a que se completen sin bloquear la ejecución.
  • await: Espera a que el proceso en segundo plano se complete. Durante este tiempo, la interfaz de usuario o el hilo principal no se bloquean.
    
    void main() async {
      await fetchDataAndDisplay();
    }
    
    Future<void> fetchDataAndDisplay() async {
      print('Fetching data...');
      String data = await fetchData();
      print(data);
    }
    
    Future<String> fetchData() {
      return Future.delayed(Duration(seconds: 2), () => "Data fetched!");
    }
    

En este ejemplo, la función fetchDataAndDisplay es asincrónica y hace uso de await para esperar a que la función fetchData se complete. Lo importante aquí es que, aunque estamos “esperando” a que la operación termine, la interfaz de usuario sigue respondiendo y no se bloquea.

3. Manejo de errores en operaciones asincrónicas

Cuando trabajamos con operaciones asincrónicas, es importante manejar los errores que puedan ocurrir durante la ejecución. Como una conexión de red fallida. Para esto, utilizamos bloques try-catch en combinación con await.


    Future<void> fetchDataAndDisplay() async {
      try {
        print('Fetching data...');
        String data = await fetchData();
        print(data);
      } catch (e) {
        print('Error fetching data: $e');
      }
    }
    

De esta manera, si ocurre un error al intentar obtener los datos, podemos capturarlo y manejarlo de forma adecuada sin provocar un cierre forzado en la aplicación.

4. Widgets asincrónicos en Flutter: FutureBuilder

Flutter nos proporciona un widget muy útil para manejar operaciones asincrónicas directamente en la interfaz de usuario: el FutureBuilder. Este widget nos permite construir la interfaz de usuario basándonos en el estado de un Future, lo que es perfecto para mostrar datos que provienen de una operación de larga duración, como la consulta a una API.


    Future<String> fetchData() {
      return Future.delayed(Duration(seconds: 2), () => "Data fetched!");
    }
    
    Widget build(BuildContext context) {
      return FutureBuilder<String>(
        future: fetchData(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return CircularProgressIndicator();
          } else if (snapshot.hasError) {
            return Text('Error: ${snapshot.error}');
          } else {
            return Text('Data: ${snapshot.data}');
          }
        },
      );
    }
    

Este ejemplo muestra un CircularProgressIndicator mientras se espera a que el Future se complete. Una vez que la operación termina, se muestra el resultado en un Text widget. Si ocurre un error, se muestra un mensaje de error. A continuación vemos un ejemplo más completo de cómo utilizar la asincronía en Flutter consultando una API y mostrar los datos en pantalla.

Ejemplo práctico: Aplicación para previsión del tiempo

Veamos en un ejemplo práctico cómo podemos utilizar la asincronía en Flutter creando una aplicación que consulta una API y muestra los datos en pantalla. Para ello recogeremos el tiempo para A Coruña (Galicia, España) de la API de 7Timer. Si hacemos la petición a la API de la siguiente manera https://www.7timer.info/bin/astro.php?lon=-8.1339&lat=42.5751&ac=0&unit=metric&output=json&tzshift=0 obtendremos un JSON con la información del tiempo actual y para los próximos días.

En esta aplicación mostraremos la temperatura, la velocidad del viento y la dirección del viento a lo largo de las horas y los días. Veamos el código fuente más relevante de la aplicación de ejemplo, en nuestro Github tienes el código completo.

Utilizaremos un FutureBuilder para manejar el estado de la petición y mostrar una interfaz de usuario basándonos en él.


    FutureBuilder<List<WeatherForecast>>(
        future: fetchWeatherData(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return ErrorDisplay(onRetry: () {
              setState(() {
                _forceError = !_forceError;
              });
            });
          } else if (snapshot.hasData) {
            final forecastData = snapshot.data!;
            final groupedForecasts = groupForecastsByDay(forecastData);
            return Column(
              children: [
                WeatherHeader(forecast: forecastData[0]),
                WeatherList(
                  groupedForecasts: groupedForecasts,
                  initTime: initTime,
                  onForecastTap: onForecastTap,
                ),
              ],
            );
          } else {
            return Center(child: Text('No data available'));
          }
        },
      ),
    

En este código, fetchWeatherData es una función que se encarga de realizar la petición a la API y devolver una lista de objetos WeatherForecast, una clase definida en nuestro modelo. Dependiendo del estado del Future, mostramos un indicador de carga, un mensaje de error o la información del tiempo en la interfaz de usuario si todo ha funcionado correctamente.


      Future<List<WeatherForecast>> fetchWeatherData() async {
        await initializeDateFormatting('es_ES', null);
        if (_forceError) {
          throw Exception('Forced error');
        }
        final response = await http.get(Uri.parse(
            'https://www.7timer.info/bin/astro.php?lon=$longitude&lat=$latitude&ac=0&unit=metric&output=json&tzshift=0'));
    
        if (response.statusCode == 200) {
          final data = jsonDecode(response.body);
          initTime =
          data['init']; // Guardamos el init (fecha de inicio del pronóstico)
          return (data['dataseries'] as List)
              .map((json) => WeatherForecast.fromJson(json))
              .toList();
        } else {
          throw Exception('Failed to load weather data');
        }
      }
    

Utilizamos async y await para realizar la petición a la API de manera asincrónica y esperar a que la respuesta esté disponible. Si la petición es exitosa, convertimos los datos JSON en objetos WeatherForecast y los devolvemos. Si hay un error lanzamos una excepción que será capturada por el FutureBuilder y mostraremos un mensaje de error en la pantalla.

En la lista de previsiones tenemos un callback que se ejecuta cuando se toca una de las previsiones. En este caso, se trata de un callback síncrono pero podríamos hacerlo asíncrono si necesitáramos realizar alguna operación que tardara algún tiempo.


    class WeatherList extends StatelessWidget {
        final Function(WeatherForecast) onForecastTap;
        ...
        
        const WeatherList({
          ...
          required this.onForecastTap,
        });
    
        @override
        Widget build(BuildContext context) {
          return Expanded(
            child: ListView(
                ListTile(
                    ...
                    onTap: () => onForecastTap(forecast),
          ));
        }
    }
    

De esta forma devolvemos la petición de click en la previsión a la clase padre y ella se encargará de gestionar la acción a realizar. Te recomiendo que eches un vistazo al código fuente completo de la aplicación que hemos dejado en nuestro repositorio de GitHub. Ahí podrás ver cómo se ha implementado la aplicación de principio a fin, y descargarte el código para probarlo en tu propio entorno y modificarlo a tu gusto.

Conclusión

La asincronía es un concepto fundamental en la programación moderna, especialmente en el desarrollo de aplicaciones móviles y web. Como hemos visto, en Flutter, con el lenguaje Dart, podemos manejar operaciones asincrónicas de manera eficiente y sencilla, gracias a constructos como Futures, async y await.

Al comprender cómo funciona la asincronía y cómo aplicarla en Flutter, podemos crear aplicaciones más eficientes, reactivas y responsivas, que brinden una mejor experiencia de usuario. La asincronía nos permite realizar tareas intensivas en segundo plano sin bloquear la interfaz de usuario, lo que es esencial para mantener la fluidez y la interactividad en nuestras apps. Y sobre todo ¡Happy Coding!

Artículos relacionados

Quizá te puedan interesar

September 15, 2023

Introducción a Flutter

Flutter es un framework de desarrollo de aplicaciones móviles creado por Google, utiliza el motor de …

leer más