Home Flutter Books Dart Apprentice

10
Asynchronous Programming

You’ve come a long way in your study of the Dart programming language. This chapter is an important one, as it fills in the remaining gaps needed to complete your apprenticeship. In it, you’ll not only learn how to deal with code that takes a long time to complete, but along the way, you’ll also see how to handle errors, connect to a remote server and read data from a file.

Your computer does a lot of work, and it does the work so fast that you don’t usually realize how much it’s actually doing. Every now and then, though, especially on an older computer or phone, you may notice an app slow down or even freeze. This may express itself as jank during an animation: that annoying stutter that happens when the device is doing so much work that some animation frames get dropped.

Tasks that take a long time generally fall into two categories: I/O tasks, and computationally intensive tasks. I/O, or input-output, includes things like reading and writing files, accessing a database, or downloading content from the internet. These all happen outside the CPU, so the CPU has to wait for them to complete. Computationally intensive tasks, on the other hand, happen inside the CPU. These tasks may include things like decrypting data, performing a mathematical calculation, or parsing JSON.

As a developer, you have to think about how your app, and in particular your UI, will respond when it meets these time-consuming tasks. Can you imagine if a user clicked a download button in your app, and the app simply froze until the 20 MB download was complete? You’d be collecting one-star reviews in a hurry.

Thankfully, Dart has a powerful solution baked into the very core of the language that allows you to gracefully handle delays without blocking the responsiveness of your app.

Concurrency in Dart

A thread is a sequence of commands that a computer executes. Some programming languages support multithreading, which is running multiple threads at the same time, while others do not. Dart, in particular, is a single-threaded language.

“What? Was it designed back in 1990 or something?”

No, Dart was actually created in 2011, well into the age of multicore CPUs.

“What a waste of all those other processing cores!”

Ah, but no. This choice to be single-threaded was made very deliberately and has some great advantages as you’ll soon see.

Parallelism vs. concurrency

To understand Dart’s model for handling long-running tasks, and also to see why the creators of Dart decided to make Dart single-threaded, it’s helpful to understand the difference between parallelism and concurrency. In common English, these words mean approximately the same thing, but in computer science, there’s a distinction.

A problem with parallelism

Little Susie has four pieces of chocolate left in the box next to her bed. She used to have ten, but she’s already eaten six of them. She’s saved the best ones for last, because after school today, three of her friends are coming home with her. She can’t wait to share her chocolates with them. Imagine her horror, though, when she gets home and finds only two pieces of chocolate left in the box! After a lengthy investigation, it turns out that Susie’s brother had discovered her stash and helped himself to two of the chocolates. From that day on, Susie always locked her box whenever she left home.

Dart isolates

Dart’s single thread runs in what it calls an isolate. Each isolate has its own allocated memory area, which ensures that no isolate can access any other isolate’s state. That means that there’s no need for a complicated locking system. It also means that sensitive data is much more secure. Such a system greatly reduces the cognitive load on a programmer.

But isn’t concurrency slow?

If you’re running all of a program’s tasks on a single thread, it seems like it would be really slow. However, it turns out that that’s not usually the case. In the following image, you can see tasks running on two threads in the top portion, and the same tasks running on a single thread in the bottom portion.

Synchronous vs. asynchronous code

The word synchronous is composed of syn, meaning “together”, and chron, meaning “time”, thus together in time. Synchronous code is where each instruction is executed in order, one line of code immediately following the previous one.

print('first');
print('second');
print('third');
first
second
third

The event loop

You’ve learned that Dart is based around concurrency on a single thread, but how does Dart manage to schedule tasks asynchronously? Dart uses what it calls an event loop to execute tasks that had previously been postponed.

Running code in parallel

When people say Dart is single-threaded, they mean that Dart only runs on a single thread in the isolate. However, that doesn’t mean you can’t have tasks running on another thread. One example of this is when the underlying platform performs some work at the request of Dart. For example, when you ask to read a file on the system, that work isn’t happening on the Dart thread. The system is doing the work inside its own process. Once the system finishes its work, it passes the result back to Dart, and Dart schedules some code to handle the result in the event queue. A lot of the I/O work from the dart:io library happens in this way.

Futures

You now know, at a high level, how Dart handles asynchronous code with its event loop. Now it’s time to learn how to work with asynchronous code at a practical level.

The Future type

Dart has a type called Future, which is basically a promise to give you the value you really want later. Here’s the signature of a method that returns a future:

Future<int> countTheAtoms();

States for a future

Before a future completes, there isn’t really 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:

Example of a future

One easy way to see a future in action is with the Future.delayed constructor.

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

Getting the result with callbacks

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.

print('Before the future');

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

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

Getting the result with async-await

Callbacks are pretty easy to understand, but they can be hard to read, especially if you nest them in multiple layers. 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.

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

  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

Handling errors with try-catch blocks

The syntax of a try-catch block looks like this:

try {

} catch (error) {

} finally {

}

Try-catch blocks with async-await

Here’s what the future looks like inside the 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

Catching an error

In order to see what happens when there’s an error, add the following line to the try block on the line immediately before print('Value: $value'):

throw Exception('There was an error');
Before the future
Exception: There was an error
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 same reason: You can implement an interface with a mock network request class to see how your UI will react while the app is waiting for a response.

Creating a data class

The web API you’re going to use will return some data about a todo list item. The data will be in JSON format, so in order to convert that into a more usable Dart object, you’ll create a special class to hold the data.

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

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

  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, and then add the following dependency:

dependencies:
  http: ^0.12.2
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 {
  final url = 'https://jsonplaceholder.typicode.com/todos/1';
  final response = await http.get(url);
  final statusCode = response.statusCode;
  if (statusCode == 200) {
    final rawJsonString = response.body;
    final jsonMap = jsonDecode(rawJsonString);
    final todo = Todo.fromJson(jsonMap);
    print(todo);
  } else {
    throw HttpException('$statusCode');
  }
}
userId: 1
id: 1
title: delectus aut autem
completed: false

Handling errors

There are a few things that could go wrong with the code above, so you’ll need to be ready to handle any errors that come up. 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 run the code again. You should see the following output:

SocketException: 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';

Testing a JSON format exception

Replace the following line:

final rawJsonString = response.body;
final rawJsonString = 'abc';
FormatException: Unexpected character (at character 1)
abc
^

Mini-exercises

  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 the code above with a try-catch block.

Streams

A future represents a single value that will arrive in the future. A stream, on the other hand, represents multiple values that will arrive in the future. Think of a stream like a list of futures. You can imagine a stream meandering through the woods as the autumn leaves fall onto the surface of the water. Each time a leaf floats by, it’s like the value that a Dart stream provides.

Subscribing to a stream

The dart:io library contains a File class which allows you to read data from a file. First, you’ll read data the easy way using the readAsString method, which returns the contents of the file as a future. Then you’ll do it again by reading the data as a stream of bytes.

Adding an assets file

You need a text file to work with, so you’ll add that to your project now.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Reading as a string

Now that you’ve created the text file, replace your Dart code with the following:

import 'dart:io';

Future<void> main() async {
  final file = File('assets/text.txt');
  final contents = await file.readAsString();
  print(contents);
}

Increasing the file size

If the file is large, you can read it as a stream. This allows you to start processing the data more quickly, since you don’t have to wait to finish reading the entire file as you did in the last example.

Reading from a stream

Replace the contents in the body of the main function with the following code:

final file = File('assets/text_long.txt');
final stream = file.openRead();
stream.listen(
  (data) {
    print(data.length);
  },
);
65536
65536
65536
65536
65536
65536
52783

Using an asynchronous for loop

Just as you can use callbacks or async-await to get the value of a future, there are also two ways to get the values of a stream. In the example above, you used the listen callback. Here is the same example using an asynchronous for loop:

Future<void> main() async {
  final file = File('assets/text_long.txt');
  final stream = file.openRead();
  await for (var data in stream) {
    print(data.length);
  }
}

Error handling

Like futures, stream events can also produce an error rather than a value. You can handle errors using a callback or try-catch blocks.

Using a callback

One way to handle errors is to use the onError callback like so:

final file = File('assets/text_long.txt');
final stream = file.openRead();
stream.listen(
  (data) {
    print(data.length);
  },
  onError: (error) {
    print(error);
  },
  onDone: () {
    print('All finished');
  },
);

Using try-catch

The other way to handle errors on a stream is with a try-catch block in combination with async-await. Here is what that looks like:

try {
  final file = File('assets/text_long.txt');
  final stream = file.openRead();
  await for (var data in stream) {
    print(data.length);
  }
} on Exception catch (error) {
    print(error);
} finally {
  print('All finished');
}
FileSystemException: Cannot open file, path = 'assets/pink_elephants.txt' (OS Error: No such file or directory, errno = 2)
All finished

Cancelling a stream

As mentioned above, you may use the cancelOnError parameter to tell the stream that you want to stop listening in the event of an error. However, even if there isn’t an error, you should always cancel your subscription to a stream if you no longer need it. This allows Dart to clean up the memory the stream was using. Failing to do so can cause a memory leak.

import 'dart:async';
import 'dart:io';

Future<void> main() async {
  final file = File('assets/text_long.txt');
  final stream = file.openRead();
  StreamSubscription<List<int>> subscription;
  subscription = stream.listen(
    (data) {
      print(data.length);
      subscription.cancel();
    },
    cancelOnError: true,
    onDone: () {
      print('All finished');
    },
  );
}

Transforming a stream

Being able to transform a stream as the data is coming in is very powerful. In the examples above, you never did anything with the data except print the length of the list of bytes. Those bytes represent text, though, so you’re going to transform the data from numbers to text.

Viewing the bytes

Replace the contents of main with the following code:

final file = File('assets/text.txt');
final stream = file.openRead();
stream.listen(
  (data) {
    print(data);
  },
);
[76, 111, 114, 101, ... ]

Decoding the bytes

Next, you’ll take the UTF-8 bytes and convert them to a string.

import 'dart:convert';
import 'dart:io';

Future<void> main() async {
  final file = File('assets/text.txt');
  final stream = file.openRead();
  await for (var data in stream.transform(utf8.decoder)) {
    print(data);
  }
}

Mini-exercises

The following code produces a stream that outputs an integer every second and then stops after the tenth time.

Stream<int>.periodic(
  Duration(seconds: 1),
  (value) => value,
).take(10);

Isolates

Most of the time it’s fine to run your own code synchronously, and for long-running I/O tasks, you can use Dart libraries that return futures or streams. However you may sometimes discover that your code is too computationally expensive and degrades the performance of your app.

App stopping synchronous code

Have a look at this example:

String playHideAndSeekTheLongVersion() {
  var counting = 0;
  for (var i = 1; i <= 10000000000; i++) {
    counting = i;
  }
  return '$counting! Ready or not, here I come!';
}
print("OK, I'm counting...");
print(playHideAndSeekTheLongVersion());

App stopping asynchronous code

Since you’ve read this far in the chapter, you should be aware that making the function asynchronous doesn’t fix the problem:

Future<String> playHideAndSeekTheLongVersion() async {
  var counting = 0;
  await Future(() {
    for (var i = 1; i <= 10000000000; i++) {
      counting = i;
    }
  });
  return '$counting! Ready or not, here I come!';
}
print("OK, I'm counting...");
print(await playHideAndSeekTheLongVersion());

Spawning an isolate

When you’re used to using futures from the Dart I/O libraries, it’s easy to get lulled into thinking that futures always run in the background, but that’s not the case. If you want to run some computationally intensive code on another thread, then you’ll need to create a new isolate to do that.

Using a send port to return results

Add the new version of playHideAndSeekTheLongVersion as a top level method in your file:

import 'dart:isolate';

void playHideAndSeekTheLongVersion(SendPort sendPort) {
  var counting = 0;
  for (var i = 1; i <= 1000000000; i++) {
    counting = i;
  }
  sendPort.send('$counting! Ready or not, here I come!');
}

Spawning the isolate and listening for messages

Replace the main method with the following code:

Future<void> main() async {
  final receivePort = ReceivePort();

  final isolate = await Isolate.spawn(
    playHideAndSeekTheLongVersion,
    receivePort.sendPort,
  );

  receivePort.listen((message) {
    print(message);
    receivePort.close();
    isolate.kill();
  });
}

Challenges

Before moving on, here are some challenges to test your knowledge of asynchronous programming. 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: Whose turn is it?

This is a fun one and will test how well you understand how Dart handles asynchronous tasks. In what order will Dart print the text with the following print statements? Why?

void main() {
  print('1 synchronous');
  Future(() => print('2 event queue')).then(
    (value) => print('3 synchronous'),
  );
  Future.microtask(() => print('4 microtask queue'));
  Future.microtask(() => print('5 microtask queue'));
  Future.delayed(
    Duration(seconds: 1),
    () => print('6 event queue'),
  );
  Future(() => print('7 event queue')).then(
    (value) => Future(() => print('8 event queue')),
  );
  Future(() => print('9 event queue')).then(
    (value) => Future.microtask(
      () => print('10 microtask queue'),
    ),
  );
  print('11 synchronous');
}

Challenge 2: Care to make a comment?

The following link returns a JSON list of comments:

https://jsonplaceholder.typicode.com/comments

Challenge 3: Datastream

The following code allows you to stream content from the URL:

final url = Uri.parse('https://raywenderlich.com');
final client = http.Client();
final request = http.Request('GET', url);
final response = await client.send(request);
final stream = response.stream;

Challenge 4: Fibonacci from afar

In Challenge 4 of Chapter 4, you wrote some code to calculate the nth Fibonacci number. Repeat that challenge, but run the code in a separate isolate. Pass the value of n to the new isolate as an argument, and send the result back to the main isolate.

Key points

  • Dart is single-threaded and handles asynchronous programming through concurrency, rather than through parallelism.
  • Concurrency refers to rescheduling tasks to run later on the same thread, while parallelism refers to running tasks at the same time on different threads.
  • The way Dart implements the scheduling of asynchronous tasks is by using an event loop, which has an event queue and a microtask queue.
  • Synchronous code always runs first and cannot be interrupted. This is followed by anything in the microtask queue, and when these are completed, by any tasks in the event queue.
  • You may run Dart code on another thread only by spawning a new isolate.
  • Dart isolates do not share any memory state and may only communicate through messages.
  • Using a future, which is of type Future, tells Dart that the requested task may be rescheduled 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.
  • A stream, which is of type Stream, is a series of futures.
  • Using a stream enables you to handle data events as they happen rather than waiting for them all to finish.
  • You can handle errors on futures and streams with callbacks or try-catch blocks.

Where to go from here?

This chapter taught you how to use futures and streams, but a good next step would be learning how to create them yourself. There’s also a lot more that you can do through stream manipulation.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.