Asynchronous Programming in Flutter

Asynchronous Programming allows your app to complete time-consuming tasks, such as retrieving an image from the web, or writing some data to a web server, while running other tasks in parallel and responding to the user input. This improves the user experience and the overall quality of your software.

In Dart and Flutter, you can write asynchronous code leveraging Futures, and the async/await pattern: these patterns exist in most modern programming languages, but Flutter also has a very efficient way to build the user interface asynchronously using the FutureBuilder class. 

By following the recipes in this tutorial, you will achieve a thorough understanding of how to leverage Futures in your apps, and you will also learn how to choose and use the right tools among the several options you have in Flutter, including Futures, async/await, and FutureBuilder

In this tutorial, we will cover the following topics:

  • Using a Future
  • Using async/await to remove callbacks
  • Completing Futures
  • Firing multiple Futures at the same time
  • Resolving errors in asynchronous code
  • Using Futures with StatefulWidgets
  • Using FutureBuilder to let Flutter manage your Futures
  • Turning navigation routes into asynchronous functions
  • Getting the results from a dialog    

Technical requirements

To follow along with the recipes in this tutorial, you should have the following software installed on your Windows, Mac, Linux, or Chrome OS device:

  • The Flutter SDK.
  • The Android SDK when developing for Android.
  • macOS and Xcode when developing for iOS.
  • An emulator or simulator, or a connected mobile device enabled for debugging.
  • Your favorite code editor: Android Studio, Visual Studio Code, and IntelliJ IDEA. are recommended. All should have the Flutter/Dart extensions installed.

Using a Future

When you write your code, you generally expect your instructions to run sequentially, one line after the other. For instance, let’s say you write the following:

int x = 5;
int y = x * 2;

You expect the value of y to be equal to 10 because the instruction int x = 5 completes before the next line. In other words, the second line waits for the first instruction to complete before being executed.

In most cases, this pattern works perfectly, but in some cases, and specifically, when you need to run instructions that take longer to complete, this is not the recommended approach, as your app would be unresponsive until the task is completed. That’s why in almost all modern programming languages, including Dart, you can perform asynchronous operations.

Asynchronous operations do not stop the main line of execution, and therefore they allow the execution of other tasks before completing.

Consider the following diagram:

In the diagram, you can see how the main execution line, which deals with the user interface, may call a long-running task asynchronously without stopping to wait for the results, and when the long-running task completes, it returns to the main execution line, which can deal with it.

Dart is a single-threaded language, but despite this, you can use asynchronous programming patterns to create reactive apps.

In Dart and Flutter, you can use the Future class to perform asynchronous operations. Some use cases where an asynchronous approach is recommended include retrieving data from a web service, writing data to a database, finding a device’s coordinates, or reading data from a file on a device. Performing these tasks asynchronously will keep your app responsive.

In this recipe, you will create an app that reads some data from a web service using the http library. Specifically, we will read JSON data from the Google Books API.

Getting ready

In order to follow along with this recipe, there are the following requirements:

  • Your device will need an internet connection to retrieve data from the web service. 
  • The starting code for this recipe is available at 

How to do it…

You will create an app that connects to the Google Books API to retrieve a Flutter book’s data from the web service, and you will show part of the result on the screen as shown in the screenshot:

The steps required in order to retrieve and show the data using a Future are outlined here:

  1. Create a new app and, in the pubspec.yaml file, add the html dependency (always make sure you are using the latest version, checking at https://pub.dev/packages/http/install):
dependencies:
flutter:
sdk: flutter
http: ^0.13.1
  1. The starting code in the main.dart file contains an ElevatedButton, a Text containing the result, and a CircularProgressIndicator. Now, type the following code:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Future Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: FuturePage(), ); }}
class FuturePage extends StatefulWidget {
@override
_FuturePageState createState() => _FuturePageState();
}
class _FuturePageState extends State<FuturePage> {
String result;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Back from the Future'),
),
body: Center(
child: Column(children: [
Spacer(),
ElevatedButton(
child: Text('GO!'),
onPressed: () {
}, ),
Spacer(),
Text(result.toString()),
Spacer(),
CircularProgressIndicator(),
Spacer(),
]), ), ); }}

There’s nothing special about this code. Just note that in the Column we have placed a CircularProgressIndicator: as long as the progress animation keeps moving, this means the app is responsive. When it stops, it means the user interface is waiting for a process to complete.

  1. Now we need to create a method that retrieves some data from a web service: in particular, we’ll use the Google Books API for this recipe. At the end of the _FuturePageState class, add a method called getData() as shown here:
  Future<Response> getData() async {
final String authority = 'www.googleapis.com';
final String path = '/books/v1/volumes/junbDwAAQBAJ';
Uri url = Uri.https(authority, path);
return http.get(url);
}
  1. In order to call the getData() method when the user taps the ElevatedButton, add the following code in the onPressed function:
 ElevatedButton(
child: Text('GO!'),
onPressed: () {
result = '';
setState(() {
result = result;
});
getData()
.then((value) {
result = value.body.toString().substring(0, 450);
setState(() {
result = result;
});
}).catchError((_){
result = 'An error occurred';
setState(() {
result = result;
});
});
},
),

How it works…

In the preceding code, we are calling the getData() method, but after that, we are adding the then function.

Note the following:

  • The getData() method returns a Future. Futures are generics, so you have the option to specify the type of Future you are returning; if the return value of a method is Future<int>, it means that your method will return a Future containing an integer number. In this case, specifying the type is not required, so we could also write the following:
Future getData() async {

The preceding code would work just as well.

  • The getData() method is marked as async. It is considered a good practice to mark your asynchronous methods with the async keyword, but it’s not required in this example
  • The http.get method makes a call to the Uri you specify as a parameter, and when the call completes, it returns an object of type Response.

Uniform Resource Identifier (URI) is a sequence of characters that universally identify a resource. Examples of URIs include a web address, an email address, a barcode or ISBN book code, and even a telephone number.

  • When building a Uri in Flutter, you pass the authority (which is the domain name in this example) and the path within the domain where your data is located.
  • In this example, the URL is the address of specific book data in JSON format.

When a method returns a Future, it does not return the actual value but the promise of returning a value at a later time.

Imagine a takeaway restaurant: you enter the restaurant and place your order. Instead of giving you the food immediately, the teller gives you a receipt, containing the number of your order. This is a promise of giving you the food as soon as it’s ready.

Futures in Flutter work in the same way: in our example, we will get a Response object at a time in the future, as soon as the http connection has completed, successfully or with an error.

then is called when the Future returns successfully, and value is the result of the call. In other words, what’s happening here is this:

  1. You call getData.
  2. The execution continues in the main thread.
  3. At some time in the futuregetData returns a Response.
  4. The then function is called, and the Response value is passed to the function.
  5. You update the state of the widget calling setState and showing the first 450 characters of the result.

After the then() method, you concatenate the catchError function. This is called if the Future does not return successfully: in this case, you catch the error and give some feedback to the user.

A Future may be in one of three states:

  1. Uncompleted: You called a Future, but the response isn’t available yet.
  2. Completed successfully: The then() function is called.
  3. Completed with an error: The catchError() function is called.

Following an asynchronous pattern in your apps using Futures is not particularly complicated in itself: the main point here is that the execution does not happen sequentially, so you should understand when the returning values of an async function are available (only inside the then() function) and when they are not.

See also

A great resource to understand the asynchronous pattern in Flutter is the official codelab available at https://dart.dev/codelabs/async-await

For Futures, in particular, I also recommend watching the official video at http://y2u.be/OTS-ap9_aXc.

On a more theoretical basis, but extremely important to understand, is the concept of Isolate, which can explain how asynchronous programming works in Dart and Flutter: there’s a thorough explanation in the video linked here: http://y2u.be/vl_AaCgudcY.

Using async/await to remove callbacks

Futures, with their then callbacks, allow developers to deal with asynchronous programming. There is an alternative pattern to deal with Futures that can help make your code cleaner and easier to read: the async/await pattern.

Several modern languages have this alternate syntax to simplify code, and at its core, it’s based on two keywords: async and await:

  • async is used to mark a method as asynchronous, and it should be added before the function body.
  • await is used to tell the framework to wait until the function has finished its execution and returns a value. While the then callback works in any method, await only works inside async methods.

When you use await, the caller function must use the async modifier, and the function you call with await should also be marked as async.

What happens under the hood is that when you await an asynchronous function, the line of execution is stopped until the async operation completes.

Here, you can see an example of writing the same code with the then callback and the await syntax:

In the preceding code, you can see a comparison between the two approaches of dealing with Futures. Please note the following:

  • The then() callback can be used in any method; await requires async.
  • After the execution of the await statement, the returned value is immediately available to the following lines.
  •  You can append a catchError callback: there is no equivalent syntax with async/await 

Getting ready

In order to follow along with this recipe, there are the following requirements:

  • Your device will need an internet connection.
  • If you followed along with the previous recipe, you’ll only need to edit the existing project. Otherwise, create a new app with the starting code available at 

How to do it…

In this recipe, you will see the advantages of using the async/await pattern: 

  1. Add the following three methods to the main.dart file, at the bottom of the _FuturePageState class:
  Future<int> returnOneAsync() async {
await Future<int>.delayed(const Duration(seconds: 3));
return 1;
}

Future<int> returnTwoAsync() async {
await Future<int>.delayed(const Duration(seconds: 3));
return 2;
}

Future<int> returnThreeAsync() async {
await Future<int>.delayed(const Duration(seconds: 3));
return 3;
}
  1. Under the three methods you just created, add the count() method leveraging the async/await pattern:
Future count() async {
int total = 0;
total = await returnOneAsync();
total += await returnTwoAsync();
total += await returnThreeAsync();
setState(() {
result = total.toString();
});
}
  1. Call the count() method from the onPressed function of the “GO” button: 

ElevatedButton(
child: Text('GO!'),
onPressed: () {
count();
}
...
  1. Try out your app: you should see the result 6 after 9 seconds, as shown in the following screenshot (the wheel will keep turning – this is expected):

How it works…

The great feature of the async/await pattern is that you can write your code like sequential code, with all the advantages of asynchronous programming (namely, not blocking the UI thread). After the execution of the await statement, the returned value is immediately available to the following lines, so the total variable in our example can be updated just after the execution of the await statement. 

The three methods used in this recipe (returnOneAsyncreturnTwoAsync, and returnThreeAsync) each wait 3 seconds and then return a number. If you wanted to sum those numbers using the then callback, you would write something like this:

returnOneAsync().then((value) {
total += value;
returnTwoAsync().then((value) {
total += value;
returnThreeAsync().then((value) {
total += value;
setState(() {
result = total.toString();
});
});
});
});

You can clearly see that, even if it works perfectly, this code would soon become very hard to read and maintain: nesting several then callbacks one into the other is sometimes called “callback hell,” as it easily becomes a nightmare to deal with.

The only rule to remember is that while you can place a then callback anywhere (for instance, in the onPressed method of an ElevatedButton), you need to create an async method in order to use the await keyword.

See also

A great resource to understand the asynchronous pattern in Flutter is the official codelab available at https://dart.dev/codelabs/async-await

For the async/await pattern, in particular, I also recommend watching the official video at http://y2u.be/SmTCmDMi4BY.

Completing Futures

Using a Future with thencatchErrorasync, and await will be enough for most use cases, but there is another way to deal with asynchronous programming in Dart and Flutter: the Completer class.

Completer creates Future objects that you can complete later with a value or an error. We will be using Completer in this recipe.

Getting ready

In order to follow along with this recipe, there are the following requirements:

How to do it…

In this recipe, you will see how to use the Completer class to perform a long-running task:

  1. Add the following code in the main.dart file, in the _FuturePageState class:
Completer completer;
Future<int> getNumber() {
completer = Completer<int>();
calculate();
return completer.future;
}
calculate() async {
await new Future.delayed(const Duration(seconds : 5));
completer.complete(42);
}
  1. If you followed the previous recipes in the chapter, comment out the code in the onPressed function.
  2. Add the following code in the onPressed() function:
getNumber().then((value) {
setState(() {
result = value.toString();
});
});

If you try the app right now, you’ll notice that after 5 seconds delay, the number 42 should show up on the screen.

How it works…

Completer creates Future objects that can be completed later. The Completer.future that’s set in the getNumber method is the Future that will be completed once complete is called.

In the example in this recipe, when you call the getNumber() method, you are returning a Future, by calling the following:

return completer.future;

The getNumber() method also calls the calculate() async function, which waits 5 seconds (here, you could place any long-running task), and calls the completer.complete method.

Completer.complete changes the state of the Completer, so that you can get the returned value in a then() callback. 

 Completers are very useful when you call a service that does not use Futures, and you want to return a Future. It also de-couples the execution of your long-running task from the Future itself.

There’s more…

You can also call the completeError method of a Completer when you need to deal with an error:

  1. Change the code in the calculate() method like this:
calculate() async {
try {
await new Future.delayed(const Duration(seconds : 5));
completer.complete(42);
}
catch (_) {
completer.completeError(null);
}
}
  1. In the call to getNumber, you could then concatenate a catchError to the then function:
getNumber().then((value) {
setState(() {
result = value.toString();
});
}).catchError((e) {
result = 'An error occurred';
});

See also

You can have a look at the full documentation for the Completer object at https://api.flutter.dev/flutter/dart-async/Completer-class.html.

Firing multiple Futures at the same time

When you need to run multiple Futures at the same time, there is a class that makes the process extremely easy: FutureGroup.

FutureGroup is available in the async package, which must be added to your pubspec.yaml, and imported into your dart file as shown in the following code block:

import 'package:async/async.dart';

Please note that dart:async and async/async.dart are different libraries: in many cases, you need both to run your asynchronous code.

FutureGroup is a collection of Futures that can be run in parallel. As all the tasks run in parallel, the time of execution is generally faster than calling each asynchronous method one after another.

When all the Futures of the collection have finished executing, a FutureGroup returns its values as a List, in the same order they were added into the group.

You can add Futures to a FutureGroup using the add() method, and when all the Futures have been added, you call the close() method to signal that no more Futures will be added to the group.
If any of the Futures in the group returns an error, the FutureGroup will return an error and will be closed.

In this recipe, you will see how to use a FutureGroup to perform several tasks in parallel.

Getting ready

In order to follow along with this recipe, there is the following requirement:

  • You should have completed the code in the previous recipe: Using async/await to remove callbacks.

How to do it…

In this recipe, instead of waiting for each task to complete, you will use a FutureGroup to run three asynchronous tasks in parallel: 

  1. Add the following code in the _FuturePageState class, in the main.dart file of your project:
void returnFG() {
FutureGroup<int> futureGroup = FutureGroup<int>();
futureGroup.add(returnOneAsync());
futureGroup.add(returnTwoAsync());
futureGroup.add(returnThreeAsync());
futureGroup.close();
futureGroup.future.then((List <int> value) {
int total = 0;
value.forEach((element) {
total += element;
});
setState(() {
result = total.toString();
});
});
}
  1. In order to try this code, you can just add the call to returnFG() in the onPressed method of the ElevatedButton (remove or comment out the old code if necessary):
onPressed: () {
returnFG();
}
  1. Run the code. This time you should see the result faster (after about 3 seconds instead of 9).

How it works…

In the returnFG() method, we are creating a new FutureGroup with this instruction: 

FutureGroup<int> futureGroup = FutureGroup<int>();

FutureGroup is a generic, so FutureGroup<int> means that the values returned inside the FutureGroup will be of type int.

The add method allows you to add several Futures in a FutureGroup. In our example, we added add three Futures: 

futureGroup.add(returnOneAsync());
futureGroup.add(returnTwoAsync());
futureGroup.add(returnThreeAsync());

Once all the Futures have been added, you always need to call the close method. This tells the framework that all the futures have been added, and the tasks are ready to be run:

futureGroup.close();

In order to read the values returned by the collection of Futures, you can leverage the then() method of the future property of the FutureGroup. The returned values are placed in a List, so you can use a forEach loop to read the values. You can also call the setState() method to update the UI:

 futureGroup.future.then((List <int> value) {
int total = 0;
value.forEach((element) {
total += element;
});
setState(() {
result = total.toString();
});
});
}

See also

FutureGroup is extremely useful and much less taken into account than it should be, so there’s not much documentation for it at the time of writing. The official documentation page for this class is available at https://api.flutter.dev/flutter/package-async_async/FutureGroup-class.html.   

Resolving errors in asynchronous code           

There are several ways to handle errors in your asynchronous code. In this recipe, you will see a few examples of dealing with errors, both using the then() callback and the async/await pattern.

Getting ready

In order to follow along with this recipe, you will need the following:

  • If you followed along with any of the previous recipes in this chapter, you’ll only need to edit the existing project.

How to do it…

We are going to divide this section into two sub-sections. In the first section, we will deal with the errors by using the then() callback function and in the second section, we will deal with those errors using the async/await pattern.

DEALING WITH ERRORS USING THE THEN() CALLBACK:

The most obvious way to catch errors in a then() callback is using the catchError callback. To do so, follow these steps:

  1. Add the following method to the the _FuturePageState class in the main.dart file:
  Future returnError() {
throw ('Something terrible happened!');
}
  1. Whenever a method calls returnError, an error will be thrown. To catch this error, place the following code in the onPressed method of the ElevatedButton:
          returnError()
.then((value){
setState(() {
result = 'Success';
});
}).catchError((onError){
setState(() {
result = onError;
});
}).whenComplete(() => print('Complete'));
  1. Run the app and click the GO! button. You will see that the catchError callback was executed, updating the result State variable as shown in the following screenshot:
  1. You should also see that the whenComplete callback was called as well. In DEBUG CONSOLE, you should see the Complete string:

DEALING WITH ERRORS USING ASYNC/AWAIT

When you use the async/await pattern, you can just handle errors with the try… catch syntax, exactly like you would when dealing with synchronous code. Refactor the handleError() method as shown here: 

Future handleError() async {
try {
await returnError();
}
catch (error) {
setState(() {
result = error;
});
}
finally {
print('Complete');
}
}

If you call handleError() in the onPressed method of the ElevatedButton, you should see that the catch was called, and the result is exactly the same as in the catchError callback.

How it works…

To sum it up, when an exception is raised during the execution of a FuturecatchError is called; whenComplete is called in any case, both for successful and error-raising Futures. When you use the async/await pattern, you can just handle errors with the try… catch syntax.

Again, handling errors with await/async is generally more readable than the callback equivalent.

See also

There’s a very comprehensive guide on error handling in Dart. Each concept explained in this tutorial applies to Flutter as well: https://dart.dev/guides/libraries/futures-error-handling.

Using Futures with StatefulWidgets

As mentioned previously, while Stateless widgets do not keep any state information, Stateful widgets can keep track of variables and properties, and in order to update the app, you use the setState() method. State is information that can change during the life cycle of a widget.

There are four core lifecycle methods that you can leverage in order to use Stateful widgets:

  • initState() is only called once when the State is built. You should place the initial setup and starting values for your objects here. Whenever possible, you should prefer this to the build() method.
  • build() gets called each time something changes. This will destroy the UI and rebuild it from scratch.
  • deactivate() and dispose() are called when a widget is removed from the tree: use cases of these methods include closing a database connection or saving data before changing route.

So let’s see how to deal with Futures in the context of the lifecycle of a widget.

Getting ready

In order to follow along with this recipe, you will need the following:

  • If you followed along with any of the previous recipes in this chapter, you’ll only need to edit the existing project.
  • For this recipe, we’ll use the geolocator library, available at https://pub.dev/packages/geolocator. Add it to your pubspec.yaml file. 

How to do it…

In this example, we want to find the user location coordinates and show them on the screen as soon as they are ready. Getting the coordinates is an asynchronous operation that returns a Future. Follow these steps:

  1. Create a  new file called geolocation.dart in the lib folder of your project.
  2. Create a new stateful widget, called LocationScreen.
  3. In the State class of the Geolocation widget, add the code that shows the user their current position. The final result is shown here:
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';

class LocationScreen extends StatefulWidget {
@override
_LocationScreenState createState() => _LocationScreenState();
}

class _LocationScreenState extends State<LocationScreen> {
String myPosition = '';
@override
void initState() {
getPosition().then((Position myPos) {
myPosition = 'Latitude: ' + myPos.latitude.toString() + ' - Longitude: ' + myPos.longitude.toString();
setState(() {
myPosition = myPosition;
});
});
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Current Location')),
body: Center(child:Text(myPosition)),
);
}

Future<Position> getPosition() async {
Position position = await Geolocator().getLastKnownPosition(desiredAccuracy: LocationAccuracy.high);
return position;
}
}
  1. In the main.dart file, in the home property of the MaterialApp in the MyApp class, add the call to LocationScreen:
home: LocationScreen(),
  1. Run the app. After a few seconds, you should see your position at the center of the screen. 

How it works…

getPosition() is an asynchronous method that returns a Future. It leverages a Geolocator class to retrieve the last known position of the user.

Now the question may arise: where should getPosition be called? As the position should be retrieved only once, the obvious choice should be leveraging the initState method, which only gets called once, when the widget is loaded. 

It is recommended to keep initState synchronous, therefore you can use the then syntax to wait for the callback and update the state of the widget. myPosition is a state String variable that contains the message the user will see after the device has retrieved the coordinates, and it includes the latitude and longitude.

In the build method, there is just a centered Text containing the value of myPosition, which is empty at the beginning and then shows the string with the coordinates.

There’s more…

There is no way to know exactly how long an asynchronous task might take, so it would be a good idea to give the user some feedback while the device is retrieving the current position, with a CircularProgressIndicator. What we want to achieve is to show the animation while the position is being retrieved, and as soon as the coordinates become available, hide the animation, and show the coordinates. We can achieve that with the following code in the build() method:

 @override
Widget build(BuildContext context) {
Widget myWidget;
if (myPosition == '') {
myWidget = CircularProgressIndicator();
} else {
myWidget = Text(myPosition);
}
return Scaffold(
appBar: AppBar(title: Text('Current Location')),
body: Center(child:myWidget),
);
}

If you cannot see the animation of the CircularProgressIndicator, it might mean your device is too fast: try purposely adding a delay before the Geolocator() call, with the instruction await Future<int>.delayed(const Duration(seconds: 3));.

There is also an easier way to deal with Futures and stateful widgets: we’ll see it in the next recipe!

See also

It is extremely important to understand the widget lifecycle in Flutter. Have a look at the official documentation here for more details: https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html.

Using the FutureBuilder to let Flutter manage your Futures

The pattern of retrieving some data asynchronously and updating the user interface based on that data is quite common. So common in fact that in Flutter, there is a widget that helps you remove some of the boilerplate code you need to build the UI based on Futures: it’s the FutureBuilder widget.

You can use a FutureBuilder to integrate Futures within a widget tree that automatically updates its content when the Future updates. As a FutureBuilder builds itself based on the status of a Future, you can skip the setState instruction, and Flutter will only rebuild the part of the user interface that needs updating.

FutureBuilder implements reactive programming, as it will take care of updating the user interface as soon as data is retrieved, and this is probably the main reason why you should use it in your code: it’s an easy way for the UI to react to data in a Future.

FutureBuilder requires a future property, containing the Future object whose content you want to show, and a builder. In the builder, you actually build the user interface, but you can also check the status of the data: in particular, you can leverage the connectionState of your data so you know exactly when the Future has returned its data.

For this recipe, we will build the same UI that we built in the previous recipe: Using Futures with StatefulWidgets. We will find the user location coordinates and show them on the screen, leveraging the Geolocator library, available at https://pub.dev/packages/geolocator.

Getting ready

You should complete the project in the previous recipe, Using Futures with StatefulWidgets, before starting this one. 

How to do it…

To implement this, follow these steps:

  1. Modify the getPosition() method. This will wait 3 seconds and then retrieve the current device’s position (see the previous recipe for the complete code):
  Future<Position> getPosition() async {
await Future<int>.delayed(const Duration(seconds: 3));
Position position = await
Geolocator().getLastKnownPosition(desiredAccuracy:
LocationAccuracy.high);
return position;
}
  1.  Write the following code in the build method of the State class:
 @override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Current Location')),
body: Center(child: FutureBuilder(
future: getPosition(),
builder: (BuildContext context, AsyncSnapshot<dynamic>
snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return CircularProgressIndicator();
}
else if (snapshot.connectionState ==
ConnectionState.done) {
return Text(snapshot.data);
}
else {
return Text('');
}
},
),
));
}

How it works…

In this recipe, getPosition is the Future we will pass to the FutureBuilder. In this case, initState is not required at all: FutureBuilder takes care of updating the user interface whenever there’s a change in the data and state information.

Note that there are two properties we are setting for FutureBuilder: the future, which in this example is our getPosition() method, and the builder.

The builder takes the current context and an AsyncSnapshot, containing all the Future data and state information: the builder must return a widget.

The connectionState property of the AsyncSnapshot object makes you check the state of the Future. In particular, you have the following:

  • waiting means the Future was called but has not yet completed its execution.
  • done means that the execution completed.

There’s more…

Now, we should never take for granted that a future completed the execution without any error. For exception handling, you can check whether the future has returned an error, making this class a complete solution to build your Future-based user interface. You can actually catch errors in a FutureBuilder, checking the hasError property of the Snapshot, as shown in the following code:

 else if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text('Something terrible happened!');
}
return Text(snapshot.data);
}

As you can see, FutureBuilder is an efficient, clean, and reactive way to deal with Futures in your user interface.

See also

FutureBuilder can really enhance your code when composing a UI that depends on a Future. There is a guide and a video to dig deeper at this address: https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html.

Turning navigation routes into asynchronous functions

In this recipe, you will see how to leverage Futures using Navigator to transform a Route into an async function: you will push a new screen in the app and then await the route to return some data and update the original screen.

The steps we will follow are these:

  • Adding an ElevatedButton that will launch the second screen.
  • On the second screen, we will make the user choose a color.
  • Once the color is chosen, the second screen will update the background color on the first screen.

Here, you can see a screenshot of the first screen:

And here, the second screen, which allows choosing a color with three ElevatedButtons:

Getting ready

In order to follow along with this recipe, there is the following requirement:

  • If you followed along with any of the previous recipes in this chapter, you’ll only need to edit the existing project.

How to do it…

We will begin by creating the first screen, which is a stateful widget containing a Scaffold with a centered ElevatedButton. Follow these steps:

  1. Create a new file, called navigation_first.dart.
  2. Add the following code to the navigation_first.dart file (note that in the onPressed of the button, we are calling a method that does not exist yet, called _navigateAndGetColor()):
import 'package:flutter/material.dart';

class NavigationFirst extends StatefulWidget {
@override
_NavigationFirstState createState() => _NavigationFirstState();
}

class _NavigationFirstState extends State<NavigationFirst> {
Color color = Colors.blue[700];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: color,
appBar: AppBar(
title: Text('Navigation First Screen'),
),
body: Center(
child: ElevatedButton(
child: Text('Change Color'),
onPressed: () {
_navigateAndGetColor(context);
}),
),
);
}
  1. Here comes the most interesting part of this recipe: we want to await the result of the navigation. The _navigateAndGetColor method will launch NavigationSecond and await the result from the Navigator.pop() method on the second screen. Add the following code for this method:
 _navigateAndGetColor(BuildContext context) async {
color = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => NavigationSecond()),
);
setState(() {
color = color;
});
}
  1. Create a new file called navigation_second.dart.
  2. In the navigation_second.dart file, add a new stateful widget, called NavigationSecond. This will just contain three buttons: one for blue, one for green, and one for red.
  3. Add the following code to complete the screen:
import 'package:flutter/material.dart';

class NavigationSecond extends StatefulWidget {
@override
_NavigationSecondState createState() => _NavigationSecondState();
}

class _NavigationSecondState extends State<NavigationSecond> {
@override
Widget build(BuildContext context) {
Color color;
return Scaffold(
appBar: AppBar(
title: Text('Navigation Second Screen'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
ElevatedButton(
child: Text('Red'),
onPressed: () {
color = Colors.red[700];
Navigator.pop(context, color);
}),
ElevatedButton(
child: Text('Green'),
onPressed: () {
color = Colors.green[700];
Navigator.pop(context, color);
}),
ElevatedButton(
child: Text('Blue'),
onPressed: () {
color = Colors.blue[700];
Navigator.pop(context, color);
}),
],),));}}
  1. In the home property of the MaterialApp in the main.dart method, call NavigationFirst:
home: NavigationFirst(),
  1. Run the app and try changing the colors of the screen.

How it works…

The key for this code to work is passing data from the Navigator.pop() method from the second screen. The first screen is expecting a Color, so the pop method returns a Color to the NavigationFirst screen.

This pattern is an elegant solution whenever you need to await a result that comes from a different screen in your app. We can actually use this same pattern when we want to await a result from a dialog window, which is exactly what we will do in the next recipe!

Getting the results from a dialog      

This recipe shows an alternative way of await-ing some data from another screen, which was shown in the previous recipe,  but this time, instead of using a full-sized page, we will use a dialog box: actually dialogs behave just like routes that can be await-ed.

An AlertDialog can be used to show pop-up screens that typically contain some text and buttons, but could also contain images or other widgets. AlertDialogs may contain a title, some content, and actions. The actions property is where you ask for the user’s feedback (think of “save,” “delete,” or “accept”).

There are also design properties such as elevation or background, or shape or color, that help you make an AlertDialog well integrated into the design of your app.

In this recipe, we’ll perform the same actions that we implemented in the previous recipe, Turning navigation routes into asynchronous functions. We will ask the user to choose a color in the dialog, and then change the background of the calling screen according to the user’s answer, leveraging Futures.

The dialog will look like the one shown in the following screenshot:

Getting ready

In order to follow along with this recipe, there is the following requirement:

  • The starting code for this recipe is available at

How to do it…

To implement this functionality, follow these steps:

  1. Add a new file to your project, calling it navigation_dialog.dart.
  2. Add the following code to the navigation_dialog.dart file: 
import 'package:flutter/material.dart';

class NavigationDialog extends StatefulWidget {
@override
_NavigationDialogState createState() => _NavigationDialogState();
}

class _NavigationDialogState extends State<NavigationDialog> {
Color color = Colors.blue[700];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: color,
appBar: AppBar(
title: Text('Navigation Dialog Screen'),
),
body: Center(
child: ElevatedButton(
child: Text('Change Color'),
onPressed: () {
}),
),
);
}
  1. Now create the asynchronous method that will return the chosen color, calling it showColorDialog, and marking it as async:
 _showColorDialog(BuildContext context) async {
color = null;
await showDialog(
barrierDismissible: false,
context: context,
builder: (_) {
return AlertDialog(
title: Text('Very important question'),
content: Text('Please choose a color'),
actions: <Widget>[
TextButton(
child: Text('Red'),
onPressed: () {
color = Colors.red[700];
Navigator.pop(context, color);
}),
TextButton(
child: Text('Green'),
onPressed: () {
color = Colors.green[700];
Navigator.pop(context, color);
}),
TextButton(
child: Text('Blue'),
onPressed: () {
color = Colors.blue[700];
Navigator.pop(context, color);
}),
],
);
},
);
setState(() {
color = color;
});
}
  1. In the build method, in the onPressed property of the ElevatedButton, call the _showColorDialog that you just created:
onPressed: () {
_showColorDialog(context);
}),
  1. In the home property of the MaterialApp in the main.dart method, call NavigationDialog:
home: NavigationDialog(),
  1. Run the app and try changing the background color of the screen.

How it works…

In the preceding code, note that the barrierDismissible property tells whether the user can tap outside of the dialog box to close it (true) or not (false). The default value is true, but as this is a “very important question,” we set it to false.

The way we close the alert is by using the Navigator.pop method, passing the color that was chosen: in this, an Alert works just like a Route.

Now we only need to call this method from the onPressed property of the “Change color” button:

onPressed: () {
_showColorDialog(context).then((Color value){
setState(() {
color = value;
});
});

This recipe showed the pattern of waiting asynchronously for data coming from an alert dialog.

Written by

XR Developer responsible for end-to-end development of XR solutions spanning multiple domains, by using various XR and WebXR libraries.

Leave a Reply