Here’s everything you need to know about the Flutter Future, Await, and Async.
Flutter is written using Dart, and Dart is a single-threaded language. This means that a Flutter app can only do one thing at a time, but it does not mean that Flutter apps are forced to wait for slower processes.
Flutter apps use an event loop. This should come as no surprise since Android has a main looper and iOS has a run loop (aka main loop). Heck, even JavaScript devs are unimpressed since JavaScript itself has a … wait for it … event loop. Yes, all the cool kids are using an event loop these days.
An event loop is a background infinite loop which periodically wakes up and looks in the event queue for any tasks that need to run. If any exist, the event loops put them onto the run stack if and only if the CPU is idle. As your app is running instructions, they run serially – one after another. If an instruction is encountered that may potentially block the main thread waiting on some resource, it is started and the “wait” part is put on a separate queue.
Why Would It wait?
Certain things are slow compared to the CPU. Reading from a file is slow. Writing to a file is even slower. Communicating via Ajax? Forget about it. If we kept the waiting activity on the main thread, it would block all other commands. What a waste!
The way this is handled in JavaScript, iOS, Android, and now Dart is this:
- An activity that is well-known to be slow is started up as normal.
- The moment it begins waiting for something – disk, HTTP request, whatever – it is moved away from the CPU.
- A listener of sorts is created. It monitors the activity and raises an alert when it is finished waiting.
- The reference to that listener is returned to the main thread. This reference object is known as a Future.
- The main thread continues chugging along its merry way.
- When the waiting activity is finally resolved, the event loop sees it and runs an associated method (aka. a callback
function) on the main thread to handle finishing up the slow event.
All you do is write the code to create the future and to handle futures that are returned from other methods:
// Say goReadAFile() is slow and returns a Future Future myFuture = goReadAFile();
In Dart you have the ability to specify the type of thing that Future will give you eventually:
Type of future | When it’s ready, I’ll have a … |
---|---|
Future<String> | … string |
Future<Foo> | … Foo |
Future<Map<String, dynamic>> | … Map whose keys are Strings and whose values are dynamic |
When we have that Future object, you may not have the data, but you definitely have a promise to get that data in the Future. (See what they did there?)
How do we get the data from a Future?
You tell the Future what to do once the data is ready. Basically, you’re responding to a “Yo, the data is ready” event and telling the Future what to do by registering a function.
myFuture.then(myCallback);
The .then()
function is how you register that callback function. The callback should be written to handle the promised data. For example, if we have a Future, then our callback should have this signature:
void myCallback(Foo theIncomingData) { doSomethingWith(theIncomingData); }
So if the Future will return a Person, your callback should receive a Person. If the Future promises a String, your callback should receive a String. And so forth.
Your callbacks should always return void because there’s no way that the .then function can receive a returned value. This makes a ton of sense when you think about it because remember that it is no longer running within the main thread of your app so it has no way of merging back in. So how do you get a value from the callback? Several methods, but the most understandable is that you use a variable that is defined outside the callback:
class FooState extends State<FooComponent> { String _firstName; // <-- A variable known by the whole class Widget build(BuildContext context) { // return a widget } void _myCallback(String someVar) { _firstName = someVar; // <-- Getting a value OUT of an async callback } }
Tacking a .then()
onto your Future object can occasionally muddy up your code. If you prefer, you can clean it up a bit with await
.
await
There’s another way to get the data which is more straightforward to read. Instead of using .then()
, you can await the data.
Foo theIncomingData = await somethingThatReturnsAFuture();
Awaiting pauses the running code to … well … wait for the Future to resolve before it moves on to the next line. In the preceding example, the “Foo” that you’re awaiting is returned and put into theIncomingData
. Simple. Or maybe it isn’t that simple…
async
Like it or not, when you use await inside a function, that function is now in danger of blocking the main thread, so it must be marked as async. For example, this function;
Bar someFunction() { Foo theIncomingData = someFunction(); return new Bar(); }
becomes this when we await
Future<Bar> someFunction() async { Foo theIncomingData = await somethingThatReturnsAFuture(); return new Bar(); }
Note that when we added an await on that second line, we must mark the function itself with async. The subtle thing is that when it is marked as async, anything returned from that function is immediately wrapped in a Future
unless it is already one.
Are you sitting down? Check this out: whenever you choose to await a future, the function must be marked as
async, and therefore all who call it must be awaited and they must be marked as async and so on. Eventually you get to a high enough spot in the call chain that you’re not in a function so you don’t have to mark it as async
. Maybe I spoke too soon when I said this is simpler.
NOTE: The Flutter build() method cannot be async, but events like onPress
can. So try to steer your async activities into events to solve this recursive async-await-async-await thing.
Here are your Futures takeaways:
- Futures allow your Dart code to be asynchronous – it can handle slow-running processes in a separate thread (kind
of). - You can handle the callbacks of those things with either a .then(callback) or by awaiting them.
- If you
await
in a function, that function must be marked as async