Asynchronous programming with Dart
Learning objectives
- You know the basics of asynchronous programming with Dart.
Heavy calculations or retrieving data over the network can take plenty of time to finish. Such time-consuming operations are meaningful to execute when there is time for executing them.
Programming languages often support both synchronous and asynchronous operations. For synchronous operations, the program execution waits for them to finish, while for asynchronous operations, the program execution proceeds to the next operation, adding the asynchronous operations into a queue that is processed at some point. The following example outlines the difference between synchronous and asynchronous code.
When the above program is run, at first, two Hello!
printouts appear in the console. After 5 seconds, we see a printout Hello again!
. The Future class is used to demonstrate asynchronous programming, where the Future.delayed is used to create a function call that is executed after a given delay. The delay above is 5 seconds, defined using the Duration class.
In terms of the program behavior, the difference between synchronous and asynchronous functions is that when a synchronous function is called from program code, the execution of the code (where the function is called from) waits until the synchronous function has finished execution. For asynchronous functions, the execution of code (where the asynchronous function is called from) does not need to wait.
Defining asynchronous functions
Asynchronous functions are defined using the async
keyword that is placed after the parentheses (and potential arguments) of a function definition. The following is an example of an asynchronous function with two parameters that waits for a given number of seconds and then prints a given message.
void waitAndPrint(String message, int seconds) async {
Future.delayed(Duration(seconds: seconds), () => print(message));
}
In the following example, we call the asynchronous function three times.
When we run the program, the program outputs first Hello 3
, then Hello 2
, and finally Hello 1
. In practice, all of the functions are executed nearly simultaneously, which leads to a situation where the total time that it takes to execute the program is approximately three seconds.
Returning a Future
Asynchronous functions may return an instance of the Future class, which represents a result of an asynchronous operation. The function Future.delayed returns a response. In the case of a function that simply prints something, the returned value is a void
.
In the following example, we have adjusted the waitAndPrint
function so that it returns an instance of Future<void>
.
When we run the program, the output is again the same as in the previous case. The program first outputs Hello 3
, then Hello 2
, and finally Hello 1
.
Waiting for an asynchronous function
Waiting for the execution of an asynchronous function is done using the await
keyword. The await
keyword can only be used in asynchronous functions, i.e. functions that have async
in the function definition. In the following example, we have adjusted the main function to be asynchronous by adding the keyword async
to the function definition, and have added the await
keyword to the first waitAndPrint
call.
Now, when we run the program, the program first waits for the execution of the call waitAndPrint("Hello 1", 3)
, after which the subsequent function calls happen.
The program first prints Hello 1
, followed by Hello 3
, and Hello 2
. In total, the program takes approximately 5 seconds to execute.
Waiting in an asyncronous function
Naturally, we can also wait for the execution of asynchronous function calls within asynchronous functions. In the following example, the await
call is placed within the asynchronous waitAndPrint
function, which is again called from the main
function.
Now, the function does not return a Future
, but a void.
When we run the program, the output is again familiar. The program first outputs Hello 3
, then Hello 2
, and finally Hello 1
. The execution time of the program is again approximately three seconds. This happens because waiting for the execution of the Future.delayed
-call happens within the asynchronous function, while the main
function does not wait for the execution of individual waitAndPrint
-calls.
While the above example returns the value resulting from the last await
command, one can also use await
within asynchronous functions and return a Future
. The following example is a small modification to the prior examples, where the function waitAndPrint
waits for an additional second.
When running the above program, after approximately one second, the message Waited enough.
is printed three times. After another second, the message Hello 3
is printed. This is followed by printing Hello 2
and Hello 1
. Altogether, the program takes approximately 4 seconds to complete.
Returning a value in Future
Asynchronous functions can return values. As seen previously, returned values are encapsulated using the Future class that represents the result of an asynchronous operation.
In the following example, we define an asynchronous function called heavyTask
that takes a String as a parameter and returns the String with Done:
-suffix after 3 seconds. As the operation is asynchronous, the return type of the function is Future<String>
, meaning that the result of the asynchronous operation function will be a string. In the example, we see that we can assign the returned value to variable in the main
function, which is not asynchronous.
In the main
function above, we have stored the result of the asynchronous operation into the variable calc1
.
Accessing a value in a Future
To access a value in a Future, we need wait for the task to complete -- to achieve this, we can adjust the main
function so that it is asynchronous and use the keyword await
to wait for the asynchronous operation to complete, as shown below.
In the above example, we also store the resulting value in a new variable called result
and print it.
Another approach, which does not require changing the main
function into an asynchronous function, is to rely on callback methods provided by the Future class. Here, the method then is particularly useful.
The following example demonstrates the use of then
.
Async/await
The async/await
syntax described above allows structuring asynchronous code in a similar way to synchronous code. The syntax first appreared in C#
around 2011 and 2012, and has been since adopted by a wide variety of programming languages, including Dart, Rust, TypeScript, JavaScript, and Python.