Dart: Futures and Streams
Learn how to use Futures and Streams for writing asynchronous code in dart By Michael Katz.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Dart: Futures and Streams
30 mins
Future Types
So far, you’ve seen delayed
, which performs an operation after a time delay. Two other handy Future
constructors are value
and error
.
To try them, replace main
once again with:
void main() {
final valueFuture = Future.value("Atlanta");
final errorFuture = Future.error("No city found");
valueFuture.then((value) => print("Value found: " + value));
errorFuture.then((value) => print("Value found: " + value));
}
As you can see, Future.value
needs a value and Future.error
needs an error description.
When run, you get the following output:
Value found: Atlanta
Uncaught Error: No city found
Notice that the error future’s then
is not evaluated and an uncaught error is recorded in the console. You’ll learn about error handling in the next section.
A few other constructor functions are available for creating Future
instances. Of those, sync
might be the handiest.
Add the following helper function:
int calculatePopulation() {
return 1000;
}
This simulates a population calculation. It’s a regular function that returns immediately.
Next, in main
, add to the bottom:
valueFuture
.then((value) => Future.sync(calculatePopulation))
.then((value) => print("Population found: $value"));
The sync
constructor is similar to the value
constructor. The main difference is it’s also possible for sync
to error as well as complete. It’s most useful in an operation like this where you want to run some synchronous code in response to an asynchronous one.
You’ll mostly use a future in two main ways. One way is by wrapping an async
function; the other is by transforming futures returned from APIs and packages. You’ll learn more about these in a future section.
Dealing with Errors
As you’ve seen, some futures can produce errors. That can include network i/o errors, disconnects or data deserialization errors.
Error handling is built into the Future API. The way to handle errors is to catch them and then act. For example, add the following to main
:
errorFuture
.then((value) => print("Then callback called."))
.catchError((error) => print("Error found: " + error));
A catchError
callback is provided, which catches the error. If you run and look in the console, you’ll see the then
callback was not run, but the catchError
callback was.
Error found: No city found
Futures can have a chain of then
callbacks that transform the results of one operation with one catch block at the end of the chain. This single block will handle any error along the way. For example:
errorFuture
.then((value) => print("Load configuration"))
.then((value) => print("Login with username and password."))
.then((value) => print("Deserialize the user information."))
.catchError((error) => print("Couldn't load user info, please try again"));
In this example, you can imagine a multistep startup sequence where no matter where it fails, you can prompt the user to try again.
You can also have different error handling for different errors. For example, replace main
again with:
class NetworkError implements Exception {}
class LoginError implements Exception{}
void main() {
final errorFuture = Future.error(LoginError());
errorFuture.then((value) => print("Success!"))
.catchError((error) => print("Network failed, try again."),
test: (error) => error is NetworkError)
.catchError((error) => print("Invalid username or password."),
test: (error) => error is LoginError)
.catchError((error) => print("Generic error, log it!"));
}
This creates two custom exception types: NetworkError
and LoginError
. Then, with a series of catchError
calls, it uses the optional test
parameter to run various callbacks depending on the type of error. You could use the test
parameter to check HTTP codes or error descriptions.
Using Async and Await
If you like writing code that performs asynchronous operations, but you don’t like the chaining of callbacks, the async/await keyword pair will be useful.
Denoting a function as async lets the compiler know that it won’t return right away. You then call such a function with an await keyword so the program knows to wait before continuing. You saw it earlier with the simulated network operation delay, but now you’ll learn to use it.
First, recall that when using a Future
, the return value has to be in a then
callback. For reference, replace main
with:
void main() {
print("Loading future cities...");
// 1
fetchSlowCityList().then(printCities);
// 2
print("done waiting for future cities");
}
In this example, you’ll notice:
- The data handling function is set with the future’s
then
method. - This print is executed immediately after starting the future, so the print happens in the console before the city list prints.
This can be rewritten to be easier to read and to get the print statement to print after the work is finished. Once again, replace main
with:
// 1
void main() async {
print("Loading future cities...");
// 2
final cities = await fetchSlowCityList();
// 3
printCities(cities);
// 4
print("done waiting for future cities");
}
This modification uses await
when calling the Future
, making the code more linear. Here’s some details:
- First, the
main
function has theasync
keyword. This indicates the function will not immediately return. It’s required for any function that uses anawait
. - By using the
await
keyword here, you can assign thevalue
of the completedFuture
to a local variable instead of having to wrap the value handler in a completion block. Program execution will stop on this line until theFuture
completes. - By waiting for completion, the code can now ensure that
cities
will have a value so you can use it as an input to another function such asprintCities
. - The final
print
statement will happen after the city list is printed, which you can verify by running the program and checking the console.
Take a look at an async
function. You added this function earlier:
Future<List<String>> fetchSlowCityList() async {
print("[SIMULATED NETWORK I/O]");
await Future.delayed(Duration(seconds: 2));
return fetchCityList();
}
The consequence of using an await
inside a function is you must annotate the function definition with the async
keyword. That means the function does not immediately return. Also, you have to designate the function’s return type is a Future
. In this case, the return
value is the output of fetchCityList()
, which is a List
. By using the async
keyword, this value is wrapped in a Future
, and thus the function definition needs to indicate that.
The return
value is a List
. When it’s used in main
, the assignment to cities
with the await
treats this value’s type as List
. When using async/await
, other than in function definitions, you needn’t worry about the Future
type.