Futures can be seen everywhere in Dart asynchronous programming, such as when a network request returns a Future object, or when accessing a SharedPreferences returns a Future object, and so on. Asynchronous operations allow your program to continue processing other work while waiting for one operation to complete. Dart uses Future objects to represent the results of asynchronous operations. As we know from previous articles, Dart is a single-threaded language, so delayed computations need to be implemented asynchronously, so the Future represents the asynchronous return.

1. Review some concepts

1.1 EventLoop

Dart’s EventLoop is similar to Javascript in that it has two FIFO queues: an Event Queue and a MicroTask Queue.

  • The Event Queue consists of IO, gesture, draw, Timer, Stream, and Future
  • MicroTask QueueMainly consists of microtasks (very short internal operations) within the Dart, typically throughscheduleMicroTaskMethod, which has a higher priority than the Event Queue

1.2 Process of event cycle execution

  • 1. Initialize two queues, namely event queue and microtask queue
  • 2, performmainmethods
  • 3. Start EventLoop

Note: The Event queue is blocked while the event loop is processing microTasks.

2. Why a Future

Dart is known to be a single-threaded model language, and if you need to perform some delayed operations or IO operations, the default single-line synchronized mode may cause the main ISOLATE to block, resulting in render lag. With that in mind, we need to implement a single-threaded asynchronous approach based on Dart’s built-in non-blocking API, in which Future plays an important role. Future represents the result returned asynchronously. When an asynchronous delayed calculation is performed, a Future result is returned first, and subsequent code can continue to execute without blocking the main ISOLATE. When the calculation results in the Future arrive, if the then callback is registered, You get the final calculated value for a successful callback, and an exception message for a failed callback.

Some people may be a little confused, but why do you need a Future? As mentioned in the previous article, futures are much more lightweight than isolates, and creating too many isolates is also very expensive for system resources. If you are familiar with the SOURCE code of THE ISOLATE, you know that the ISOLATE maps to the OSThread of the operating system. Therefore, Future is recommended to replace ISOLATE for those with low latency.

Dart async and await are async and await are async and await are async and await are async. The biggest advantage of Future over Async and await is that it provides powerful chain calls. Chain calls have the advantage of making clear the dependencies before and after code execution and catching exceptions. Let’s take a common example. For example, when we need to request book details, we need to get the corresponding book ID first, that is, two requests have a dependency relationship. In this case, if we use async,await may not be as flexible as future.

  • With async and await:
_fetchBookId() async {
  //request book id
}

_fetchBookDetail() async {
  // We need to await bookId inside _fetchBookDetail, so to execute _fetchBookId first and then _fetchBookDetail,
  // we must await _fetchBookId() in _fetchBookDetail, so there is a problem that _fetchBookDetail internally couples _fetchBookId
  // Once the _fetchBookId is changed, the _fetchBookDetail should be changed accordingly
  var bookId = await _fetchBookId();
  //get bookId then request book detail
}

void main() async {
   var bookDetail = await _fetchBookDetail();// Finally request _fetchBookDetail in the main function
}

// There is also exception catching
_fetchDataA() async {
  try {
    //request data
  } on Exception{
    // do sth
  } finally {
    // do sth
  }
}

_fetchDataB() async {
  try {
    //request data
  } on Exception{
    // do sth
  } finally {
    // do sth}}void main() async {
  // Prevent exception crashes by adding try-catch traps inside each method
  var resultA = await _fetchDataA();
  var resultB = await _fetchDataB();
}
Copy the code
  • The realization of the Future
_fetchBookId() {
  //request book id
}

_fetchBookDetail() {
  //get bookId then request book detail
}

void main() {
   Future(_fetchBookId()).then((bookId) => _fetchBookDetail());
  // Or the following way
   Future(_fetchBookId()).then((bookId) => Future(_fetchBookDetail()));
}

Catch the implementation of the exception
_fetchBookId() {
  //request book id
}

_fetchBookDetail() {
  //get bookId then request book detail
}

void main() {
  Future(_fetchBookId())
      .catchError((e) => '_fetchBookId is error $e')
      .then((bookId) => _fetchBookDetail())
      .catchError((e) => '_fetchBookDetail is error $e');
}

Copy the code

To summarize, there are three reasons why Future is needed:

  • In the SINGLE-threaded Dart model, futures are an integral part of Dart asynchrony as the return result of asynchrony.
  • In most Dart or Flutter business scenarios, Future is much lighter and more efficient than ISOLATE in implementing asynchronism.
  • In some special cases, Future has an advantage over Async and await over chained calls.

3. What is Future

3.1 Official Description

In technical terms, a future is an object of class Future

, which represents the result of an asynchronous operation of type T. If the asynchronous operation does not require the result, the future can be of type Future

. When a function that returns a future object is called, two things happen:

  • Queues function operations for execution and returns an incompleteFutureObject.
  • Later, when the function operation is complete,FutureThe object becomes complete and carries either a value or an error.

3.2 Personal Understanding

A Future can be thought of as a “box” of data. An asynchronous request will return a Future “box” at first, and then proceed with the rest of the code. When the result of the asynchronous request is returned a few moments later, the Future “box” opens and contains either the value of the request result or the request exception. The Future will have three states: an Uncompleted state, with the “box” closed; Completed with a value, the box opened and returned the result status normally; Completed with an error, the “box” opened and failed to return the exception state;

The following uses a Flutter example to understand the process of the Future in combination with EventLoop. Here is a button that will request a web image when clicked, and then display the image.

RaisedButton(
  onPressed: () {
    final myFuture = http.get('https://my.image.url');// Return a Future object
    myFuture.then((resp) {// Register then events
      setImage(resp);
    });
  },
  child: Text('Click me! '),Copy the code
  • First, when you clickRaisedButton“, will pass oneTapEvent to the Event Queue (because the system gesture belongs to the Event Queue), andTapEvents are passed to the EventLoop for processing.

  • The event loop then processes itTapThe event will eventually trigger executiononPressedMethod, which makes a network request using the HTTP library and returns a Future. You get the data “box”, but its state is closed (uncompleted). And then usethenRegister for callbacks when the data “box” is opened. At this timeonPressedThe method is done, and then it’s just waiting, waiting for the image data to come back from the HTTP request, while the whole event loop is running around processing other events.

  • Eventually, the HTTP requested image data arrives, and Future will load the actual image data into the “box” and open the box and register itthenThe callback method is triggered to retrieve the image data and display the image.

4. Future state

There are three states of the Future: an Uncompleted state, in which the box is closed; Completed with a value, the box opened and returned the result status normally; Upon completing the state with an error, the box opens and returns the exception status on failure.

There are actually five states in the Future source code:

  • __stateIncomplete: _ Initial incomplete state, waiting for a result
  • The __statePendingComplete: _Pending state indicates that the Future object is still being evaluated and no result is available.
  • __stateChained: _ Link state (usually occurs when the current Future is linked to another Future and the result of the other Future becomes the result of the current Future)
  • __stateValue: _ Completes the state with a value
  • __stateError: _ Completes the state with an exception
class _Future<T> implements Future<T> {
  /// Initial state, waiting for a result. In this state, the
  /// [resultOrListeners] field holds a single-linked list of
  /// [_FutureListener] listeners.
  static const int _stateIncomplete = 0;

  /// Pending completion. Set when completed using [_asyncComplete] or
  /// [_asyncCompleteError]. It is an error to try to complete it again.
  /// [resultOrListeners] holds listeners.
  static const int _statePendingComplete = 1;

  /// The future has been chained to another future. The result of that
  /// other future becomes the result of this future as well.
  /// [resultOrListeners] contains the source future.
  static const int _stateChained = 2;

  /// The future has been completed with a value result.
  static const int _stateValue = 4;

  /// The future has been completed with an error result.
  static const int _stateError = 8;

  /** Whether the future is complete, and as what. * /
  int_state = _stateIncomplete; . }Copy the code

5. How to use Future

5.1 Basic Use of Future

  • 1. factory Future(FutureOr computation())

The simple creation of a Future can be done through its constructor, by passing in an asynchronous execution Function.

//Future's factory constructor
factory Future(FutureOr<T> computation()) {
  _Future<T> result = new _Future<T>();
  Timer.run(() {// A Timer is created internally
    try {
      result._complete(computation());
    } catch(e, s) { _completeWithErrorCallback(result, e, s); }});return result;
}
Copy the code
void main() {
  print('main is executed start');
  var function = () {
    print('future is executed');
  };
  Future(function);
  print('main is executed end');
}
// Or pass an anonymous function directly
void main() {
  print('main is executed start');
  var future = Future(() {
    print('future is executed');
  });
  print('main is executed end');
}
Copy the code

Output result:You can see from the output that the Future output is an asynchronous process, sofuture is executedOutput inmain is executed endAfter output. This is because normal code in the main method is executed synchronously, so the main method is placed firstmain is executed start 和 main is executed endOutput, and wait until the main method is finishedMicroTaskIf a task exists in the queue, execute it until it is foundMicroTask QueueIs empty, then will go to checkEvent QueueSince the very nature of Future is to open one up internallyTimerImplement asynchrony, and eventually the asynchrony event is put intoEvent Queue, this time just check to the currentFutureThe Event Loop will handle thisFuture. So it finally printsfuture is executed .

  • 2. Future.value()

Create a Future object that returns the specified value. Note that the asynchron is actually implemented inside the value via scheduleMicrotask. As mentioned in the previous article, there are only two ways to implement asynchronous futures: using Timer and scheduleMicrotask

void main() {
  var commonFuture = Future((){
    print('future is executed');
  });
  var valueFuture = Future.value(100.0);//
  valueFuture.then((value) => print(value));
  print(valueFuture is Future<double>);
}
Copy the code

Output result:True is printed first because it was executed synchronouslyvalueFutureBut whycommonFutureTo perform thevalueFutureAnd after that, that’s becauseFuture.valueThe interior is actually throughscheduleMicrotaskImplement asynchronous, then it is not difficult to understand that the check will start after the main method has finished executingMicroTaskIs there a task in the queuevalueFutureJust do it until it’s checkedMicroTask QueueIs empty, then will go to checkEvent QueueSince the very nature of Future is to open one up internallyTimerImplement asynchrony, and eventually the asynchrony event is put intoEvent Queue, this time just check to the currentFutureThe Event Loop will handle thisFuture

Future.value:

  factory Future.value([FutureOr<T> value]) {
    return new _Future<T>.immediate(value);// The immediate method of _Future is actually called
  }

// Enter the immediate method
 _Future.immediate(FutureOr<T> result) : _zone = Zone.current {
    _asyncComplete(result);
  }
// Then execute the _asyncComplete method
 void _asyncComplete(FutureOr<T> value) {
    assert(! _isComplete);if (value is Future<T>) {// If value is a Future, link it to the current Future
      _chainFuture(value);
      return;
    }
    _setPendingComplete();// Set the PendingComplete state
    _zone.scheduleMicrotask(() {// The scheduleMicrotask method is called
      _completeWithValue(value);// Finally, the _completeWithValue method is called back to pass in the value
    });
  }
Copy the code
  • 3. Future.delayed()

Create a future that is deferred. Internally, a deferred asynchronous operation is implemented by creating a deferred Timer. Future.delayed The main two parameters are passed: Duration and Function, which will be executed asynchronously.

void main() {
  var delayedFuture = Future.delayed(Duration(seconds: 3), () {/ / delay 3 s
    print('this is delayed future');
  });
  print('main is executed, waiting a delayed output.... ');
}
Copy the code

The future.delayed method uses a delayed Timer implementation, which can be seen in the source code:

factory Future.delayed(Duration duration, [FutureOr<T> computation()]) {
    _Future<T> result = new _Future<T>();
    new Timer(duration, () {// Create a delayed Timer that is passed to the Event Queue and processed by EventLoop
      if (computation == null) {
        result._complete(null);
      } else {
        try {
          result._complete(computation());
        } catch(e, s) { _completeWithErrorCallback(result, e, s); }}});return result;
  }
Copy the code

5.2 Advanced use of Future

  • 1. Future forEach method

ForEach takes two arguments: an Iterable collection object and a Function method that takes Iterable elements as arguments

void main() {
  var futureList = Future.forEach([1.2.3.4.5], (int element){
    return Future.delayed(Duration(seconds: element), () => print('this is $element'));// This is 1 every 1s, this is 2 every 2s, this is 3 every 3s...
  });
}
Copy the code

Output result:

  • 2. Future’s any method

The Future’s any method returns the result of the first Future executed, whether it returns normally or returns an error.

void main() {
  var futureList = Future.any([3.4.1.2.5].map((delay) =>
          new Future.delayed(new Duration(seconds: delay), () => delay)))
      .then(print)
      .catchError(print);
}
Copy the code

Output result:

  • 3. Future’s doWhile method

The future. doWhile method executes an action repeatedly until it returns false or Future to exit the loop. Especially suitable for some recursive request subclasses of data.

void main() {
  var totalDelay = 0;
  var delay = 0;

  Future.doWhile(() {
    if (totalDelay > 10) {// Out of the loop after 10s
      print('total delay: $totalDelay s');
      return false;
    }
    delay += 1;
    totalDelay = totalDelay + delay;
    return new Future.delayed(new Duration(seconds: delay), () {
      print('wait $delay s');
      return true;
    });
  });
}
Copy the code

Output result:

  • 4. Future wait method

Used to wait for multiple futures to complete and integrate their results, somewhat similar to the ZIP operation in RxJava. There are two possible outcomes:

  • If all futures return normal results: the return result of the future is the set of results for all specified futures
  • If one future returns an error: the future returns the value of the first error
void main() {
  var requestApi1 = Future.delayed(Duration(seconds: 1), () = >15650);
  var requestApi2 = Future.delayed(Duration(seconds: 2), () = >2340);
  var requestApi3 = Future.delayed(Duration(seconds: 1), () = >130);

  Future.wait({requestApi1, requestApi2, requestApi3})
      .then((List<int> value) => {
        // Add up the results
        print('${value.reduce((value, element) => value + element)}')}); }Copy the code

Output result:

// Exception handling
void main() {
  var requestApi1 = Future.delayed(Duration(seconds: 1), () = >15650);
  var requestApi2 = Future.delayed(Duration(seconds: 2), () = >throw Exception('api2 is error'));
  var requestApi3 = Future.delayed(Duration(seconds: 1), () = >throw Exception('api3 is error'));Api3 is error. This is because API3 executes first, because API2 delays 2s

  Future.wait({requestApi1, requestApi2, requestApi3})
      .then((List<int> value) => {
        // Add up the results
        print('${value.reduce((value, element) => value + element)}')}); }Copy the code

Output result:

  • 5. Microtask methods for Future

We all know that futures typically add events to the Event Queue, but the Future.microtask method provides a way to add events to the MicroTask Queue, creating a Future that runs on the MicroTask Queue. As mentioned above, the MicroTask queue has a higher priority than the Event queue, and futures are usually executed on the Event queue, so futures created by MicroTask will be executed before other futures.

void main() {
  var commonFuture = Future(() {
    print('common future is executed');
  });
  var microtaskFuture = Future.microtask(() => print('microtask future is executed'));
}
Copy the code

Output result:

  • 6. Future sync method

The future. sync method returns a synchronous Future, but note that if the Future registers with then as an asynchronous Future, it will add the Future to the MicroTask Queue.

void main () {
  Future.sync(() = >print('sync is executed! '));
  print('main is executed! ');
}
Copy the code

Output result:If you use THEN to register the result of listening for a Future, then it is asynchronous and the Future is added to the MicroTask Queue.

void main() {
  Future.delayed(Duration(seconds: 1), () = >print('this is delayed future'));// Regular Future will be added to the Event Queue
  Future.sync(() = >100).then(print);// Sync's Future needs to be added to the MicroTask Queue
  print('main is executed! ');
}
Copy the code

5.3 Processing the result returned by the Future

  • 1. The Future. Then method

Futures typically use the then method to register Future callbacks. Note that the Future also returns a Future object, so you can use chained calls to use the Future. This makes it possible to take the output of the previous Future as the input of the next Future, which can be written as a chained call.

void main() {
  Future.delayed(Duration(seconds: 1), () = >100)
      .then((value) => Future.delayed(Duration(seconds: 1), () = >100 + value))
      .then((value) => Future.delayed(Duration(seconds: 1), () = >100 + value))
      .then((value) => Future.delayed(Duration(seconds: 1), () = >100 + value))
      .then(print);// The output summation is 400
}
Copy the code

Output result:

  • 2. Future. CatchError method

Register a callback to handle Future with exceptions

void main() {
  Future.delayed(Duration(seconds: 1), () = >throw Exception('this is custom error'))
      .catchError(print);The catchError callback returns the Future of the exception
}
Copy the code

Output result:

  • 3. The Future. WhenComplete method

The Future.whenComplete method is similar to finally in try-catch-finally in exception catching. A Future will eventually call whenComplete whether it calls the result normally or throws an exception.

//with value
void main() {
  Future.value(100)
      .then((value) => print(value))
      .whenComplete(() => print('future is completed! '));
  print('main is executed');
}

//with error
void main() {
  Future.delayed(
          Duration(seconds: 1), () = >throw Exception('this is custom error'))
      .catchError(print)
      .whenComplete(() => print('future is completed! '));
  print('main is executed');
}
Copy the code

Output result:

6. Scenarios used by Future

Based on the above introduction about Future, I believe that you should have a base of Future usage scenarios in mind. Here is a brief summary:

  • 1. Futurescan be used for scenarios that are generally asynchronous in Dart, especially when dealing with multiple futures-dependencies and aggregations.
  • 2. You can use async and await for processing individual futures in Dart scenarios that typically implement async
  • 3. For time-consuming tasks in Dart, it is not recommended to use Future and use ISOLATE instead.

7. Summary from Mr. Xiong Meow

This is the end of asynchronous programming with Future, which is much lighter and easier to implement asynchronously than ISOLATE. And the await method has an advantage over aysnc in chained calls. However, it is important to note that if you encounter time-consuming and heavy tasks, you are still advised to use ISOLATE, because the Future still runs in the main thread. One other thing to note is that you need to have a good understanding of EventLoop concepts such as Event Queue and priority of MicroTask Queue. Many of the advanced asynchronous apis behind Dart are based on event loops.

Thank you for your attention, Mr. Xiong Meow is willing to grow up with you on the technical road!