Original address: medium.com/dartlang/da…
Author: Medium.com/kathyW_392…
Published: September 18, 2019-8 minutes to read
Many asynchronous Dart apis return futures.
One of the most basic apis for Dart asynchronous programming is an object of type futures-future. For the most part, Dart’s futures are very similar to future or promise apis in other languages.
This article discusses the concepts behind Dart futures and shows you how to use the Future API. It also discusses the Flutter FutureBuilder widget, which helps you asynchronously update the Flutter UI based on your future state.
Thanks to Dart language features like async-await, you may never need to use the Future API directly. But you’ll almost certainly encounter futures in your Dart code. And you might want to create futures or read code that uses the Future API.
This is the second article in the asynchronous Programming in Dart video series based on Flutter in Focus. The first article, “Debilitating and Event Cycles,” describes the basics of Dart support for back-end work.
If you like to learn by watching or listening, everything in this article will be covered in the following video.
www.youtube.com/watch?v=OTS…
Andrew Brogdon’s video was the inspiration for this article.
You can think of futures as little gift boxes for data. Someone hands you a gift box like this, and it starts out closed. In a moment the box bounced open and there was either something valuable or something wrong inside.
So a future can be in one of three states.
- The unfinished box has been closed
- It’s done. It has a value. The box is open and your gift (data) is ready.
- There are wrong completions. The box was opened, but there was a problem.
Most of the code you’ll see revolves around handling these three states. You receive a future, and you need to decide what to do before the box opens, what to do when it opens with a value, and what to do if an error occurs. You’ll often see this 1-2-3 pattern.
Three states of the future
You may remember the event loop from our article on the Dart event loop (figure below). The nice thing about futures is that they’re really just an API built to make it easier to use event loops.
The Dart event loop handles only one event at a time.
The Dart code you write is executed by a thread. The entire time your application is running, that little thread keeps going around, fetching events from the event queue and processing them.
Suppose you have some code to download a button (below implemented as a RaisedButton). When the user clicks, your button starts downloading the image.
RaisedButton(
onPressed: () {
final myFuture = http.get('https://my.image.url');
myFuture.then((resp) {
setImage(resp);
});
},
child: Text('Click me! '),Copy the code
First a click event occurs. The event loop gets the event and calls your tap handler (you use the RaisedButton constructor’s onPressed parameter). Your handler uses the HTTP library to make a request (http.get()) and get a future return (myFuture).
So now you have your little box, myFuture. It starts off off. To register a callback when it opens, you can use then().
Once you have your gift box, you wait. Maybe some other event comes in, the user does something, and your little box is sitting there, and the event loop keeps going around.
Finally, the image data is downloaded, and the HTTP library says, “Great! I’ve got my future stuff here.” Good, I found this future here. “It puts the data in the box, and it pops off, and that triggers your callback.
Now, that little code you handed to then() executes, and it displays the image.
Throughout this process, your code never touches the event loop directly. No matter what else is going on, or what other events are coming in. All you need to do is take the future from the HTTP library and say what to do when the future is complete.
In real code, you also need to deal with errors. We’ll show you how to do that later.
Let’s take a closer look at Future’s API, some of the uses you just saw.
Ok, first question: How do you get an instance of a Future? In most cases, you don’t create futures directly. This is because many common asynchronous programming tasks already have libraries that generate futures for you.
For example, network communication returns to a future.
final myFuture = http.get('http://example.com');
Copy the code
Getting access to shared preferences also returns a future.
final myFuture = SharedPreferences.getInstance();
Copy the code
But you can also use the Future constructor to create futures.
Future constructor
The simplest constructor is the Future(), which takes a function and returns a Future that matches the return type of the function. The function then runs asynchronously, and the future completes with the return value of the function. Here is an example using Future().
void main() {
final myFuture = Future(() {
return 12;
});
}
Copy the code
Let’s add a few print statements to clarify the asynchronous part.
void main() {
final myFuture = Future(() {
print('Creating the future.'); // Prints second.
return 12;
});
print('Done with main().'); // Prints first.
}
Copy the code
If you run this code in DartPad(dartpad.dev), the entire main function completes before the function given to the Future() constructor. This is because the Future() constructor initially just returns an unfinished Future. He said, “Here is the box. You take it, and I’ll run your function and put some data in it.” This is the output of the previous code.
Done with main().
Creating the future.
Copy the code
The other constructor, future.value (), comes in handy when you already know the Future value. This constructor is useful when you are building services that use caching. Sometimes you already have the value you need, so you can just put it there.
final myFuture = Future.value(12);
Copy the code
The future.value () constructor has a corresponding function to handle errors. Called future.error (), it works much the same way, but receives an error object and an optional stack trace.
final myFuture = Future.error(ArgumentError.notNull('input'));
Copy the code
The most convenient Future constructor is probably future.delayed (). It works like Future(), except that it waits a specified amount of time before running the function and completing the Future.
One way to use future.delayed () is when you are creating mock network services for testing. If you need to make sure your load spinner displays correctly, the future of latency is your friend.
final myFuture = Future.delayed(
const Duration(seconds: 5), () = >12,);Copy the code
The use of the future
Now that you know the origin of futures, let’s talk about how to use them. As we mentioned earlier, the main purpose of using a future is to account for the three states it can be in: incomplete, value-completed, or error-completed.
The following code uses future.delayed () to create a Future completed in 3 seconds with a value of 100.
void main() {
Future.delayed(
const Duration(seconds: 3), () = >100,);print('Waiting for a value... ');
}
Copy the code
As this code executes, main() runs from top to bottom, creating the future and printing “wait values…” That whole time, the future is unfinished. It won’t be finished in three seconds.
To use completed values, you can use then(). That’s an instance method on each future that you can use to register a callback when the future is done with a value. You give it a function, and it needs a single argument that matches the future type. Once the future completes with a value, your function will be called with that value.
void main() {
Future.delayed(
const Duration(seconds: 3), () = >100,
).then((value) {
print('The value is $value. '); // Prints later, after 3 seconds.
});
print('Waiting for a value... '); // Prints first.
}
Copy the code
This is the output of the previous code.
Waiting for a value... (3 seconds pass until callback executes)
The value is 100.
Copy the code
In addition to executing your code, then() returns a future of its own that matches the return value of any function you give it. So if you need to make several asynchronous calls, you can chain them together, even if they have different return types.
_fetchNameForId(12)
.then((name) => _fetchCountForName(name))
.then((count) => print('The count is $count. '));
Copy the code
Going back to our first example, what happens if that initial future doesn’t complete a value — what if something goes wrong when it does? The then() method expects a value. You need a way to register another callback in case an error occurs.
The answer is catchError(). It works the same as then(), except that it receives an error instead of a value and executes if the future completes with an error. Just like then(), the catchError() method returns a future of its own, so you can set up a complete chain of then() and catchError() methods, waiting for each other.
Note: If you use the async-await language feature, you do not need to call then() or catchError(). Instead, wait for completed values and use try-catch-finally to handle errors. See the asynchronous support section of the Dart language Tour for details.
Here is an example of using catchError() to handle future completion errors.
void main() {
Future.delayed(
Duration(seconds: 3), () = >throw 'Error! '.// Complete with an error.
).then((value) {
print(value);
}).catchError((err) {
print('Caught $err'); // Handle the error.
});
print('Waiting for a value... ');
}
Copy the code
You can even give catchError() a test function to check for errors before calling the callback. You can have multiple catchError() functions in this way, each checking for a different type of error. Here is an example of specifying a test function using the optional test argument of catchError().
void main() {
Future.delayed(
Duration(seconds: 3), () = >throw 'Error! ',
).then((value) {
print(value);
}).catchError((err) {
print('Caught $err');
}, test: (err) { // Optional test parameter.
return err is String;
});
print('Waiting for a value... ');
}
Copy the code
Now that you’ve made it this far, hopefully you can see how the next three states are often reflected in the structure of your code. In the previous example, there were three blocks.
- The first block is to create an unfinished future.
- There is then a function to call a value on future completion.
- Then there is another function that is called if an error occurs on future completion.
There’s another method you might want to use: whenComplete(). You can use it to execute a function when it’s done in the future, whether it’s done with a value or an error.
It’s kind of like the finally block in try-catch-finally. There is code to execute, if all goes well, wrong code, and code to run no matter what.
Futures are used in Flutter
So that’s one point of how you create futures and how you use their value. Now let’s talk about putting them to work in Flutter.
Suppose you have a web service that returns some JSON data and you want to display it. You can create a StatefulWidget that creates the future, checks for completion or errors, calls setState(), and generally handles all wiring manually.
Or you can use FutureBuilder. This is a widget that comes with the Flutter SDK. You give it a future and a builder function, and when the future is complete, it will automatically rebuild its subroutine.
The FutureBuilder widget works by calling its builder function, which requires a snapshot of the context and future current state.
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Use a FutureBuilder.
return FutureBuilder<String>( future: _fetchNetworkData(), builder: (context, snapshot) {}, ); }}Copy the code
You can check the snapshot to see if it will complete in error in the future.
return FutureBuilder<String>( future: _fetchNetworkData(5), builder: (context, snapshot) { if (snapshot.hasError) { // Future completed with an error. return Text( 'There was an error', ); } throw UnimplementedError("Case not handled yet"); });Copy the code
Otherwise you can check the hasData property to see if it completes with a value.
return FutureBuilder<String>(
future: _fetchNetworkData(5),
builder: (context, snapshot) {
if (snapshot.hasError) {
// Future completed with an error.
return Text(
'There was an error',); }else if (snapshot.hasData) {
// Future completed with a value.
return Text(
json.decode(snapshot.data)['field']); }throw UnimplementedError("Case not handled yet"); });Copy the code
If neither hasError nor hasData is true, then you know you’re still waiting, and you can output something as well.
return FutureBuilder<String>(
future: _fetchNetworkData(5),
builder: (context, snapshot) {
if (snapshot.hasError) {
// Future completed with an error.
return Text(
'There was an error',); }else if (snapshot.hasData) {
// Future completed with a value.
return Text(
json.decode(snapshot.data)['field']); }else {
// Uncompleted.
return Text(
'No value yet! ',); }});Copy the code
Even in the Flutter code, you can see how these three states are constantly present: incomplete, valued completion, and error completion.
conclusion
This article covered what futures represent and how to use the Future and FutureBuilder apis to create futures and use their completion values.
If you want to learn more about working with futures — and have the option to test your understanding with working examples and interactive exercises — check out the Asynchronous Code Lab on Futures, Asynchrony, and Waiting.
Or move on to the next asynchronous programming video in the Dart series. It talks about flows, which are a lot like futures because they can provide values or errors. But the futures only give you one result and then stop, while the flow just keeps going.
www.youtube.com/watch?v=nQB…
Many thanks to Andrew Brogdon, who created the video on which this article is based.