Async in Flutter - Advanced Futures API

7 min read, 31 July, 2018

This article is the last one related to using standard Future API in Flutter. In this article, we'll discuss advanced usage of Futures, including static methods Future.any, Future.doWhile, Future.forEach and Future.wait. Methods discussed in this article are useful when working with multiple Futures.

Future.any

Future.any accepts an Iterable of Futures and return a new Future which completes with the result of the first completed Future, whether it's completed with a value or an error. If the passed-in Iterable of Futures is empty, or none of these Futures complete, the returned Future never completes.

In the code below, three Futures are created using Future.delayed with different delays. The Future with the shortest delay completes the returned Future. The output is 1.

import 'dart:async';

void main() {
  Future
      .any([1, 2, 5].map(
          (delay) => new Future.delayed(new Duration(seconds: delay), () => delay)))
      .then(print)
      .catchError(print);
}

When to use it

Frankly speaking, I don't see many good scenarios when Future.any can be used. The good part is that the returned Future of Future.any completes with result of the first completed Future, while the bad part is a Future completed with an error will also completes the returned Future. What we may want is that the returned Future should be completed with result of the first succeed Future and fail only when all Futures complete with errors. For example, we can send requests to three different endpoints to get the same data, and use the one comes back first. The operation should only fail when none of these requests complete. Unfortunately, Future.any cannot help in this case.

The behaviour of Future.any is the same as Promise.race in ECMAScript 6. Bluebird has the API Promise.any with the desired behaviour that a rejected Promise won't resolve the result Promise.

Future.doWhile

Future.doWhile accepts an operation which returns a bool value or a Future<bool>. The operation is called repeatedly until it returns false or a Future completes with the value false. Future.doWhile returns a new Future which:

  • completes with the value null - when the operation returns false or a Future that completes with false.
  • completes with an error - when the operation throws or it returned Future<bool> completes with an error.

The call to next operation is guaranteed to happen after the call to previous operation is returned or the returned Future of previous operation is completed.

The code below makes sure the returned Future of Future.doWhile is delayed at least 10 seconds. The operation returns a new Future that delays a random number of seconds.

import 'dart:async';
import 'dart:math';

void main() {
  var random = new Random();
  var totalDelay = 0;
  Future
      .doWhile(() {
        if (totalDelay > 10) {
          print('total delay: $totalDelay seconds');
          return false;
        }
        var delay = random.nextInt(5) + 1;
        totalDelay += delay;
        return new Future.delayed(new Duration(seconds: delay), () {
          print('waited $delay seconds');
          return true;
        });
      })
      .then(print)
      .catchError(print);
}

The output of the code above looks like below. The last null is the completed value the returned Future.

waited 5 seconds
waited 2 seconds
waited 4 seconds
total delay: 11 seconds
null

When to use it

Future.doWhile can be used to handle Iterables and recursive data structures, e.g. trees. For Iterables, we may not need to iterate all the elements. Future.doWhile can be used to invoke asynchronous operations for each element and determine when to exit the iteration. For recursive data structures, we can also use asynchronous operations to determine when to exit the recursion.

Future.forEach

Future.forEach accepts an Iterable and an operation to call on each element of the Iterable. The operation can return a simple value or a Future. Operations are called on each element in order. If the operation returns a Future, the next operation is called after the Future is completed. Future.forEach returns a new Future which:

  • completes with the value null - when all elements has been processed.
  • completes with an error - when any operation on an element throws or its returned Future completes with an error.

Future.forEach is actually implemented using Future.doWhile.

In the code below, the Iterable is [1, 2, 5]. For each element, a new Future is created to print out the value after a delay.

import 'dart:async';

void main() {
  Future
      .forEach(
          [1, 2, 5],
          (delay) =>
              new Future.delayed(new Duration(seconds: delay), () => delay)
                  .then(print))
      .then(print)
      .catchError(print);
}

When to use it

Future.forEach is useful when you want to run asynchronous operations on elements in order. Unfortunately, it may not be very useful in practice. Future.forEach discards non-Future return values and completion values of Futures. The completion value of the returned Future of Future.forEach is null. So it cannot be use directly to collect values from multiple Futures. Future.forEach is mainly used to perform actions with side-effects. However, if an error occurs in the operation on an element and causes the returned Future to complete with that error, there is no way to know which element caused the error, so there is no easy way to undo those performed actions or retry starting from the element caused the error.

Future.wait

Future.wait is the most useful methods provided by Future. It waits for multiple Futures to complete and collects the results. It has three parameters:

  • Iterable<Future<T>> futures - An Iterable of Futures to wait for completion.
  • bool eagerError - When true, the returned Future is completed immediately with the first error. Otherwise, the returned Future waits for all Futures to complete. In this case, the returned Future is completed with the first error, all other errors are silently dropped. The default value is false.
  • void cleanUp(T successValue) - In case of error, cleanup is invoked on any non-null value of successful Futures. This is useful to clean up resources or rollback changes.

The returned Future of Future.wait:

  • completes with the list of all completion values - when all Futures has been completed successfully.
  • completes with the first error - when any Future completes with an error.

In the code below, future2 completes with an error. eagerError is set to true, so the returned Future completes when future2 completes. The cleanUp function prints out the successful values of future1 and future3.

import 'dart:async';

void main() {
  var future1 = new Future.delayed(new Duration(seconds: 1), () => 1);
  var future2 = new Future.delayed(new Duration(seconds: 2), () => throw 'error');
  var future3 = new Future.delayed(new Duration(seconds: 3), () => 3);
  Future
      .wait([future1, future2, future3], eagerError: true, cleanUp: (value) {
        print('processed $value');
      })
      .then(print)
      .catchError(print);
}

Below is the output of the code above.

processed 1
error
processed 3

When to use it

Future.wait should be used in most cases when working with multiple Futures. We can use it to collect results of asynchronous operations without side-effects. For operations with side-effects, we can use cleanUp function to rollback changes.

Summary

This article discusses usage of static methods in Future, including Future.any, Future.doWhile, Future.forEach and Future.wait. Future.wait should be used in most cases. The functionality provided by standard Future API is very limited and cannot satisfy most real-world requirements. In JavaScript, we can use libraries like Bluebird to work with Promises. You can create your own helper methods with functionality matching what's provided by Bluebird. A quick search points me to the library future_goodies with a bunch of helper methods. Unfortunately, this library doesn't seem to be under active maintenance.