) Dart asynchronous programming: futures,async,await

Posted by jbog91 on Thu, 03 Feb 2022 11:33:02 +0100

The article covers the following knowledge points:

  • How and when to use async and await keywords.
  • How to use async and await to affect the execution order.
  • How to use try catch to handle asynchronous call exceptions.
1, Why is asynchronous code important

Asynchronous operations allow programs to complete work while waiting for another operation to complete. Here are some common asynchronous operations:

  • Obtain data through the network.
  • Write to database.
  • Read data from file.

To perform asynchronous operations in Dart, you can use the Future class and the async and await keywords.

Look at a piece of problematic Code:

Future<String> fetchUserOrder() =>
    // Imagine that this function is more complex and slow.
    Future.delayed(
      const Duration(seconds: 2),
      () => 'Large Latte',
    );
String createOrderMessage() {
  var order = fetchUserOrder();
  return 'Your order is: $order';
}
void main() {
  print(createOrderMessage());
}
Your order is: Instance of 'Future<String>'

The following is the reason why the example cannot print the value finally generated by fetchUserOrder():

  • fetchUserOrder() is an asynchronous function that provides a string describing the user's order after a delay: "Large Latte".
  • To get the user's order, createordermessage () should call fetchuserorder () and wait for it to complete. Because createOrderMessage() does not wait for fetchUserOrder() to complete, createOrderMessage() cannot get the string value finally provided by fetchUserOrder().
  • Instead, createOrderMessage() gets a representation of the work to be done: the unfinished future.
  • Because createOrderMessage() cannot get the value describing the user's order, the example cannot print "Large Latte" to the console, but "Your order is: Instance of 'future < string >'.

Future.delayed() is a factory named constructor. The function prototype is as follows:
Future<T>.delayed(Duration duration, [FutureOr<T> computation()?])
Create a future and run its optional parameter function calculation after delaying the duration.
1. The calculation will be executed after the given duration, and the future will be completed with the generation of the calculation result.
2. If calculation returns future, the future returned by this constructor will contain the value or error of the future.
3. If the duration time is 0 or less, it will be completed in the next event loop iteration after all micro tasks run.
4. If the optional parameter compuaion is omitted, it will be regarded as calculation () = > null, and the future will be finally completed with null value. In this case, T must be nullable.
5. If the call to calculation throws an exception, the created future will be completed with an error.
---------------—
FutureOr<T> class:
A type that represents the value of future < T > or t.
Such a declaration is a public alternative to the internal [future Or value] generic type, which is not a class type. A reference to this class resolves to an internal type.
---------------—
Duration class:
A Duration represents a difference from one point in time to another. The duration may be "negative" if the difference is from a later time to an earlier.
To create a new Duration object, use this class's single constructor giving the appropriate arguments:
var fastestMarathon = const Duration(hours: 2, minutes: 3, seconds: 2);
The Duration is the sum of all individual parts. This means that individual parts can be larger than the next-bigger unit. For example, inMinutes can be greater than 59.
assert(fastestMarathon.inMinutes == 123);
var aLongWeekend = const Duration(hours: 88);
assert(aLongWeekend.inDays == 3);
Constructor:
const Duration({int days = 0, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0})

Future. The specific underlying implementation code of delayed() is as follows:

factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {
  if (computation == null && !typeAcceptsNull<T>()) {
    throw ArgumentError.value(
        null, "computation", "The type parameter is not nullable");
  }
  _Future<T> result = new _Future<T>();
  new Timer(duration, () {// Use the Timer class to complete the delay logic
    if (computation == null) {
      result._complete(null as T);
    } else {
      try {
        result._complete(computation());
      } catch (e, s) {
        _completeWithErrorCallback(result, e, s);
      }
    }
  });
  return result;
}

Key terms:

  • Synchronous operation: synchronous operation prevents other operations from executing until they are completed.
  • Synchronization function: the synchronization function only performs synchronization operations.
  • Asynchronous operation: once started, asynchronous operation allows other operations to be performed before completion.
  • Asynchronous function: an asynchronous function can perform at least one asynchronous operation or synchronous operation.
2, What is the future?

A future (lowercase "F") is an instance of the future (uppercase "F") class. Future indicates the result of asynchronous operation. There can be two states: incomplete or completed.

Note: incomplete is a Dart term, which refers to the state before the value of future is generated.

Incomplete and completed

Unfinished: when you call an asynchronous function, it returns an unfinished future. This future will wait for the asynchronous operation of the function to complete or throw an error. (in the above problematic example code, an unfinished future is returned directly)

Completed: if the asynchronous operation is successful, future will complete with a value. Otherwise, it will complete with an error.

Complete with value: complete with value. Future < T > type will be completed with value of T type. For example, future of type future < string > will generate string values. If future does not produce available values, the type of future is future < void >.

Complete with error: if the asynchronous operation performed by the function fails for any reason, future will complete with error.

In the following example, fetchuserorder () returns a future, which is completed after the print console is completed. Because it does not return available values, the type of fetchUserOrder() is future < void >.

Future<void> fetchUserOrder() {
  // Imagine that this function is fetching user info from another service or database.
  return Future.delayed(const Duration(seconds: 2), () => print('Large Latte'));
}
void main() {
  fetchUserOrder();
  print('Fetching user order...');
}
Fetching user order...
Large Latte// Print in 2 seconds

How a future is completed with an error, as shown in the following code:

Future<void> fetchUserOrder() {
	return Future.delayed(const Duration(seconds: 2),
						  () => throw Exception('Logout failed: user ID is invalid'));
}
main() {
	fetchUserOrder();
	print('Fetching user order...');
}
Fetching user order...
Uncaught Error: Exception: Logout failed: user ID is invalid// Print in 2 seconds

Quick review:

  • The future < T > instance generates a value of type T.
  • If future does not produce available values, the type of future is future < void >.
  • future can be in one of two states: incomplete or completed.
  • When you call a function that returns future, the function will queue the work to be completed and return an unfinished future.
  • When the operation of future is completed, future will complete with a value or an error.
     

Key terms:

  • Future: the future class of Dart.
  • Future: an instance of the future class.
3, Using future: async and await

When using async and await, keep two basic guidelines in mind:

  1. Using asynchronous functions, add async keyword before the function body;
  2. The await keyword works only in asynchronous functions.
    If you have an async function, you can use the await keyword to wait for a future to complete.
    Example 1: synchronization function
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>'// Don't wait, print directly

Example 2: asynchronous function

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// After 2 seconds

If other codes remain unchanged, change the main() method to:

void main() {
	print('Fetching user order...');
	print(createOrderMessage());
}

Print:

Fetching user order...// Don't wait, print directly
Instance of 'Future<String>'

There are three differences between the above two examples:

  1. The return type of the createOrderMessage() function changes from String to future < String >.
  2. The async keyword appears before the bodies of the createOrderMessage() and main() methods.
  3. The await keyword appears before calling the asynchronous functions fetchUserOrder() and createOrderMessage() methods.

Key terms:
Async: you can use the async keyword in front of a function body to mark the function as asynchronous.
async function: an asynchronous function is a function marked by async keyword.
Await: you can use the await keyword to get the completion result of an asynchronous expression. The await keyword only works in async functions.

Execution process of async and await

An asynchronous function executes synchronously until the first await keyword.

Future<void> printOrderMessage() async {
  print('Awaiting user order...');
  var order = await fetchUserOrder();
  print('future completed!');
  print('Your order is: $order');
}
Future<String> fetchUserOrder() {
  // Imagine that this function is more complex and slow.
  return Future.delayed(const Duration(seconds: 4), () => 'Large Latte');
}
Future<void> main() async {
  countSeconds(4);
  await printOrderMessage();
}
// You can ignore this function - it's here to visualize delay time in this example.
void countSeconds(int s) {
  for (var i = 1; i <= s; i++) {
    Future.delayed(Duration(seconds: i), () => print(i));
  }
}
Awaiting user order...
1// After 1 second
2// After 2 seconds
3// After 3 seconds
4// After 4 seconds
future completed!
Your order is: Large Latte
4, Handling exceptions
Future<void> printOrderMessage() async {
  try {
    print('Awaiting user order...');
    var order = await fetchUserOrder();
    print(order);
  } catch (err) {
    print('Caught error: $err');
  }
}
Future<String> fetchUserOrder() {
  // Imagine that this function is more complex.
  var str = Future.delayed(
      const Duration(seconds: 4), () => throw 'Cannot locate user order');
  return str;
}
Future<void> main() async {
  await printOrderMessage();
}
Awaiting user order...
Caught error: Cannot locate user order// After 4 seconds
5, Exercise: soften all together
String addHello(user) => 'Hello $user';

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

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

Stream tutorial : learn how to use asynchronous event sequences.

Topics: dart