Async Await Examples in Flutter

Asynchronous programming is an essential part of building modern applications that are responsive, fast, and efficient. It allows developers to write code that can perform long-running operations without blocking the main thread, making the app feel more responsive to user input.

Dart, the programming language used in Flutter, has built-in support for asynchronous programming using Futures and async/await. Futures provide a way to represent a value that may not be available yet, while async / await is a syntax that makes it easier to work with asynchronous code.

In this article, we will explore async / await in more detail and learn how to use it in Flutter. We’ll start by understanding what async/await is and how it works in Dart, and then move on to practical examples of how to use it for HTTP requests, long-running operations, and more. By the end of this article, you’ll have a solid understanding of async/await in Flutter and be able to use it in your own projects.

Understanding Futures

In Dart, a Future is an object that represents a value that may not be available yet. It allows us to perform an operation asynchronously and return a value at some point in the future. Futures are commonly used for long-running operations that may block the main thread, such as network requests or disk I/O.

A Future can have two states: incomplete or complete. When a Future is incomplete, it means that the value it represents is not yet available. When the operation associated with the Future completes, the Future becomes complete, and the value it represents is available.

Futures are created using the Future class and can be returned from functions or methods using the async keyword. For example, a function that makes an HTTP request may return a Future<String> that represents the response body as a string.

To retrieve the value of a Future, we can use the then() method or await keyword. The then() method allows us to attach a callback function that will be called when the Future completes, while the await keyword suspends the execution of the current function until the Future completes.

Here’s an example of using a Future to retrieve data from an API using the http package:

import 'package:http/http.dart' as http;

Future<String> fetchData() async {
  final response = await http.get(Uri.parse('https://example.com/data'));
  return response.body;
}

fetchData().then((data) {
  print(data);
});

In this example, the fetchData() function returns a Future<String> that represents the response body from the API. The await keyword is used to wait for the response to be returned from the server. Finally, we attach a callback function using the then() method to print the data when the Future completes.

By understanding Futures in Dart, we can better understand how async / await works and how to use it in our Flutter applications.

Using async / await with HTTP requests

HTTP requests are a common use case for asynchronous programming, as they involve waiting for a response from a server. In Flutter, we can use the http package to make HTTP requests, and async / await to handle them.

To make an HTTP request using the http package, we first need to import it into our project. We can do this by adding the following line to our pubspec.yaml file:

dependencies:
  http: ^0.13.3

Once the package is installed, we can import it into our Dart file and use it to make requests. Here’s an example of how to make an HTTP GET request using async / await:

import 'package:http/http.dart' as http;

Future<String> fetchData() async {
  final response = await http.get(Uri.parse('https://example.com/data'));
  return response.body;
}

In this example, we define a function called fetchData() that returns a Future<String>. Inside the function, we use the await keyword to wait for the response to be returned from the server using the http.get() method. The response is then returned as a string using the response.body property.

We can also handle errors that may occur during the request by using a try / catch block. Here’s an example of how to handle an HTTP request that returns an error:

import 'package:http/http.dart' as http;

Future<String> fetchData() async {
  try {
    final response = await http.get(Uri.parse('https://example.com/data'));
    return response.body;
  } catch (error) {
    return 'Error retrieving data: $error';
  }
}

In this example, we wrap the http.get() method in a try / catch block to handle any errors that may occur during the request. If an error occurs, we return a string that includes the error message.

By using async / await with HTTP requests, we can make our Flutter applications more responsive and efficient. We can also handle errors and parse the response data easily using the http package and Dart’s built-in asynchronous programming features.

Handling long-running operations

Long-running operations, such as complex calculations or heavy file I/O, can significantly slow down an application and make it feel unresponsive. In Flutter, we can use async / await to perform these operations asynchronously and keep the UI thread responsive.

To perform a long-running operation asynchronously, we can use the async keyword to define a function or method as asynchronous. Inside the function, we can use the await keyword to wait for the operation to complete without blocking the main thread.

Here’s an example of a function that performs a long-running operation using async / await:

Future<int> calculate() async {
  int sum = 0;
  for (int i = 0; i < 1000000000; i++) {
    sum += i;
  }
  return sum;
}

In this example, we define a function called calculate() that performs a long-running operation of summing up numbers from 0 to 1 billion. The function is defined as asynchronous using the async keyword, and the await keyword is used to wait for the operation to complete.

We can call this function from our Flutter application and display the result using a FutureBuilder widget. Here’s an example of how to do that:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<int>(
      future: calculate(),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return Text('Result: ${snapshot.data}');
        } else if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        }
        return CircularProgressIndicator();
      },
    );
  }
}

In this example, we define a MyWidget class that displays the result of our calculate() function using a Text widget. We use a FutureBuilder widget to asynchronously build the widget tree based on the state of the Future returned by calculate(). If the Future has data, we display the result using a Text widget. If the Future has an error, we display the error message. If the Future is incomplete, we display a CircularProgressIndicator widget.

By using async / await with long-running operations, we can keep our Flutter applications responsive and efficient, even when performing complex calculations or heavy I/O operations. We can also use widgets like FutureBuilder to display the results of asynchronous operations to the user in a clean and easy-to-understand way.

Using async/await with streams

In Flutter, streams are used to represent a sequence of asynchronous events. For example, streams can be used to handle user input or data from a server that is received over time. In this section, we’ll explore how to use async / await with streams to handle these events.

To use async / await with streams, we can convert a stream into a Future using the firstWhere() method. The firstWhere() method returns the first element of the stream that satisfies a given condition. We can use this method to wait for a specific event to occur in the stream.

Here’s an example of how to use async / await with a stream of user input events:

import 'dart:async';
import 'package:flutter/material.dart';

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final _controller = StreamController<String>();

  @override
  void dispose() {
    _controller.close();
    super.dispose();
  }

  Future<void> _handleInput() async {
    final input = await _controller.stream.firstWhere(
      (event) => event.isNotEmpty,
      orElse: () => null,
    );
    print('Input: $input');
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          onChanged: (value) => _controller.add(value),
        ),
        ElevatedButton(
          onPressed: _handleInput,
          child: Text('Submit'),
        ),
      ],
    );
  }
}

In this example, we define a MyWidget class that contains a stream controller to handle user input. We use the firstWhere() method to wait for the user to input something into the TextField widget. The firstWhere() method takes a condition as its first argument and an orElse callback as its second argument, which is called if the condition is never satisfied. In our example, the condition checks if the input is not empty. If the user inputs something, we print the input to the console.

We can also use async / await with streams that receive data over time, such as data from a server. Here’s an example of how to use async / await with a stream of data from a server:

import 'dart:async';
import 'package:http/http.dart' as http;

Future<List<String>> fetchData() async {
  final response = await http.get(Uri.parse('https://example.com/data'));
  final data = response.body.split('\n');
  return data;
}

Future<void> handleData() async {
  final data = await fetchData();
  for (final item in data) {
    print(item);
  }
}

In this example, we define a fetchData() function that retrieves data from a server using the http package. We split the response body into a list of strings using the split() method and return it as a Future. We then define a handleData() function that uses async / await to wait for the data to be returned from the server. Once the data is returned, we loop through each item in the list and print it to the console.

By using async / await with streams, we can handle asynchronous events in a clean and easy-to-understand way. We can wait for specific events to occur in a stream using the firstWhere() method, and handle data that is received over time using a combination of async / await and stream processing.

Best practices for using async/await

Using async / await can make your code more readable and easier to reason about, but it’s important to follow some best practices to ensure that your code is efficient and works as expected. Here are some best practices to keep in mind when using async / await in Flutter:

  1. Avoid using async / await unnecessarily

While async / await can simplify asynchronous code, it’s important to only use it when necessary. If you have a simple operation that doesn’t require any asynchronous processing, there’s no need to use async / await. Using async / await when it’s not necessary can introduce unnecessary overhead and reduce the performance of your app.

  1. Use try-catch blocks to handle errors

When using async / await, it’s important to handle errors properly. To handle errors, wrap your asynchronous code in a try-catch block. This will ensure that any exceptions thrown during execution are caught and handled gracefully.

Future<void> fetchData() async {
  try {
    final response = await http.get(Uri.parse('https://example.com/data'));
    final data = response.body.split('\n');
    print(data);
  } catch (e) {
    print('Error fetching data: $e');
  }
}

In this example, we use a try-catch block to handle any errors that may occur during the data fetching process. If an error occurs, we print an error message to the console.

  1. Use the await keyword sparingly

While it’s tempting to use the await keyword everywhere, doing so can result in slower performance. Whenever you use the await keyword, your code will block until the operation completes. If you have a long chain of asynchronous operations, it’s better to use a combination of then() and catchError() methods to handle the results.

Future<void> fetchData() async {
  http.get(Uri.parse('https://example.com/data'))
    .then((response) => response.body.split('\n'))
    .then((data) => print(data))
    .catchError((e) => print('Error fetching data: $e'));
}

In this example, we use the then() method to handle the results of each asynchronous operation, and the catchError() method to handle any errors that may occur. This code will execute faster than using await because it doesn’t block the execution of the code.

  1. Use the FutureBuilder widget for asynchronous operations in UI

When performing asynchronous operations in a UI, it’s important to use the FutureBuilder widget. The FutureBuilder widget is designed to handle asynchronous operations in a UI and ensures that your app remains responsive while the operation is executing.

class MyWidget extends StatelessWidget {
  Future<List<String>> fetchData() async {
    final response = await http.get(Uri.parse('https://example.com/data'));
    final data = response.body.split('\n');
    return data;
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<String>>(
      future: fetchData(),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return ListView(
            children: snapshot.data.map((item) => Text(item)).toList(),
          );
        } else if (snapshot.hasError) {
          return Text('Error fetching data: ${snapshot.error}');
        }
        return CircularProgressIndicator();
      },
    );
  }
}

In this example, we define a MyWidget class that uses the FutureBuilder widget to handle the results of an asynchronous data fetching operation. The FutureBuilder widget takes a Future as its first argument and a builder function as its second argument. The builder function is called when the Future completes and is responsible for building the UI based on the state of the Future. If the Future completes successfully, the builder function builds a ListView widget using the data returned by the Future. If the Future fails with an error, the builder function displays an error message. If the Future is still in progress, the builder function displays a progress indicator.

  1. Use the async keyword with caution

The async keyword can be used to define asynchronous functions, but it’s important to use it with caution. When you mark a function as async, it returns a Future object, which means that other parts of your code can execute before the function completes. This can cause unexpected behavior if you’re not careful. As a best practice, only use the async keyword when you need to perform asynchronous processing within a function.

  1. Avoid mixing sync and async code

When working with async / await, it’s important to avoid mixing synchronous and asynchronous code. If you mix sync and async code, it can lead to unpredictable behavior and can make your code harder to read and understand. As a best practice, keep your synchronous and asynchronous code separate.

By following these best practices, you can ensure that your code is efficient, reliable, and easy to read and understand. Async / await is a powerful feature of the Dart language, and when used properly, it can make your code more robust and maintainable.

Examples of using async/await

  1. Fetching data from an API

One common use case for async / await is when you need to fetch data from an API. Here’s an example of how you can use the http package to fetch data from a REST API:

Future<List<User>> getUsers() async {
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
  if (response.statusCode == 200) {
    final jsonList = json.decode(response.body) as List<dynamic>;
    return jsonList.map((json) => User.fromJson(json)).toList();
  } else {
    throw Exception('Failed to load users');
  }
}

In this example, the getUsers() function returns a Future that resolves to a list of User objects. The await keyword is used to wait for the http.get() method to complete before continuing with the rest of the function. If the HTTP request is successful (i.e. the status code is 200), the response body is parsed as a JSON list, which is then mapped to a list of User objects. If the request fails, an exception is thrown.

  1. Updating the UI after a long-running operation

Another common use case for async / await is when you need to update the UI after a long-running operation. Here’s an example of how you can use async / await to run a long-running operation in the background, while still allowing the user to interact with the app:

class ExampleScreen extends StatefulWidget {
  @override
  _ExampleScreenState createState() => _ExampleScreenState();
}

class _ExampleScreenState extends State<ExampleScreen> {
  bool _isProcessing = false;

  Future<void> _startLongRunningOperation() async {
    setState(() {
      _isProcessing = true;
    });
    await Future.delayed(Duration(seconds: 5));
    setState(() {
      _isProcessing = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Example Screen'),
      ),
      body: Center(
        child: _isProcessing
            ? CircularProgressIndicator()
            : ElevatedButton(
                onPressed: _startLongRunningOperation,
                child: Text('Start Long-Running Operation'),
              ),
      ),
    );
  }
}

In this example, the _startLongRunningOperation() method sets the _isProcessing state to true, which displays a progress indicator on the screen. It then uses the await keyword to wait for a Future.delayed() call to complete before setting the _isProcessing state back to false, which removes the progress indicator from the screen.

  1. Parsing and processing large amounts of data

Finally, async / await can be useful when you need to parse and process large amounts of data. Here’s an example of how you can use the async / await syntax to parse and process a large CSV file:

Future<List<String>> parseCsvFile(String filePath) async {
  final file = File(filePath);
  final contents = await file.readAsString();
  final lines = LineSplitter().convert(contents);
  final data = <String>[];
  for (final line in lines) {
    final values = line.split(',');
    for (final value in values) {
      data.add(value.trim());
    }
  }
  return data;
}

In this example, the parseCsvFile() function reads the contents of a CSV file, splits it into lines, and then splits each line into values. The await keyword is used to wait for the file contents to be read before continuing with the rest of the function. The parsed values are added to a list, which is then returned as the result of the function.

  1. Using async / await with animations

Async / await can also be useful when working with animations, which often involve asynchronous operations such as waiting for a timer to complete or waiting for a widget to finish building. Here’s an example of how you can use async / await to create a simple animation:

class ExampleAnimation extends StatefulWidget {
  @override
  _ExampleAnimationState createState() => _ExampleAnimationState();
}

class _ExampleAnimationState extends State<ExampleAnimation>
    with TickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    );
    _animation = Tween<double>(begin: 0, end: 1).animate(_controller);
  }

  Future<void> _startAnimation() async {
    await _controller.forward();
    await _controller.reverse();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Example Animation'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ScaleTransition(
              scale: _animation,
              child: Container(
                width: 200,
                height: 200,
                color: Colors.blue,
              ),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _startAnimation,
              child: Text('Start Animation'),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

In this example, the _startAnimation() method uses the await keyword to wait for the animation to play forward and then reverse before continuing with the rest of the function. This creates a simple animation that scales a blue container up and down over a 2 second period.

These are just a few examples of how async / await can be used in Flutter. By using async / await effectively, you can write code that is more efficient, easier to read, and more responsive to user interactions.