If F=D and D=ST, then F=ST, right?
Flutter is written using Dart and Dart is a single-threaded language then Flutter apps are single-threaded. This means that a Flutter app can only do one thing at a time.
That is all true. But that does not mean that Flutter apps are forced to wait for slower processes.
Flutter apps use an event loop
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 puts 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!
- 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 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.
In Dart you have the ability to specify the type of thing that Future will give you eventually:
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 which we refer to as a callback.
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<Foo> then our callback should have this signature:
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:
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.
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 example above, the “Foo” that you’re awaiting is returned and put into theIncomingData. Simple.
Or maybe it isn’t that simple…
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 …
… becomes this when we await:
Note that when we added an await on line 2, 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 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.
Hint: 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 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.
If you’d like to do some more reading on Futures, here’s a thorough coverage from the Dart team: https://www.dartlang.org/tutorials/language/futures
(Did you know that you can clap up to 50 times per article? Help me out by hitting the clap button a bunch of times if this has been interesting or helpful to you. Thanks very much!)