If you are new to Flutter you might have difficulties working with async code.
For some people, this might even be the first time seeing anything like async
code. This article should help you grasp basic concepts about Future
in
Flutter and make you a proficient async programmer.
Sync vs. Async programming
Imagine two different kinds of communication: in-person talk and email correspondence. When you talk with somebody who stands right in front of you there is hardly any delay. You ask something and your peer immediately responds. And importantly, you wait for the other person to answer. This is synchronous communication.
On the other hand, when you write an email you might not hear back right away. You may receive a reply after a few hours, days, or in some cases, never. Unlike with the in-person talk you expect the delay when you chat on email. You don't sit idly while waiting for a reply. You do some different tasks in the meanwhile and return when the email arrives. This is asynchronous communication.
And this is the point with async programming. Some operations are instant and you are supposed to wait for them to finish. However, other operations (like network requests) might take unknown time to end. If your program waits for them it cannot do anything else in the meantime. This would appear to the user as unresponsiveness or even lags.
The problem is that the compiler does not know which operations (i.e. methods or functions) are instant and which are not. It needs to handle them differently. Hence you need to need to tell the compiler (or interpreter) exactly which operations should be awaited and which should not.
async
keyword
Dart's way of telling the compiler about async operations is using the async
keyword.
Lets start with simple synchronous code. Below you have a main
function and
fetchNumber
function which fetches some number in 3 seconds. This example does
not use async
and therefore the compiler will wait for each operation.
import 'dart:io';
void main() {
print(fetchNumber());
print('Hello world!');
}
int fetchNumber() {
print('Starting to sleep for 3 seconds!');
sleep(Duration(seconds: 3));
return 1337;
}
Because fetchNumber
is not marked as async and because there is a sleep
for
3 seconds, every other operation like the print('Hello world)
will be delayed.
You need two things to make a function async: mark it as async
and wrap the
return type in Future
.1 async
tells the compiler that this function is
asynchronous. Future
, however, deserves a chapter on its own.
Future
Into the Future<T>
represents a T
value you ought to receive in the future. You can
for example have Future<int>
, which means that you expect to receive an int
value (or an error).
Future
can be in 3 different states: in progress, done with value and
done with error. Future
is by default in progress which means that no value
or error is yet available.
Because you don't know exactly when Future<T>
finishes with either value T
or an error, you need a way to listen to its changes. This might be done using
callbacks. Callbacks are a special way of registering a method to be called
after some time or event. In the case of Future
the callback method is
then()
and will be called after Future
finishes with value. Lets change the
previous example to use a callback.
void main() {
fetchNumber().then((value) => print(value));
print('Hello world!');
}
Future<int> fetchNumber() async {
print('Starting to sleep for 3 seconds!');
return Future.delayed(Duration(seconds: 3), () => 1337);
}
There are more callback methods for the Future
type.2 You can use
catchError()
callback to handle a situation when Future
finishes with an
exception. And in some cases, you can apply timeout()
callback to prevent
stuck Future
and return own value when given time passes.
await
keyword
When you overuse callback you might end up in a special place called callback
hell. You often need to work with multiple async methods at the time and
nesting multiple callbacks make code quickly unreadable. For cases when you want
to keep your code tidy there is a better choice: await
. The keyword await
lets you work with async calls synchronously. Using await
you can work with
async code the same way you work with sync code.
Future<void> main() async {
print(await fetchNumber());
print('Hello world!');
}
Future<int> fetchNumber() async {
print('Starting to sleep for 3 seconds!');
await Future.delayed(Duration(seconds: 3));
return 1337;
}
// Starting to sleep for 3 seconds!
// 1337
// Hello world!
When you put the await
keyword before an async call you tell the compiler that
you want only its value T
. The program will stop at the given line and wait
for the Future
to finish. Note that you can only use await
inside methods
marked as async
.
You should note that await
changes type signature. Whenever you put await
before an async method you will only receive the final value T
.
Future<int> fetchNumber() async {
return Future.delayed(Duration(seconds: 2), () => 10);
}
// Type of `result` is Future<int>. Compiler will not pause at line below.
var result = fetchNumber();
// Type of `awaitedResult` is int. Compiler will pause at this line.
var awaitedResult = await fetchNumber();
Using await
allows you to use regular try/catch statements and catch possible
exceptions without needing callbacks. This significantly helps when working with
async operations.
Future<void> main() async {
try {
print(await fetchNumber());
} catch (e) {
print('Error: $e');
}
print('Hello world!');
}
Future<int> fetchNumber() async {
throw 'An exceptional exception!';
return 1337;
}
// Error: An exceptional exception!
// Hello world!
Useful lints
Bugs caused by async code are one of the hardest to debug. I often forget to
await futures and then its only matter of when something breaks. This is easily
prevented using
unawaited_futures
lint rule.
Whenever you call an async method in async context (inside a different async
method) and forget to put await
you will get a warning.
Create a file named analysis_options.yaml
at your project's root (next to the
pubspec.yaml
) and put the following code inside.
linter:
rules:
- unawaited_futures
Another nice lint is unnecessary_await_in_return
. When you return some
Future
value from a method you don't have put await
before. In the example
below when you call the fetchNumber
you will need to await its returned
Future
. Because the first fetchNumber
is marked async the returned value
will always be wrapped inside the Future
and therefore any await inside is
redundant.3
// DON'T
Future<int> fetchNumber() async {
return await Future.delayed(Duration(seconds: 3), () => 1337);
}
// DO
Future<int> fetchNumber() {
return Future.delayed(Duration(seconds: 3), () => 1337);
}
Async Widgets
You have an async call that fetches some data and want to display it in a
Flutter app. The easiest way to do this is with the FutureBuilder
widget.
However, we can create our own version to see how it works.
Futures can be in different states: loading, done, or error. Therefore you need
to use StatefulWidget
and change state accordingly.
class AsyncBuilder extends StatefulWidget {
@override
_AsyncBuilderState createState() => _AsyncBuilderState();
}
class _AsyncBuilderState extends State<AsyncBuilder> {
// `int` value we expect to receive. When fetching is in progress
// this will be null.
int value;
@override
void initState() {
super.initState();
// Add callback to async `fetchNumber` call. You cannot use `await`
// because `initState` is not async.
fetchNumber().then((result) {
// Once we receive our number we trigger rebuild.
setState(() {
value = result;
});
});
}
@override
Widget build(BuildContext context) {
// When value is null show loading indicator.
if (value == null) {
return const CircularProgressIndicator();
}
return Text('Fetched value: $value');
}
}
This example does not handle possible exceptions. You might accomplish this by
adding a new variable called error
(with dynamic
type). In case the Future
throws you save the error into the error
variable. Then inside the build
method check if error != null
and show some message.
As I mentioned few paragraphs above you will most of the time use the
FutureBuilder
widget from Flutter. It does everything as the example above
(and probably better).
FutureBuilder(
future: fetchNumber(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('Fetched value: ${snapshot.data}');
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return CircularProgressIndicator();
}
},
)
There is, however, one important caveat. FutureBuilder
requires a future
param. In our case, we provide future value using the fetchNumber
method. Due
to this each time the widget that contains FutureBuilder
above will call the
fetchNumber
and acquire a new future
value. FutureBuilder
then on each
rebuild checks if the params changed and updates itself. This means that the
future
provided might be called several times (possibly on each rebuild) which
is wasteful.
To fix this we need to call fetchNumber
only once inside the initState
, save
the returned Future<int>
into a variable, and provide only the variable into
the FutureBuilder
. This way the equality check inside FutureBuilder
works
and the async call will not be repeated on rebuilds.
class _AsyncWidgetState extends State<AsyncWidget> {
Future<int> future;
@override
void initState() {
super.initState();
future = fetchNumber();
}
Future<int> fetchNumber() async {
return Future.delayed(Duration(seconds: 3), () => 1337);
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('Fetched value: ${snapshot.data}');
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return CircularProgressIndicator();
}
},
);
}
}
Because the initState
is guaranteed to be called only once in stateful widgets
there will not be multiple futures created. By the way, this is the behavior of
our custom AsyncBuilder
widget.
Footnotes
-
This is a simplification and not exactly the truth. You can omit
async
and only set the return type toFuture
. However, theasync
keyword will automatically set every returned value asFuture
. This makes writing code easier. ↩ -
Both cases work the same and this lint serves only to keep tidy codebase. ↩