Asynchronous calls and Future objects

How should our app handle a situation where it needs a service that can take some time to return its result, for example, when we have to fetch or store data, read a large file, or need a response from a web service. Obviously, the app can't wait for this to end and the result to arrive (the so-called synchronous way), because this would freeze the screen (and the app) and frustrate users. A responsive web app must be able to call this service and immediately continue with what it was doing. Only when the result returns should it react and call some function on the response data. This is called working in an asynchronous, non-blocking way. Most of the methods in dart:io for server-side Dart apps work in this way.

Developers with a Java or C# background would perhaps think of starting another thread to do additional work, but Dart can't do it. Dart has to compile to JavaScript, so similar to JavaScript, it also works in a single-threaded model that is tightly-controlled by the browser's event loop. On the Web (client as well as server), the code has to execute as asynchronously as possible in order to not block the browser from serving its user or a server process from serving its many thousands of client requests. The JavaScript world has long solved this issue using callbacks; this is a function that is "called" when the result of the first function called returns ("backs"). In the following code snippet, the first function that will return the result is doStuff and handle is registered as a callback that will work on the result; when an error occurs (onError), handleError is invoked:

doStuff((result) {
  handle(result);
}, onError: (e) {
  handleError(e);
});

The same mechanism can be used in Dart; but, here, we have a more elegant way to handle this with objects appropriately called the Future objects. Now, we will define doStuff to return a Future object; this is a value (which could be an error) that is not yet available when doStuff returns. However, it will be available sometime in the future after doStuff has been executed (indicated using the then keyword). The same code snippet written using the Future objects will then be much more readable:

doStuff()  
  .then( (result) => handle(result) )
  .catchError( (e) => handleError(e) );

The doStuff method returns a Future object, so it could have been written as:

Future fut1 = doStuff();  
fut1.then( (result) => handle(result) )
    .catchError( (e) => handleError(e) );

However, the first or even the following shorter way is idiomatically used:

doStuff()
  .then(handle)
  .catchError(handleError);

Then, it registers the handle callback and catchError calls handleError when an error occurs, which stops the error from propagating. It could be considered as the asynchronous version of a try/catch construct (there is also a .whenComplete handler that is always executed and it corresponds with finally). The syntax advantage becomes even clearer when callbacks are nested to enforce the execution order, because this results in ugly and difficult-to-read code (sometimes referred to as callback hell). Suppose a doStuff2 computation has to occur between doStuff and handle, the first snippet will become much less readable:

doStuff((result){
doStuff2((result){
handle((result) {
      });
   }, onError: (e) {
  handleError(e);
  });
}, onError: (e) {
  handleError(e);
  });

However, the version using the Future objects remains very simple:

doStuff()
  .then(doStuff2)
  .then(handle)
  .catchError(handleError);

Through this chaining syntax, it looks like synchronous code, but it is purely an asynchronous code executing; catchError catches any errors that occur in the chain of the Future objects. As a simple, but working, example, suppose a future1 app needs to show or process a large bigfile.txt file and we don't want to wait until this I/O is completely done:

import 'dart:io';                                            (1)
import 'dart:async';

var file;                                                    (2)

main() {
  file = new File('bigfile.txt'),                            (3)
  // using Future:
  readFileFuture();
  // using async / await 
  readFileAsync();
    .then( (text) => print(text) )                           
    .catchError( (e) => print(e) );                          
  // do other things while file is read in
  ...                                                        (7)
}
readFileFuture() {
    file.readAsString()                                       (4)
                .then((text) => print(text))                   (5)
                .catchError((e) => print(e));                 (6)
// shorter version:
//  file.readAsString()
//    .then(print)
//    .catchError(print);
}

readFileAsync() async {
  try {
      var text = await file.readAsString();
      await print(text);
  }
  catch (e) {
    print(e);
  }
}

To work with files and directories, dart:io is needed in line (1); the Future functionality comes from dart:async (line (2)). In line (3), a File object is created. In line (4), the action to read the file is started asynchronously. However, the program immediately continues executing lines (7) and beyond. When the file is completely read through, line (5) prints its contents; should an e error (for example, a nonexisting file) have occurred, it is printed in line (6). You can even leave out the intermediary variables and write:

  file.readAsString()
    .then(print)
 	 .catchError(print);

In the readFileAsync method, we do the same, but now with the newer async / await syntax (see Chapter 3, Structuring Code with Classes and Libraries). Error handling can be applied here with normal try/catch. Asynchronous functions return the Future objects, so it will boil down to the same mechanism, but the syntax will be more readable.

You can find more information at https://www.dartlang.org/articles/futures-and-error-handling/.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.67.5