Chapters

Hide chapters

Dart Apprentice: Beyond the Basics

First Edition · Flutter · Dart 2.18 · VS Code 1.71

Dart Apprentice: Beyond the Basics

Section 1: 15 chapters
Show chapters Hide chapters

12. Futures
Written by Jonathan Sande

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

You’ve got dishes to wash, phone calls to return, clothes to dry and emails to write…aaaand you’ll get to them right after watching one more meme video. Why work so hard now when you’ve got so much time tomorrow?

You’re not the only one who’s good at procrastination. Dart is also an expert at rescheduling things for the future. In the previous chapter, you learned how Dart handles asynchronous code with its event loop. You also learned how to add tasks to the event and microtask queues using the Future class. In this chapter, you’ll shift your focus from the internal workings of the event loop and learn some practical uses of working with futures. These are asynchronous tasks that complete after some time.

Here are some common examples of tasks that Dart handles asynchronously:

  • Making network requests.
  • Reading and writing a file.
  • Accessing a database.

With each of these, you express your intent to perform the task but have to wait for the task to complete. This chapter will teach you how to make network requests, but the process for handling all these asynchronous operations is similar.

The Future Type

Dart’s Future type is a promise to complete a task or give you a value in the future. Here’s the signature of a function that returns a future:

Future<int> countTheAtoms();

Future itself is generic; it can provide any type. In this case, the future is promising to give you an integer. In your code, if you called countTheAtoms, Dart would quickly return an object of type Future<int>. In effect, this is saying, “Hey, I’ll get back to you with that int sometime later. Carry on!”, in which case you’d proceed to run whatever synchronous code is next.

Behind the scenes, Dart has passed your request on to, presumably, an atom-counting machine, which runs independently of your main Dart isolate. There’s nothing on the event queue at this point, and your main thread is free to do other things. Dart knows about the uncompleted future, though. When the atom-counting machine finishes its work, it tells Dart, which puts the result, along with any code you gave it to handle the result, on the event queue. Dart says, “Sorry that took so long. Who knew there were 9.2 quintillion atoms in that little grain of sand! I’ll put your handling code at the end of the event queue. Give the event loop a few milliseconds, and then it’ll be your turn.”

Note: Because the largest an int can be on a 64 bit system is 9,223,372,036,854,775,807, or 2^63 − 1, it would be better to use BigInt as the return type of countTheAtoms. Although slower, BigInt can handle arbitrarily large numbers. When int values are too big at compile time, there’s a compile-time error. However, at runtime, they overflow — that is, 9223372036854775807 + 1 == -9223372036854775808.

States for a Future

Before a future completes, there isn’t anything you can do with it. But after it completes, it will have two possible results: the value you were asking for or an error. This all works out to three different states for a future:

Izlivryivuz Votei Uxpib
Sagara jwuhix

Example of a Future

One easy way to see a future in action is with the Future.delayed constructor. You saw an example of that in the last chapter, so the following is a review:

// 1
final myFuture = Future<int>.delayed(
  // 2
  Duration(seconds: 1),
  // 3
  () => 42,
);
print(myFuture);
Instance of 'Future<int>'

Getting the Results

There are two ways to get at the value after a future completes. One is with callbacks, and the other is with async-await.

Using Callbacks

As you learned in Chapter 2, “Anonymous Functions”, a callback is an anonymous function that will run after some event has completed. In the case of a future, there are three callback opportunities: then, catchError and whenComplete. You used then in the last chapter, but you’ll see how all three work now.

print('Before the future');

final myFuture = Future<int>.delayed(
  Duration(seconds: 1),
  () => 42,
)
    .then(
      (value) => print('Value: $value'),
    )
    .catchError(
      (Object error) => print('Error: $error'),
    )
    .whenComplete(
      () => print('Future is complete'),
    );

print('After the future');
Before the future
After the future
Value: 42
Future is complete.

Using Async-Await

Callbacks are pretty easy to understand, but they can be hard to read, especially if you nest them. A more readable way to write the code above is using the async and await syntax. This syntax makes futures look much more like synchronous code.

Writing the Code

Replace the entire main function with the following:

// 1
Future<void> main() async {
  print('Before the future');

  // 2
  final value = await Future<int>.delayed(
    Duration(seconds: 1),
    () => 42,
  );
  print('Value: $value');

  print('After the future');
}
Before the future
Value: 42
After the future

Handing Potential Errors

You learned about try-catch blocks in Chapter 10, “Error Handling”. Here’s what the future you wrote above looks like inside a try-catch block:

print('Before the future');

try {
  final value = await Future<int>.delayed(
    Duration(seconds: 1),
    () => 42,
  );
  print('Value: $value');
} catch (error) {
  print(error);
} finally {
  print('Future is complete');
}

print('After the future');
Before the future
Value: 42
Future is complete
After the future

Asynchronous Network Requests

In the examples above, you used Future.delayed to simulate a task that takes a long time. Using Future.delayed is useful during app development for this reason: You can implement an interface with a mock network request class to see how your UI will react while the app waits for a response.

Creating a Data Class

The web API you’ll use will return some data about a to-do list item. The data will be in JSON format. To convert that to a more usable Dart object, you’ll create a special class to hold the data. Unsurprisingly, many people call this a data class. Such classes usually don’t contain many methods because the data is the focus.

class Todo {
  Todo({
    required this.userId,
    required this.id,
    required this.title,
    required this.completed,
  });

  factory Todo.fromJson(Map<String, dynamic> jsonMap) {
    return Todo(
      userId: jsonMap['userId'] as int,
      id: jsonMap['id'] as int,
      title: jsonMap['title'] as String,
      completed: jsonMap['completed'] as bool,
    );
  }

  final int userId;
  final int id;
  final String title;
  final bool completed;

  @override
  String toString() {
    return 'userId: $userId\n'
        'id: $id\n'
        'title: $title\n'
        'completed: $completed';
  }
}

Adding the Necessary Imports

The http package from the Dart team lets you make a GET request to a real server. Make sure your project has a pubspec.yaml file, then add the following dependency:

dependencies:
  http: ^0.13.5
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;

Making a GET Request

Now that you have the necessary imports, replace your main function with the following code:

Future<void> main() async {
  // 1
  final url = 'https://jsonplaceholder.typicode.com/todos/1';
  final parsedUrl = Uri.parse(url);
  // 2, 3
  final response = await http.get(parsedUrl);
  // 4
  final statusCode = response.statusCode;
  if (statusCode != 200) {
    throw HttpException('$statusCode');
  }
  // 5
  final jsonString = response.body;
  dynamic jsonMap = jsonDecode(jsonString);
  // 6
  final todo = Todo.fromJson(jsonMap);
  print(todo);
}
userId: 1
id: 1
title: delectus aut autem
completed: false

Handling Errors

A few things could go wrong with the code above, so you’ll need to be ready to handle any errors. First, surround all the code inside the body of the main function with a try block:

try {
  final url = 'https://jsonplaceholder.typicode.com/todos/1';
  // ...
}
on SocketException catch (error) {
  print(error);
} on HttpException catch (error) {
  print(error);
} on FormatException catch (error) {
  print(error);
}

Testing a Socket Exception

Turn off your internet and rerun the code. You should see the following output:

Failed host lookup: 'jsonplaceholder.typicode.com'

Testing an HTTP Exception

Change the URL to the following:

final url = 'https://jsonplaceholder.typicode.com/todos/pink-elephants';
HttpException: 404
final url = 'https://jsonplaceholder.typicode.com/todos/1';

Exercise

  1. Use the Future.delayed constructor to provide a string after two seconds that says, “I am from the future.”
  2. Create a String variable named message that awaits the future to complete with a value.
  3. Surround your code with a try-catch block.

Creating a Future From Scratch

In the network request example, you simply used the future that the http library provided for you. Sometimes, though, you have to create a future from scratch. One example is when you implement an interface that requires a future.

abstract class DataRepository {
  double? fetchTemperature(String city);
}
abstract class DataRepository {
  Future<double> fetchTemperature(String city);
}
class FakeWebServer implements DataRepository {
  @override
  Future<double> fetchTemperature(String city) {
    return 42.0;
  }
}
A value of type 'double' can't be returned from the method 'fetchTemperature' because it has a return type of 'Future<double>'.

Using the Future Constructor

The most direct way of creating a future is to use one of the constructors of Future.

Unnamed Constructor

Replace your fetchTemperature implementation in FakeWebServer with the following:

@override
Future<double> fetchTemperature(String city) {
  return Future(() => 42.0);
}

Completing With a Value

Another way to specify that you want the future to complete with a value is to use the Future.value named constructor. Replace fetchTemperature in FakeWebServer with the new form:

@override
Future<double> fetchTemperature(String city) {
  return Future.value(42.0);
}

Completing With an Error

Remember that a future can complete with either a value or an error. If you want to return a future that completes with an error, use the Future.error named constructor.

@override
Future<double> fetchTemperature(String city) {
  return Future.error(ArgumentError("$city doesn't exist."));
}

Giving a Delayed Response

If you were making a Flutter app, it might be nice to wait a while before the future completes so you can see the circular spinner moving for a second or two in the UI. For that, use the Future.delayed constructor you’ve seen previously.

@override
Future<double> fetchTemperature(String city) {
  return Future.delayed(
    Duration(seconds: 2),
    () => 42.0,
  );
}
@override
Future<double> fetchTemperature(String city) {
  return Future.delayed(
    Duration(seconds: 2),
    () => throw ArgumentError('City does not exist.'),
  );
}

Using an Async Method

In addition to using the Future constructors, an easy way to create a future from scratch is to add the async keyword.

@override
Future<double> fetchTemperature(String city) async {
  return 42.0;
}

Using a Completer

The previous solutions are fine for most scenarios. However, if you need maximum flexibility, you can use the Completer class to create a future.

import 'dart:async';
@override
Future<double> fetchTemperature(String city) {
  // 1
  final completer = Completer<double>();
  if (city == 'Portland') {
    // 2
    completer.complete(42.0);
  } else {
    // 3
    completer.completeError(ArgumentError("City doesn't exist."));
  }
  // 4
  return completer.future;
}

Testing Your Future Out

Now that you’ve made your future, you can use it as you would any other future.

Future<void> main() async {
  final web = FakeWebServer();
  try {
    final city = 'Portland';
    final degrees = await web.fetchTemperature(city);
    print("It's $degrees degrees in $city.");
  } on ArgumentError catch (error) {
    print(error);
  }
}
It's 42.0 degrees in Portland.

Challenges

Before moving on, here are some challenges to test your knowledge of futures. It’s best if you try to solve them yourself, but if you get stuck, solutions are available in the challenge folder of this chapter.

Challenge 1: Spotty Internet

Implement FakeWebServer.fetchTemperature so it completes sometimes with a value and sometimes with an error. Use Random to help you.

Challenge 2: What’s the Temperature?

Use a real web API to get the temperature and implement the DataRepository interface from the lesson.

https://fcc-weather-api.glitch.me/api/current?lat=45.5&lon=-122.7

Challenge 3: Care to Make a Comment?

The following link returns a JSON list of comments:

https://jsonplaceholder.typicode.com/comments

Key Points

  • Using a future, which is of type Future, tells Dart that it may reschedule the requested task on the event loop.
  • When a future completes, it will contain either the requested value or an error.
  • A method that returns a future doesn’t necessarily run on a different process or thread. That depends entirely on the implementation.
  • You can handle errors from futures with callbacks or try-catch blocks.
  • You can create a future using a named or unnamed Future constructor, returning a value from an async method or using a Completer.

Where to Go From Here?

If you enjoyed making HTTP requests to access resources from a remote server, you should consider server-side development with Dart. Using a single language for both the front end and the back end is nothing short of amazing. No cognitive switching is required because everything you’ve learned in this book also applies to writing Dart code on the server.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now