Asynchrony in Flutter

Sep 15, 2024

This article is part of the series:

Introduction to asynchrony in programming

In the world of programming, one of the biggest challenges developers face is how to manage tasks that take time to complete without blocking the execution of the rest of the program. These tasks include, for example, accessing external resources (such as databases or web services), reading large files, or any CPU-intensive operations.

Asynchrony is a programming technique that allows programs to start tasks without waiting for them to finish, enabling other parts of the code to continue executing. Instead of stopping and waiting for the task’s response, the program keeps running and, once the task is completed, the system is notified that the operation has finished.

Asynchrony vs synchrony

When a program executes operations synchronously, it must wait for each task to complete before proceeding. This is like standing in line at a store: each customer waits their turn until the previous customer has finished their purchase. In applications, this can cause delays if the task we are waiting for takes too long to complete.

On the other hand, asynchrony allows tasks that may take time, such as network operations or file reading, to not block the program’s execution. Instead of waiting for a task to finish, the program keeps moving forward and handles the result of the asynchronous operation when it’s ready. This improves the smoothness and responsiveness of software, especially in applications that need to be highly interactive.

Examples of asynchrony in programming

Asynchrony is commonly used in scenarios where the waiting time can be long or indefinite. Some typical examples include:

  • Input/output (I/O) operations: such as reading or writing to files, connecting to databases, or interacting with external web services.
  • API calls: when an application needs to make a request to a remote server and retrieve a response, instead of waiting for the response to arrive, the code continues executing and handles the response when it’s available.
  • Graphical interfaces: in user interface applications, keeping the interface responsive while performing background tasks is key to not blocking the interaction between the application and the user.

Mechanisms for handling asynchrony

In modern development, there are several techniques and mechanisms for handling asynchrony. Among the most common are:

  1. Callbacks: While not exclusive to asynchrony, a callback is a function passed as an argument to another function, which is executed when invoked.
  2. Promises/Futures: These represent a value that will be available in the future. Promises allow chaining actions that depend on the completion of a task.
  3. async/await: These keywords allow writing asynchronous code that looks synchronous, making it easier to read and maintain.

Different programming languages handle asynchrony in different ways. For example, in JavaScript, promises are widely used, while in Python and Dart, async and await have become the standard for writing asynchronous code clearly and concisely.

In the next section, we will explore how Flutter, using the Dart language, implements this concept and how we can use it to create mobile applications with a smooth user experience.

Asynchrony in Flutter through Dart

Now that we understand the general concept of asynchrony in programming, it’s time to see how it applies in Flutter. It’s important to mention that Flutter’s asynchronous handling comes directly from the language it’s written in: **Dart **. Dart was designed with asynchrony as one of its fundamental features, long before Flutter existed.

Dart, the language behind Flutter, has native support for efficiently handling asynchrony using operations like * Futures* and the async and await keywords. These allow tasks to run in the background while the app remains interactive.

1. Futures: The foundation of asynchrony in Dart

A Future in Dart represents something that is expected to be available in the future. It is a promise that an asynchronous operation will eventually complete and either return a value or an error.

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

In this example, the fetchData function returns a Future<String> that completes after 2 seconds. Then, the then method is used to handle the value returned by the Future when it becomes available. The result can be received using then or with await in a function marked as async.


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

In this case, the main function is marked as async. While this is generally something to avoid, and it shouldn’t cause issues initially, it allows us to use await to wait for the Future to complete. Let’s explore more about async and await in the next section.

2. async and await: Simplifying asynchronous code

Writing asynchronous code with Futures can quickly become complex, especially if we need to handle multiple tasks. To simplify this process, Dart provides the async and await keywords.

  • async: Used to declare an asynchronous function. This means the function can contain asynchronous operations and wait for them to complete without blocking execution.
  • await: Waits for the background process to complete. During this time, the user interface or main thread is not blocked.
    
    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!");
    }
    

In this example, the fetchDataAndDisplay function is asynchronous and uses await to wait for the fetchData function to complete. The key point here is that, even though we are “waiting” for the operation to finish, the user interface remains responsive and is not blocked.

3. Handling errors in asynchronous operations

When working with asynchronous operations, it’s important to handle any errors that might occur during execution, such as a failed network connection. For this, we use try-catch blocks in combination with await.


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

This way, if an error occurs while trying to fetch the data, we can catch it and handle it properly without causing the application to crash.

4. Asynchronous widgets in Flutter: FutureBuilder

Flutter provides us with a very useful widget to handle asynchronous operations directly in the user interface: the FutureBuilder. This widget allows us to build the user interface based on the state of a Future, which is perfect for displaying data coming from a long-running operation, such as querying an 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}');
          }
        },
      );
    }
    

This example shows a CircularProgressIndicator while the Future is still pending. Once the operation completes, the result is displayed in a Text widget. If an error occurs, an error message is shown. Below is a more complete example of how to use asynchrony in Flutter by querying an API and displaying the data on the screen.

Practical example: Weather forecast app

Let’s look at a practical example of how we can use asynchrony in Flutter by creating an application that queries an API and displays the data on the screen. For this, we will retrieve the weather for A Coruña (Galicia, Spain) using the 7Timer API. If we make the request to the API like this: https://www.7timer.info/bin/astro.php?lon=-8.1339&lat=42.5751&ac=0&unit=metric&output=json&tzshift=0, we will get a JSON with the current weather information and the forecast for the coming days.

In this application, we will display the temperature, wind speed, and wind direction throughout the hours and days. Below is the most relevant source code from the sample application. You can find the complete code on our Github.

We will use a FutureBuilder to handle the state of the request and display a user interface based on it.


    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'));
          }
        },
      ),
    

In this code, fetchWeatherData is a function responsible for making the request to the API and returning a list of WeatherForecast objects, a class defined in our model. Depending on the state of the Future, we display a loading indicator, an error message, or the weather information in the user interface if everything has worked correctly.


      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');
        }
      }
    

We use async and await to make the API request asynchronously and wait for the response to become available. If the request is successful, we convert the JSON data into WeatherForecast objects and return them. If there is an error, we throw an exception that will be caught by the FutureBuilder, and an error message will be displayed on the screen.

In the forecast list, we have a callback that is executed when one of the forecasts is tapped. In this case, it is a synchronous callback, but we could make it asynchronous if we needed to perform an operation that takes some time.


    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),
          ));
        }
    }
    

This way, we pass the click event on the forecast back to the parent class, which will handle the action to be performed. I recommend taking a look at the complete source code of the application, which we have made available in our GitHub repository. There, you can see how the application is implemented from start to finish, and download the code to try it out in your own environment and modify it to your liking.

Conclusion

Asynchrony is a fundamental concept in modern programming, especially in mobile and web application development. As we’ve seen, in Flutter, using the Dart language, we can handle asynchronous operations efficiently and easily, thanks to constructs like Futures, async, and await.

By understanding how asynchrony works and how to apply it in Flutter, we can create more efficient, responsive, and interactive applications that provide a better user experience. Asynchrony allows us to perform background-intensive tasks without blocking the user interface, which is essential for keeping our apps smooth and interactive. And most importantly, Happy Coding!

Related posts

That may interest you

September 15, 2023

Flutter introduction

Flutter is a mobile app development framework created by Google. It utilizes the Skia rendering …

read more