Sande Jonathan Dart Apprentice Beyond The Basics PDF
Sande Jonathan Dart Apprentice Beyond The Basics PDF
Computer: Most any computer running a recent version of Windows, macOS or Linux.
Visual Studio Code: This book uses Visual Studio Code for the examples, but you can use another IDE if you prefer.
If you don’t have access to a computer with the above requirements, it’s also possible to
run most of the example code in this book by visiting dartpad.dev in your smartphone’s
web browser.
2
Dart Apprentice: Beyond the Basics
https://github.com/kodecocodes/dabb-materials/tree/editions/1.0
Forums
We’ve also set up an official forum for the book at https://forums.kodeco.com/c/books/dart-
apprentice-beyond-basics. This is a great place to ask questions about the book or to
submit any errors you may find.
3
Dart Apprentice: Beyond the Basics
iii Dedications
“To the greatest Coder of them all.”
— Jonathan Sande
4
Dart Apprentice: Beyond the Basics
5
Dart Apprentice: Beyond the Basics About the Team
6
Dart Apprentice: Beyond the Basics
v Acknowledgments
The predecessor of this book was named Dart Apprentice, which we later split and
expanded into Dart Apprentice: Fundamentals and Dart Apprentice: Beyond the Basics. The
original tech editors for Dart Apprentice were Brian Kayfitz and John Bennedict (JB)
Lorenzo. Their comments and suggestions greatly improved the content quality. Some of
Brian’s recommendations didn’t make it into the original Dart Apprentice but were
influential in designing the structure of the current two-book series. Chris Belanger was
an editor for the first edition of Dart Apprentice, and Joseph Howard created a video course
that influenced the structure and content of that book.
We would also like to thank Michael Thomsen on the Dart Team at Google for reviewing
Dart Apprentice and giving recommendations for updated content to include in this edition.
7
Dart Apprentice: Beyond the Basics
vi Introduction
Dart is a modern and powerful programming language. Google intentionally designed it to
be unsurprising. In many ways, it’s a boring language, and that’s a good thing! It means
Dart is fast and easy to learn. While Dart does have some unique characteristics, if you
have any experience with other object-oriented or C-style languages, you’ll immediately
feel at home with Dart.
Many people are learning Dart because of the Flutter UI framework. It was no accident that
Flutter chose Dart as its language. The Dart virtual machine allows lightning-fast
development-time rebuilds, and its ahead-of-time compiler creates native applications for
every major platform. As one of the most versatile languages on the market today, you can
use Dart to write anything from command-line apps and backend servers to native
applications for Android, iOS, web, Mac, Windows, Linux and even embedded devices.
It’s no wonder then that developers across the world have taken notice. Rather than
completely rewriting the same application in different languages for multiple platforms,
developers save countless hours by using a single language and a shared codebase. This
translates to a win for companies as well because they save money without sacrificing
speed.
So, welcome!
1 Dart Apprentice: Fundamentals is the first of the two-part series. It covers basic programming concepts like
expressions, data types, control flow, loops, functions, classes and collections. Readers who complete this book
have reached the upper-beginner level.
2 Dart Apprentice: Beyond the Basics, the book you have here, builds on the concepts you learned in Dart Apprentice:
Fundamentals and introduces new topics like string manipulation, anonymous functions, inheritance, interfaces,
generics, error handling and asynchronous programming. If you complete this book, you can consider yourself a
solid intermediate-level programmer in Dart.
8
Dart Apprentice: Beyond the Basics Introduction
In addition to the starter project, chapters will also have final and challenge folders. You
can refer to the final folder if you get lost during the lesson. It’ll contain the code from that
lesson. Likewise, the challenge folder will contain the answers to the exercises and
challenges from that chapter. You’ll learn the most if you don’t copy and paste this code
but type it yourself.
Exercises
You’ll sometimes find exercises in the middle of a chapter after learning about some topic.
These are optional but generally easy to complete. Like the challenges, they’ll help you
solidify what you’re learning.
Challenges
Challenges are an important part of Dart Apprentice: Beyond the Basics. At the end of each
chapter, the book will give you one or more tasks to accomplish that make use of the
knowledge you learned in the chapter. Completing them will not only help you reinforce
that knowledge but will also show that you’ve mastered it.
For readers coming from Dart Apprentice: Fundamentals, you’ll learn the most by following
along and trying each of the code examples, exercises and challenges as you come to
them. The way to learn to code is by writing code and experimenting with it. That can’t be
emphasized enough.
More advanced readers may want to skim the content of this book to get up and running
quickly. If that’s you, try the challenges at the end of every chapter. If they’re easy, move
on to the next chapter. If they’re not, go back and read the relevant parts of the chapter
and check the challenge solutions.
Finally, for all readers, Kodeco is committed to providing quality, up-to-date learning
materials. We’d love to have your feedback. What parts of the book gave you one of those
aha learning moments? Was some topic confusing? Did you spot a typo or an error? Let us
know at forums.kodeco.com and look for the particular forum category for this book. We’ll
make an effort to take your comments into account in the next update of the book.
9
Dart Apprentice: Beyond the Basics
1 String Manipulation
Written by Jonathan Sande
If you came to this chapter hoping to learn how to knit or crochet, you’ll have to find
another book. In this chapter, you’ll learn how to manipulate text by adding and removing
characters, splitting and re-joining strings, and performing search-and-replace
operations. Another essential skill this chapter will teach you is how to validate user input.
Regular expressions are a powerful tool for that, and in addition to string validation, you
can also use them to extract text. Hold on to your hat because you’re about to say goodbye
to the land of beginners.
The method toLowerCase creates a new string where all the capital letters are lowercase.
Run that, and you’ll get a string your database will thank you for:
10
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
Trimming
One common thing you’ll want to remove is extra whitespace at the beginning or end of a
string. Whitespace can be problematic because two strings might appear to be the same
but are actually different. Removing this whitespace is called trimming.
trimmed no longer contains the extra spaces at the beginning or end of the string. This
works for not only the space character but also the newline character, tab character or any
other Unicode-defined White_Space character.
Use trimLeft or trimRight if you only need to trim whitespace from one end.
Padding
In contrast to trimming, sometimes you need to add extra space or other characters to the
beginning or end of a string. For example, what if you’re making a digital clock? The naive
approach would be to form your string like so:
You need to take the remainder after dividing by 60 to get minutes and seconds because
there might be more than 59 minutes and seconds in some duration, which is true in this
case where the total duration is over an hour.
Running the code above gives a result of 1:32:57 . This is reasonable for a digital clock.
However, changing the duration slightly will show the problem. Replace the first line
11
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
Rerun your code, and you’ll see the new result of timeString :
1:2:3
That doesn’t look much like a time string anymore. What you want is 1:02:03 .
Dart is here to the rescue again, this time with the padLeft method. You can use padLeft to
add any character, but in this case, you want to add zeros to the left of numbers less than
10.
The 2 in padLeft(2, '0') means you want the minimum length to be two characters long.
The '0' is the padding character you want to use. If you hadn’t specified that, the padding
would have defaulted to the space character.
Run the code again. This time, you’ll see the following result:
1:02:03
As you might have guessed, you can also use a padRight method to add characters to the
end of a string.
12
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
Martin,Emma,12,Paris,France
Smith,John,37,Chicago,USA
Weber,Hans,52,Berlin,Germany
Bio,Marie,33,Cotonou,Benin
Wang,Ming,40,Shanghai,China
Hernández,Isabella,23,Mexico City,Mexico
Nergui,Bavuudorj,21,Ulaanbaatar,Mongolia
The fields in this CSV file are ordered by surname, given name, age, city and country.
Take just one line of that file. Here’s how you would split that string at the commas to
access the fields:
The split method can split the string by any character, but here you specify that you want
it to split at ',' .
Run that code, and you’ll see that fields contains a list of strings like so:
Note that those are all separate strings now, which you can easily access. You learned how
to access the elements of a list in Dart Apprentice: Fundamentals, Chapter 12, “Lists”.
You can also go the other direction. Given some list of strings, you can join all the elements
together using the join method on List . This time use a dash instead of a comma for a
little extra variety:
Martin-Emma-12-Paris-France
13
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
Replacing
Find-and-replace is a common task you perform on any text document. You can also do the
same thing programmatically. For example, say you want to replace all the spaces with
underscores in some text. You can do this easily using the replaceAll method.
The first argument you give to replaceAll is the string you want to match — in this case, the
space character. The second argument is the replacement string, in this case, an
underscore.
Run the code above, and you’ll see the following result:
live_and_learn
If you only need to replace the first occurrence of some pattern, use replaceFirst instead of
replaceAll .
Exercises
1 Take a multiline string, such as the text below, and split it into a list of single lines. Hint: Split at the newline
character.
France
USA
Germany
Benin
China
Mexico
Mongolia
14
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
Building Strings
You learned about string concatenation in Dart Apprentice: Fundamentals, Chapter 4,
“Strings”, with the following example:
But using the + operator isn’t efficient when building up long strings one piece at a time.
The reason is that Dart strings are immutable — that is, they can’t be changed — so every
time you add two strings together, Dart has to create a new object for the concatenated
string.
Calling toString converts the string buffer to the String type. This is like the type
conversion you’ve seen when calling toInt to convert a double to the int type.
2
4
8
16
32
64
128
256
512
1024
Each power of two is printed on a new line. What if you wanted to print the numbers on a
single line, though, like so:
The print statement doesn’t allow you to do that directly. However, if you build the string
first, you can print it when you’re finished.
In every loop, you write the number to the buffer and add a space. There’s no need to call
buffer.toString() in this case because the print statement handles that internally.
Run the code above, and you should see the expected result:
Here are a few more situations where a string buffer will come in handy:
16
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
Exercise
Use a string buffer to build the following string:
*********
* ********
** *******
*** ******
**** *****
***** ****
****** ***
******* **
******** *
*********
String Validation
Hello, I’m a user of your app, and my telephone number is 555732872937482748927348728934723937489274 .
Hello, I’m a user of your app, and my credit card number is Pizza .
Hello, I’m a user of your app, and my address is '; DROP TABLE users; -- .
You should never trust user input. It’s not that everyone is a hacker trying to break into
your system — though you need to be on your guard against that, too — it’s just that a lot of
the time, innocent users make simple typing mistakes. It’s your job to make sure you only
allow data that’s in the proper format.
Verifying that user text input is in the proper form is called string validation. Here are a
few common examples of string data you should validate:
Telephone numbers
Credit card numbers
Email addresses
Passwords
Even though some of these are “numbers”, you’ll still process them as strings.
17
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
You can check whether that string begins with the letter I using startsWith . Add the
following line at the end of main :
print(text.startsWith('I')); // true
startsWith returns a Boolean value, which is true in this case. Verify that by running the
code.
print(text.endsWith('Dart')); // true
print(text.contains('love')); // true
print(text.contains('Flutter')); // false
These examples are all very nice, but how would you verify that a phone number contains
only numbers or a password contains upper and lowercase letters, numbers and special
characters?
One possible solution would be to loop through every character and check whether its
code unit value falls within specific Unicode ranges.
32 48 0 64 @ 80 P 96 ` 112 p
33 ! 49 1 65 A 81 Q 97 a 113 q
34 '' 50 2 66 B 82 R 98 b 114 r
35 # 51 3 67 C 83 S 99 c 115 s
36 $ 52 4 68 D 84 T 100 d 116 t
37 % 53 5 69 E 85 U 101 e 117 u
40 ( 56 8 72 H 88 X 104 h 120 x
41 ) 57 9 73 I 89 Y 105 i 121 y
42 * 58 : 74 J 90 Z 106 j 122 z
43 + 59 ; 75 K 91 [ 107 k 123 {
45 - 61 = 77 M 93 ] 109 m 125 }
18
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
For example, an uppercase letter must fall within the Unicode range of 65-90, a lowercase
letter within 97-122, a number within 48-57 and special characters within other ranges,
depending on the specific characters you want to allow.
Checking every character like this would be tedious, though. There’s an easier way, which
you’ll learn about in the next section.
Regular Expressions
Regular expressions, sometimes called regex, express complex matching patterns in an
abbreviated form. Most programming languages support them, and Dart is no exception.
Although there are some syntax variations between languages, the differences are minor.
Dart shares the same syntax as regular expressions in JavaScript.
There are a few ways you can use this pattern. One is to call the hasMatch method like so:
print(regex.hasMatch('concatenation')); // true
print(regex.hasMatch('dog')); // false
print(regex.hasMatch('cats')); // true
hasMatch returns true if the regex pattern matches the input string. In this case, both
concatenation and cats contain the substring cat , so these return true , whereas dog
returns false because it doesn’t match the string literal cat .
An alternative method to accomplish the same task would be to use the contains method
on String like you did earlier:
print('concatenation'.contains(regex)); // true
print('dog'.contains(regex)); // false
print('cats'.contains(regex)); // true
Matching string literals has limited use. The power of regular expressions is in the special
characters.
19
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
Regular expressions use special characters that act as wildcards. You can use them to
match more than just literal characters.
The . dot character, for example, will match any single character.
print(matchSingle.hasMatch('cat')); // true
print(matchSingle.hasMatch('cot')); // true
print(matchSingle.hasMatch('cut')); // true
print(matchSingle.hasMatch('ct')); // false
Because the . matches any single character, it will match the a of cat , the o of cot
and the u of cut . This gives you much more flexibility in what you match.
The regex pattern c.t didn’t match the string ct because . always matches one
character. If you want to also match ct , use the pattern c.?t , where the ? question mark
is a special regex character that optionally matches the character before it. Because the
previous character is . , the pattern .? matches one or zero instances of any character.
print(optionalSingle.hasMatch('cat')); // true
print(optionalSingle.hasMatch('cot')); // true
print(optionalSingle.hasMatch('cut')); // true
print(optionalSingle.hasMatch('ct')); // true
Two special characters enable you to match more than one character:
+ : The plus sign means the character it follows can occur one or more times.
* : The asterisk means the character it follows can occur zero or more times.
20
o+ matched o , ooo and ooooooo but not the empty space between the w’s of w w . On
the other hand, o* matched everything, even the empty space.
If you want to allow multiple instances of any character, combine . with + or * . Write
the following example:
print(anyOneOrMore.hasMatch('ww')); // false
print(anyOneOrMore.hasMatch('wow')); // true
print(anyOneOrMore.hasMatch('w123w')); // true
print(anyOneOrMore.hasMatch('wABCDEFGw')); // true
The . regex will match any character, but it’s often useful to match a limited set or range
of characters. You can accomplish that using [] square brackets. Only the characters you
put inside the square brackets will be used to find a match.
print(set.hasMatch('bat')); // true
print(set.hasMatch('bot')); // true
print(set.hasMatch('but')); // false
print(set.hasMatch('boat')); // false
print(set.hasMatch('bt')); // false
21
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
You can also specify ranges inside the brackets if you use the - dash character:
print(letters.hasMatch('a')); // true
print(letters.hasMatch('H')); // true
print(letters.hasMatch('3z')); // true
print(letters.hasMatch('2')); // false
The regex '[a-zA-Z]' contains two ranges: all of the lowercase letters from a to z and all
of the uppercase letters from A to Z . There will be a match as long as the input string
has at least one lower or uppercase letter.
If you want to specify which characters not to match, add ^ just after the left bracket:
What if you want to match a special character itself? You can escape it by prefixing the
special character with a \ backslash. However, because the backslash is also a special
character in Dart strings, it’s usually better to use raw Dart strings whenever you create
regular expressions. Do you still remember how to create raw strings in Dart? Prefix them
with r , which stands for “raw”.
print(escaped.hasMatch('c.t')); // true
print(escaped.hasMatch('cat')); // false
If you hadn’t prefixed the regex pattern with r , you would have needed to write 'c\\.t'
with two backslashes, one to escape the \ special character in Dart and one to escape the
. special character in regular expressions.
22
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
In the future, this book will always use raw Dart strings for regular expressions. The only
reason you wouldn’t is if you needed to insert a Dart variable using interpolation. See Dart
Apprentice: Fundamentals, Chapter 4, “Strings”, for a review on string interpolation.
If you want to validate that a phone number contains only numbers, you might expect to
use the following regular expression:
This does match the range of numbers from 0 to 9. However, you’ll discover a problem if
you try to match the following cases:
print(numbers.hasMatch('5552021')); // true
print(numbers.hasMatch('abcefg2')); // true
That second one shouldn’t be a valid phone number, but it passes your validation check
because it does contain the number 2.
print(onlyNumbers.hasMatch('5552021')); // true
print(onlyNumbers.hasMatch('abcefg2')); // false
^ : Matches the beginning of the string.
[0-9] : Matches one number in the range 0-9.
+ : Matches one or more instances of the previous character, in this case, one or more numbers in the range 0-9.
$ : Matches the end of the string.
In summary, the regex ^[0-9]+$ only will match strings that contain numbers from start to
end.
Note: The ^ character has two meanings in regex. When used inside []
square brackets, it means “not”. When used elsewhere, it matches the beginning
of the line.
23
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
Here’s how you might validate a password where you require the password to contain at
least one of each of the following:
Lowercase letter.
Uppercase letter.
Number.
Write the following code in main to demonstrate how this would work:
if (!password.contains(lowercase)) {
print('Your password must have a lowercase letter!');
} else if (!password.contains(uppercase)) {
print('Your password must have an uppercase letter!');
} else if (!password.contains(number)) {
print('Your password must have a number!');
} else {
print('Your password is OK.');
}
This first checks for lowercase, then uppercase and finally numbers.
You probably noticed that a short password like Pw1 would still work, so you’ll also want
to enforce a minimum length. One way to do that would be like so:
24
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
You could also accomplish the same task by using a regular expression:
if (!password.contains(goodLength)) {
print('Your password must be at least 12 characters long!');
}
The {} curly braces indicate a length range in regex. Using {12} means a length of
exactly 12, {12,15} means a length of 12 to 15 characters, and {12,} means a length of at
least 12 with no upper limit. Because {12,} follows the . character, you’re allowing 12 or
more of any character.
Note: Although regular expressions are powerful, they’re also notoriously hard
to read. When you have a choice, go for the more readable option. In this case,
using password.length is perhaps the better choice. But that’s subjective, and the
goodLength name is also fairly readable, so you’ll have to make that call.
Regex Summary
The table below summarizes the regular expression special characters you’ve already
learned, plus a few more you haven’t:
25
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
This list isn’t exhaustive, but it should get you pretty far.
Exercise
Validate that a credit card number contains only numbers and is exactly 16 digits long.
Extracting text
Another common task when manipulating strings is extracting chunks of text from a
longer string. You’ll learn two ways to accomplish this, one with substring and another with
regex groups.
<!DOCTYPE html>
<html>
<body>
<h1>Dart Tutorial</h1>
<p>Dart is my favorite language.</p>
</body>
</html>
Say you want to extract the text Dart Tutorial , which is between the <h1> and </h1> tags.
26
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
The D of Dart Tutorial is the 34th character in the string, and the final l of Dart Tutorial is
the 46th character. The substring method extracts a string between two indexes in a longer
string. The start index is inclusive, and the end index is exclusive. Exclusive means the
range doesn’t include that index. For example, if you write 47 as the end index, the last
character in the range will be at index 46 . This might seem strange, but it works out well
in a zero-based indexing system where the length of the string is also the end index of the
final character.
You’re now probably asking, “How in the world do I know what the index numbers are?”
Good question. The indexOf method will help you with that.
Calling indexOf('<h1>') finds where <h1> begins in the text, which turns out to be at index
30 . To find the beginning of Dart Tutorial , you need to add the length of the <h1> tag
itself, which is 4 . Adding 30 and 4 gives the start index of 34 . To find the end index,
simply search for the closing tag </h1> . Because the end index is exclusive, index 47 is
exactly what you want.
Run the code again, and you’ll see the same result.
What if there are multiple headers? In that case, you can provide a minimum start index
to indexOf as you loop through every match.
27
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
Replace main with the following example:
var position = 0;
while (true) {
var start = text.indexOf('<h1>', position) + '<h1>'.length;
var end = text.indexOf('</h1>', position);
if (start == -1 || end == -1) {
break;
}
final heading = text.substring(start, end);
print(heading);
position = end + '</h1>'.length;
}
Here, you use position to track where you are in the string. After extracting one match, you
move position to after the end index to find the next match on the next loop. indexOf will
only find the first match after the specified position. If no match is found, then indexOf will
return -1 and you can stop searching.
Dart Tutorial
Flutter Tutorial
Other Tutorials
Using the same text as in the last example, add the following code to the end of main :
// 1
final headings = RegExp(r'<h1>(.+)</h1>');
// 2
final matches = headings.allMatches(text);
28
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
Here are explanations of the numbered comments:
1 <h1> and </h1> match literal characters in the text, and .+ matches everything between them.
Surrounding .+ with parentheses, as in (.+) , marks this text as a regex group.
2 The original text has three headings that match the regex pattern, so matches will be a collection of three.
3 group(1) holds the text from the regex group you made earlier using parentheses. This example only used one
set of parentheses. If you had used a second set of parentheses, you could access that text using group(2) .
Run the code, and you’ll see the text of the three matches printed to the console:
Dart Tutorial
Flutter Tutorial
Other Tutorials
Challenges
You’ve come a long way. Before going on, try out a few challenges to test your string
manipulation ability. If you need the answers, you can find them in the supplemental
materials accompanying the book.
Extract the relevant parts of the string and print them in the following format:
minutes: 00
seconds: 12
hundredths: 34
lyrics: Row, row, row your boat
Solve the problem twice, once with substring and once with regex groups.
29
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
Key Points
The String class contains many built-in methods to modify strings, including trim , padLeft , padRight ,
split , replaceAll and substring .
When building a string piece by piece, using StringBuffer is the most efficient.
You can extract strings from text with String.substring or regex groups.
30
Dart Apprentice: Beyond the Basics Chapter 1: String Manipulation
The example in the image above finds every line that begins with a capital letter.
Combine that with replacement, and you can even use regex groups. Use $1 in the
Replacement field to capture the first group from the Find field.
The example in the image above would find something like this:
print(text.startsWith('I'))
text.startsWith('I')
31
Dart Apprentice: Beyond the Basics
2 Anonymous Functions
Written by Jonathan Sande
No, anonymous functions aren’t the secret agents of the Dart world, sneaking around
cloak-and-dagger style. They’re just functions without names. In fact, they’re simply
values. Just as 2 is an int value, 3.14 is a double value, 'Hello world' is a String value
and false is a bool value, an anonymous function is a Function value. You can assign
anonymous functions to variables and pass them around as arguments just as you would
any other value. Dart treats functions as first-class citizens.
The ability to pass functions around makes it easy to perform an action on every collection
element or tell a button to run some code when a user presses it. This chapter will teach
you how to do all this and more.
Functions as Values
All the functions you saw in Dart Apprentice: Fundamentals were named functions, which
means, well, that they had a name.
Function name
Named function
But not every function needs a name. If you remove the return type and the function
name, what’s left is an anonymous function:
Anonymous function
The return type will be inferred from the return value of the function body — String in this
case. Removing the name and return type allows you to treat the resulting anonymous
function as a value.
32
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
int number = 4;
String greeting = 'hello';
bool isHungry = true;
number is an int , greeting is a String and isHungry is a bool . On the right side of each
assignment, you have literal values: 4 is an integer literal, 'hello' is a string literal and
true is a Boolean literal.
multiply is a variable of type Function , and the anonymous function you see to the right of
the = equals sign is a function literal.
Function namedFunction() {
return () => print('hello');
}
33
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
The return value is an anonymous function of type Function . In this case, rather than using
curly-brace syntax, you’re using arrow notation.
One of the most common places you’ll use higher-order functions is with collections. You’ll
often want to perform some task on every collection element. Iterable classes in Dart come
predefined with many methods that take anonymous functions as parameters.
The image below shows three examples of higher-order functions. Mapping is where you
transform every value into a new one. One example would be squaring each value.
Filtering allows you to remove elements from a collection, such as by filtering out all the
even numbers. Reducing consolidates a collection to a single value, such as by summing
the elements.
filter: 1 2 3 4 5 6 1 3 5
reduce: 2 4 6 8 10 12 42
There are many more methods than this small sample, though. Don’t worry — you’ll
discover them in time.
For‐Each Loops
while loops and for loops allow you to iterate using an index. for-in loops are convenient
for looping over all the elements of a collection without needing an index. Dart collections
also have a forEach method that will perform whatever task you like on each collection
element.
34
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
Then, call forEach on the list and pass in an anonymous function that triples each number
in the list and prints that value:
numbers.forEach((int number) {
print(3 * number);
});
All those parentheses and curly braces can get a little confusing. To clarify things, here’s
the collection with its forEach method:
numbers.forEach(
);
(int number) {
print(3 * number);
}
The number is the current element from the list as forEach iterates through the elements.
The function body then multiplies that value by three and prints the result.
Run the code, and you’ll see the following in the console:
3
6
9
Because Dart already knows the list elements are of type int , you can omit the type
annotation for the function parameter. Replace the expression above with the abbreviated
form:
numbers.forEach((number) {
print(3 * number);
});
35
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
Note: Choosing to omit the type is a matter of preference. The pro is that your
code is more concise; the con is that you can’t see at a glance what the type is.
Use whatever form you feel is more readable.
Because the anonymous function body only contains a single line, you can replace the
curly braces with arrow notation:
Note that the Effective Dart guide in the Dart documentation recommends against using
function literals in forEach loops. The standard way to loop over a collection is with a for-
in loop:
If, on the other hand, your function is in a variable, then it’s quite readable to still use a
forEach loop:
Map collections are not iterable, so they don’t directly support for-in loops. However, they
do have a forEach method.
36
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
final flowerColor = {
'roses': 'red',
'violets': 'blue',
};
flowerColor.forEach((flower, color) {
print('$flower are $color');
});
In this case, the anonymous function has two parameters: flower is the key and color is
the value. Because flowerColor is of type Map<String, String> , Dart infers that both flower
and color are of type String .
forEach performs a task on each collection element but doesn’t return any values. The
higher-order methods that follow will return values.
37
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
Mapping, however, allows you to accomplish the same thing without a loop. Dart
collections provide this functionality with a method named map .
Note: This section’s map method differs from the Map data type you’ve studied
previously. List , Set and Map all have a map method.
map produces a new collection by taking the anonymous function that you supply and
applying it to every element of the existing collection. In the example above, because
numbers is a list of int values, x is inferred to be of type int . The first time through the
loop, x is 2 ; the second time through, x is 4 ; and so on through 12 . The anonymous
function squares each of these values.
Note the parentheses surrounding the collection elements. They tell you this is an Iterable
rather than a List , which would have been printed with square brackets.
If you really want a List instead of an Iterable , call toList on the result:
print(mapped.toList());
It’s a common mistake to forget that map produces an Iterable rather than a List , but
now you know what to do. The reason List isn’t the default is for performance sake. Recall
that iterables are lazy. The resulting collection from map isn’t computed until you need it.
map gives you a collection with the same number of elements as the original collection.
However, the higher-order method in the next section will help you weed out unnecessary
elements.
38
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
Filtering a Collection
You can filter an iterable collection like List and Set using the where method.
Like map , the where method takes an anonymous function. The function’s input is also
each element of the list, but unlike map , the value the function returns must be a
Boolean. This is what happens for each element:
1.isOdd // true
2.isOdd // false
3.isOdd // true
4.isOdd // false
5.isOdd // true
6.isOdd // false
If the function returns true for a particular element, that element is added to the
resulting collection, but if it’s false , the element is excluded. Using isOdd makes the
condition true for odd numbers, so you’ve filtered down integers to just the odd values.
(1, 3, 5)
You can use where with List and Set but not with Map — unless you access the keys or
values properties of Map .
Consolidating a Collection
Some higher-order methods take all the elements of an iterable collection and consolidate
them into one value using the function you provide. You’ll learn two ways to accomplish
this.
Using Reduce
One way to combine all the collection elements into one value is to use the reduce
method. You can combine the elements any way you like, but the example below shows
how to find their sum.
Given the following list, find the sum of all the elements by passing in an anonymous
function that adds each element to the sum of the previous ones:
39
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
The first parameter, sum , is the accumulator. It remembers the current total as each
element is added. If you were to print sum and element on each function call, this would
be what you’d get:
sum: 2, element: 4
sum: 6, element: 6
sum: 12, element: 8
sum: 20, element: 10
sum: 30, element: 12
sum starts with the value of the first element in the collection, while element begins with
the second element.
Run this, and you’ll get an error. reduce can’t assign the first element to sum because
there’s no first element.
Delete that code and continue reading to see how fold can solve this problem for you.
Using Fold
Because calling reduce on an empty list gives an error, using fold will be more reliable
when a collection has a possibility of containing zero elements. The fold method works
like reduce , but it takes an extra parameter that provides a starting value for the function.
Here’s the same result as above, but this time using fold :
40
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
There are two arguments that you gave the fold method. The first argument, 0 , is the
starting value. The second argument takes that 0 , feeds it to sum and keeps adding to it
based on the value of each element in the list.
If you were to check the values of sum and element on each iteration, you’d get the
following:
sum: 0, element: 2
sum: 2, element: 4
sum: 6, element: 6
sum: 12, element: 8
sum: 20, element: 10
sum: 30, element: 12
This time, you can see that on the first iteration, sum is initialized with 0 while element
is the first element in the collection.
Print total again to see that the final result is still 42 , as it was with reduce .
Sorting a List
You’ve previously learned how to sort a list. For a refresher, though, call sort on the
desserts list below:
41
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
sort put them in alphabetical order. This is the default sorting order for strings.
Dart also allows you to define other sorting orders. The way to accomplish that is to pass in
an anonymous function as an argument to sort . Say you want to sort strings by length and
not alphabetically. Give sort an anonymous function like so:
The names d1 and d2 aren’t going to win any good naming prizes, but they fit on the
page of a book better than dessertOne and dessertTwo do.
-1 if the first value is smaller.
1 if the first value is larger.
The values you’re comparing here are the string lengths. This is all that sort needs to
perform the custom sort.
Print desserts again, and you’ll see the list is sorted by the length of each string:
First, you filtered the list with where , then you mapped the remaining elements to
uppercase strings and finally converted the iterable to a list.
[DONUTS, BROWNIES]
42
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
Using chains of higher-order methods like this is called declarative programming and is
one of the common features of functional programming. Previously, you’ve mostly used
imperative programming, in which you tell the computer exactly how to calculate the
result you want. With declarative programming, you describe the result you want and let
the computer determine the best way to get there.
Here’s how you would get the same result as you did using the code above, but
imperatively:
Exercise
Given the following exam scores:
final scores = [89, 77, 46, 93, 82, 67, 32, 88];
2 Use where to find all the B grades, that is, all the scores between 80 and 90 .
Callback Functions
When writing an application, you often want to run some code to handle an event, whether
that event is a user pressing a button or an audio player reaching the end of the song. The
functions that handle these events are called callback functions. They’re another
important use of anonymous functions.
You don’t have to do much Flutter programming before you meet a callback function. For
example, here’s how you might create a TextButton in Flutter:
43
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
TextButton(
child: Text('Click me!'),
onPressed: () {
print('Clicked');
},
)
TextButton is the class name, and it has two required named parameters: child and
onPressed . The item of interest here is onPressed , which takes an anonymous function as
the callback. Flutter runs the code in the callback function whenever the button is
pressed.
In the example here, you simply print “Clicked”. But the beauty of letting the user supply
the callback is that your button can do anything. It could send a message, turn on the TV
or launch a nuclear missile. Please don’t use it for the latter, though.
Void Callback
The example below will walk you through building a button with a callback method.
Because the anonymous function doesn’t take any parameters or return a value, it’s
commonly referred to as a void callback.
class Button {
Button({
required this.title,
required this.onPressed,
});
onPressed is a field name that will store whatever anonymous function the user passes in.
44
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
If you were building a full-fledged Button widget, you’d probably call onPressed from
somewhere within your class. However, because you haven’t implemented that for such a
basic example, you can just call the function externally as a proof of concept. Add the
following line at the bottom of main :
myButton.onPressed();
The name onPressed without parentheses is the function itself, whereas onPressed() with
parentheses calls the function. An alternative way to execute the function code is by
calling the call method on the function:
myButton.onPressed.call();
The example above works well, but there’s one minor problem.
In this case, you passed in an anonymous function that has a parameter named apple and
returns the integer 42 . Where does that apple come from? Where does that 42 go?
Nowhere. It isn’t the void function that your implementation is expecting. If you run that
function, you get a runtime crash.
A better approach would be to let users of your Button know at compile time that they can
only give onPressed a void function.
To do that, find your Button class and replace the line final Function onPressed; with the
following:
45
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
The void ensures users can’t supply a return value, and the () empty parentheses
ensure that they can’t give you a function with parameters.
The compiler lets you know that it doesn’t like anotherButton , so delete that from main .
class MyWidget {
MyWidget({
required this.onTouch,
});
Create an instance of MyWidget with its callback method in main like so:
Whenever onTouch is executed, this function says to print the value of the x position.
Normally, you would call onTouch internally within the widget as you listen to a gesture
detector, but you can call onTouch externally as well. Write the following in main :
myWidget.onTouch(3.14);
Because the function caller sets the parameter value, this is a value setter callback.
46
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
class AnotherWidget {
AnotherWidget({
this.timeStamp,
});
Setting the timeStamp property allows your widget to call the function anytime to retrieve
the value. An ISO-8601 string is a convenient format when you need to store a time stamp.
As with previous examples, timeStamp is normally a function that your widget would call
internally, but you can also call it externally:
In this case, you can’t call the function as timeStamp() with parentheses because the
function will be null if the user didn’t provide one. However, you can use the ?. null-
aware method invocation operator to optionally execute the function using call() .
2022-10-12T14:59:14.438099
That’s the precise time this chapter was being prepared for publishing.
47
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
Simplifying With Tear‐Offs
When you have a function, you can either execute the function immediately or hold a
reference to it. The difference is in the parentheses:
Being able to reference a function by its name allows you to make some simplifications.
For example, add the following class outside of your main method:
class StateManager {
int _counter = 0;
void handleButtonClick() {
_counter++;
}
}
This class represents a simple state management class that you might use in Flutter.
Pay attention to the anonymous function that you passed to onPressed . You’ll see many
people writing code like this. The author does it all the time. You can do better, though.
The () parentheses at the end of handleButtonClick() tell Dart to execute this function, but
the () { } syntax for the anonymous function tells Dart not to execute this function yet.
Dart stores it in the onPressed property for possible execution later. You’ve got a command
to execute and a command to not execute. These cancel each other out, so you have an
opportunity to simplify that syntax.
onPressed: () {
manager.handleButtonClick();
},
48
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
And replace them with this line:
onPressed: manager.handleButtonClick,
Because handleButtonClick doesn’t have parentheses after it, it isn’t executed right away.
This is known as a tear-off. You tore the handleButtonClick method off and converted it to a
function object to be stored in the onPressed property. This syntax is much cleaner.
Tear-offs work in other places, too. Say you want to print each element in a list. You could
do that like so:
But because the anonymous function and print have the same property, city , you can use
a tear-off instead:
cities.forEach(print);
Run that to see the names of each of these large Turkish cities printed to the console:
Istanbul
Ankara
Izmir
Bursa
Antalya
class Gizmo {
Gizmo({
required this.builder,
});
49
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
The type Map<String, int> Function(List<int>) is a function that takes a list of integers as input
and returns a map of String -to- int key-values pairs. That’s quite complex and hard to
read.
class Gizmo {
Gizmo({
required this.builder,
});
This is much more readable. Flutter takes this approach of giving aliases for many of its
callback and builder functions.
You can use typedef to rename other types as well. For example, write the following line
outside of main :
This doesn’t create a new type. Instead, ZipCode is just another way of referring to the int
type. You can observe that in the code below:
The purpose of the is keyword is to distinguish between types. However, in this case, is
treats int and its alias ZipCode exactly the same … because they’re the same.
50
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
Note: If you need a new type to store postal codes, you should create a class and
not a type alias. This will allow you to distinguish the postal code type from int
and validate its data. For example, you probably wouldn’t want to allow numbers
like -1 or 42 to be postal codes.
Exercise
1 Create a class named Surface .
2 Give the class a property named onTouch , a callback function that provides x and y coordinates but returns
nothing.
4 In Surface , create a method named touch , which takes x and y coordinates and then internally calls
onTouch .
5 In main , create an instance of Surface and pass in an anonymous function that prints the x and y
coordinates.
{
v a r age = 42 ;
(parameters) { another scope
a scope // have access to age
age = 43 ;
}
}
Scope
A scope in Dart is defined by a pair of curly braces. All the code within these braces is a
scope. You can even have nested scopes within other scopes. Examples of scopes are
function bodies and the bodies of loops.
51
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
Closure Example
var counter = 0;
final incrementCounter = () {
counter += 1;
};
The anonymous function that defines incrementCounter acts as a closure. It can access
counter , even though counter is neither a parameter of the anonymous function nor
defined in the function body.
incrementCounter();
incrementCounter();
incrementCounter();
incrementCounter();
incrementCounter();
print(counter); // 5
Function countingFunction() {
var counter = 0;
final incrementCounter = () {
counter += 1;
return counter;
};
return incrementCounter;
}
Each function returned by countingFunction will have its own version of counter . So if you
were to generate two functions with countingFunction , like so:
52
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
…then each call to those functions will increment its own counter independently:
print(counter1()); // 1
print(counter2()); // 1
print(counter1()); // 2
print(counter1()); // 3
print(counter2()); // 2
Admittedly, you probably won’t write self-counting functions every day. But this example
demonstrated another aspect of the Dart programming language.
In this chapter, you learned a bit about functional programming. In the next chapter, you’ll
dive into the essentials of object-oriented programming.
Challenges
Before moving on, here are some challenges to test your knowledge of anonymous
functions. It’s best if you try to solve them yourself, but solutions are available with the
supplementary materials for this book if you get stuck.
Challenge 1: Animalsss
Given the map below:
final animals = {
'sheep': 99,
'goats': 32,
'snakes': 7,
'lions': 80,
'seals': 18,
};
Use higher-order functions to find the total number of animals whose names begin with
“s”. How many sheep, snakes and seals are there?
Pass an anonymous function to repeatTask to square the input of 2 four times. Confirm
that you get the result 65536 because 2 squared is 4 , 4 squared is 16 , 16 squared
is 256 and 256 squared is 65536 .
53
Dart Apprentice: Beyond the Basics Chapter 2: Anonymous Functions
Key Points
Anonymous functions don’t have a function name, and the return type is inferred.
Dart functions are first-class citizens and thus can be assigned to variables and passed around as values.
Higher-order functions are functions that return functions or accept them as parameters.
Dart collections contain many methods that accept anonymous functions as parameters. Examples include
forEach , map , where , reduce and fold .
Chaining higher-order methods together is typical of declarative programming and allows you to solve many
problems without the loops of imperative programming.
Callback functions are anonymous functions that you provide to handle events.
Tear-offs are function objects with the same parameters as the method you pass them to, which allows you to omit
the parameters altogether.
The typedef keyword allows you to rename types so they’re shorter or easier to understand.
Anonymous functions act as closures, capturing any variables or functions within their scope.
54
Dart Apprentice: Beyond the Basics
3 Inheritance
Written by Jonathan Sande
Do you have your mother’s eyes or your father’s nose? You weren’t built from scratch. You
inherited your biological characteristics from your ancestors when their DNA was passed
down to you. Likewise, when building classes, you often don’t need to start from scratch.
In many situations, you’ll need to create a hierarchy of classes that share some base
functionality. You can create your own hierarchies by extending classes. This is also called
inheritance because the classes form a tree in which child classes inherit from parent
classes. The parent and child classes are also called superclasses and subclasses
respectively. The Object class is the top superclass for all non-nullable types in Dart. All
other classes, except Null , are subclasses of Object .
Object
Subclass
Subclass
Note: Although there’s no named top type in Dart, since all non-nullable Dart
types derive from the Object type and Object itself is a subtype of the nullable
Object? type, Object? can be considered in practice to be the root of the type
system.
enum Grade { A, B, C, D, F }
55
Dart Apprentice: Beyond the Basics Chapter 3: Inheritance
Creating Similar Classes
Next, create two classes named Person and Student .
Here’s Person :
class Person {
Person(this.givenName, this.surname);
String givenName;
String surname;
String get fullName => '$givenName $surname';
@override
String toString() => fullName;
}
class Student {
Student(this.givenName, this.surname);
String givenName;
String surname;
var grades = <Grade>[];
String get fullName => '$givenName $surname';
@override
String toString() => fullName;
}
Naturally, the Person and Student classes are very similar, since students are in fact
persons. The only difference at the moment is that a Student will have a list of grades .
56
Dart Apprentice: Beyond the Basics Chapter 3: Inheritance
The constructor parameter names don’t refer to this anymore. Whenever you see the keyword this , you should
remember that this refers to the current object, which in this case would be an instance of the Student class.
Since Student no longer contains the field names givenName and surname , using this.givenName or
this.surname would have nothing to reference.
In contrast to this , the super keyword is used to refer one level up the hierarchy. Similar to the forwarding
constructor that you learned about in Dart Apprentice: Fundamentals, Chapter 8, “Classes”, using super(givenName,
surname) passes the constructor parameters on to another constructor. However, since you’re using super
instead of this , you’re forwarding the parameters to the parent class’s constructor, that is, to the constructor of
Person .
Super Parameters
Rather than manually forwarding constructor parameters to the superclass, you can use
super plus the parameter name directly. Replace your Student class with the following
simplified form:
Now you’re no longer using a forwarding constructor, just directly setting the parameters
in the superclass. Super nice, huh?
57
Dart Apprentice: Beyond the Basics Chapter 3: Inheritance
If there are no parameters to pass to the superclass, you don’t need to write super()
because Dart always calls the default constructor for the superclass. The reason that you
or Dart always need to make the super call is to ensure that all of the field values have
finished initializing.
Jon Snow
Jane Snow
If you have a grade, you can only add that grade to the Student and not to the Person ,
because only the Student has grades . Add the following two lines to main :
The student jane now has one grade in the grades list.
@override
String get fullName => '$surname, $givenName';
58
Dart Apprentice: Beyond the Basics Chapter 3: Inheritance
You’ve seen the @override annotation before with the toString method. While using
@override is technically optional in Dart, it does help in that the compiler will give you an
error if you think you’re overriding something that doesn’t actually exist in the parent
class.
Run the code now and you’ll see the student’s full name printed differently than the
parent’s.
Jon Snow
Snow, Jane
class SomeParent {
void doSomeWork() {
print('parent working');
}
}
Since doSomeWork in the child class makes a call to super.doSomeWork , both the parent and
the child methods run. So if you were to call the child method like so:
parent working
child doing some other work
59
Dart Apprentice: Beyond the Basics Chapter 3: Inheritance
The parent method’s work was done first since you had the super call at the beginning of
the overridden method in the child. If you wanted to do the child method’s work first,
though, you would put the super call at the end of the method, like so:
@override
void doSomeWork() {
print('child doing some other work');
super.doSomeWork();
}
Multi‐Level Hierarchy
Back to the primary example again. Add more than one level to your class hierarchy by
defining a class that extends from Student .
Sibling Classes
Create a sibling class to SchoolBandMember named StudentAthlete that also derives from
Student .
60
Dart Apprentice: Beyond the Basics Chapter 3: Inheritance
In order to remain eligible for athletics, a student athlete has an isEligible getter that
makes sure the athlete has not failed any classes. The higher-order method every on the
grades list only returns true if every element of the list passes the given condition, which,
in this case, means that none of the grades is F .
Object
Person
Student
SchoolBandMember StudentAthlete
You see that SchoolBandMember and StudentAthlete are both students, and all students are
also persons.
Since they are all different types, what type is the list?
61
Dart Apprentice: Beyond the Basics Chapter 3: Inheritance
You can see that Dart has inferred the type of the list to be List<Student> . Dart used the
most specific common ancestor as the type for the list. It couldn’t use SchoolBandMember or
StudentAthlete since that doesn’t hold true for all elements of the list.
print(jessie is Object);
print(jessie is Person);
print(jessie is Student);
print(jessie is SchoolBandMember);
print(jessie is! StudentAthlete);
Knowing that jessie is a SchoolBandMember , first guess what Dart will show and then run the
code to see if you were right.
Ready? All five will print true since jessie is SchoolBandMember , which is a subclass of
Student , which is a subclass of Person , which is a subclass of Object . The only type that
jessie is not, is StudentAthlete — which you confirmed by using the is! keyword.
Note: The exclamation mark at the end of is! has nothing to do with the null
assignment operator from null safety. It just means not.
Having an object be able to take multiple forms is known as polymorphism. This is a key
part of object-oriented programming. You’ll learn to make polymorphic classes in an even
more sophisticated way in Chapter 4, “Abstract Classes”.
62
Dart Apprentice: Beyond the Basics Chapter 3: Inheritance
You may have already noticed this fact in the code above. For example, when you’re
overriding a method, do you need to call super ? And if you do, should you call super at the
beginning of the method, or at the end? Often the only way to know is to check the source
code of the parent class. Jumping back and forth between levels of the hierarchy can make
coding difficult.
Another problem with hierarchies is that they’re tightly bound together. Changes to a
parent class can break a child class. For example, say that you wanted to “fix” the Person
class by removing givenName and replacing it with firstName and middleName .
Doing this would also require you to update, or refactor, all of the code that uses the
subclasses as well. Even if you didn’t remove givenName , but simply added middleName ,
users of classes like StudentBandMember would be affected without realizing it.
Tight coupling isn’t the only problem. What if Jessie, who is a school band member, also
decides to become an athlete? Do you make another class called
SchoolBandMemberAndStudentAthlete ? What if she joins the student union, too? Obviously, things
could get out of hand quickly.
This has led many people to say, prefer composition over inheritance. The phrase means
that, when appropriate, you should add behavior to a class rather than share behavior with
an ancestor. It’s more of a focus on what an object has, rather than what an object is. For
example, you could flatten the hierarchy for Student by giving the student a list of roles,
like so:
class Student {
List<Role>? roles;
}
When you create a student, you could pass in the roles as a constructor parameter. This
would also let you add and remove roles later. Of course, since Dart doesn’t come with the
Role type, you’d have to define it yourself. You’d need to make Role abstract enough so
that a role could be a band member, an athlete or a student union member. You’ll learn
about making abstract classes like this in the next chapter.
All this talk of composition isn’t to say that inheritance is always bad. It might make sense
to still have Student extend Person . Inheritance can be good when a subclass needs all of
the behavior of its parent. However, when you only need some of that behavior, you should
consider passing in the behavior as a parameter, or perhaps even using a mixin, which
you’ll learn about in Chapter 6, “Mixins”.
63
Dart Apprentice: Beyond the Basics Chapter 3: Inheritance
At the same time, Flutter also makes good use of inheritance. For example,
StatefulWidget and StatelessWidget are both subclasses of Widget . The Widget class
itself is abstract, a concept you’ll learn about in the next chapter.
Challenges
Before moving on, here are some challenges to test your knowledge of inheritance. It’s
best if you try to solve them yourself, but solutions are available with the supplementary
materials for this book if you get stuck.
2 Create a subclass of Fruit named Melon and then create two Melon subclasses named Watermelon and
Cantaloupe .
3 Give the Student class a list of roles, including athlete, band member and student union member. You can use
an enum for the roles.
Key Points
A subclass has access to the data and methods of its parent class.
You can create a subclass of another class by using the extends keyword.
A subclass can override its parent’s methods or properties to provide custom behavior.
64
Dart Apprentice: Beyond the Basics
4 Abstract Classes
Written by Jonathan Sande
The classes and subclasses you created previously were concrete classes. It’s not that
they’re made of cement; it just means that you can make actual objects out of them. That’s
in contrast to abstract classes, from which you can’t make objects.
“What’s the use of a class you can’t make an object out of?” asks the pragmatist. “What’s
the use of ideas?” answers the philosopher. You deal with abstract concepts all the time,
and you don’t think about them at all.
Have you ever seen an animal? “Uh, are you seriously asking me that?” you answer. The
question isn’t “have you ever seen a chicken or a platypus or a mouse.” Have you ever seen
a generic animal, devoid of all features that are relevant to only one kind of animal — just
the essence of “animal” itself? What would that even look like? It can’t have four legs
because ducks are animals and they have two legs. It can’t have hair because rattlesnakes
are animals and they don’t have hair. Worms are animals, too, right? So there go the eyes
and bones.
No one has seen an “animal” in the abstract sense, but everybody has seen concrete
instances of things that fit the abstract animal category. Humans are good at generalizing
and categorizing the observations they make, and honestly, these abstract ideas are very
useful. They allow you to make short statements like “I saw a lot of animals at the zoo”
instead of “I saw a lion, an elephant, a lemur, a shark, …”
The same thing applies in object-oriented programming. After making lots of concrete
classes, you begin to notice patterns and more generalized characteristics of the classes
you’re writing. So when you come to the point of just wanting to describe the general
characteristics and behavior of a class without specifying the exact way that class is
implemented, you’re ready to write abstract classes. In some languages, this generalized
behavior is called a protocol, but in Dart it’s called an interface. You’ll learn about that in
Chapter 5, “Interfaces”. This chapter will prepare you by teaching you the mechanics of
creating abstract classes.
Don’t be put off by the word “abstract”. It’s no more difficult than the idea of an animal.
65
Dart Apprentice: Beyond the Basics Chapter 4: Abstract Classes
@override
String toString() {
return "I'm a $runtimeType";
}
}
The way you define an abstract class in Dart is to put the abstract keyword before
class .
In addition to the class itself being abstract, Animal also has two abstract methods:
eat and move . You know they’re abstract because they don’t have curly braces; they
just end with a semicolon.
Note that just because a class is abstract doesn’t mean that it can’t have concrete
methods or data. You can see that Animal has a concrete isAlive field, with a default
value of true . Animal also has a concrete implementation of the toString method,
which belongs to the Object superclass. The runtimeType property also comes from
Object and gives the object type at runtime.
66
Dart Apprentice: Beyond the Basics Chapter 4: Abstract Classes
Concrete Subclass
Create a concrete Platypus now. Stop thinking about cement. Just add the following empty
class to your IDE below your Animal class:
That’s not because you spelled platypus wrong. It really does have a y. Rather, the error is
because when you extend an abstract class, you must provide an implementation of any
abstract methods, which in this case are eat and move .
67
Dart Apprentice: Beyond the Basics Chapter 4: Abstract Classes
Starting a comment with TODO: is a common way to mark parts of your code where you
need to do more work. Later, you can search your entire project in VS Code for the
remaining TODOs by pressing Command+Shift+F on a Mac or Control+Shift+F on a PC
and writing “TODO” in the search box. You’re going to complete these TODOs right now,
though.
print('Munch munch');
A platypus may not have teeth, but it should still be able to munch.
print('Glide glide');
As was true with subclassing normal classes, abstract class subclasses can also have their
own unique methods. Add the following method to Platypus :
void layEggs() {
print('Plop plop');
}
Readers who are well-acquainted with how platypuses (Or is it platypi?) eat, swim and give
birth can make additional word suggestions for the next edition of this book.
68
Dart Apprentice: Beyond the Basics Chapter 4: Abstract Classes
true
Munch munch
Glide glide
Plop plop
I'm a Platypus
A concrete class has access to concrete data, like isAlive , from its abstract parent class.
Dart recognized that the runtime type was Platypus , even though runtimeType comes from Object and was
accessed in the toString method of Animal .
Dart infers platypus to be of type Platypus . That’s normal, but here’s the interesting part.
Replace that line with the following one, adding the Animal type annotation:
69
Dart Apprentice: Beyond the Basics Chapter 4: Abstract Classes
Now Dart sees platypus as merely an Animal with no more special ability to lay eggs.
Comment out the line calling the layEggs method:
// platypus.layEggs();
Run the code again paying special attention to the print(platypus) results:
I'm a Platypus
At compile time, Dart treats platypus like an Animal even though at runtime Dart knows it’s
a Platypus . This is useful when you don’t care about the concrete implementation of an
abstract class, but you only care that it’s an Animal with Animal characteristics.
Now, you’re probably thinking, “Making animal classes is very cute and all, but how does
this help me save data on the awesome new social media app I’m making?” That’s where
interfaces come in. See you in the next chapter!
Challenges
Before moving on, here’s a challenge to test your knowledge of abstract classes. It’s best if
you try to solve it yourself, but a solution is available with the supplementary materials for
this book if you get stuck.
Key Points
Abstract classes define class members and may or may not contain concrete logic.
70
Dart Apprentice: Beyond the Basics
5 Interfaces
Written by Jonathan Sande
Interfaces are similar to abstract classes in that they let you define the behavior you
expect for all classes that implement the interface. They’re a means of hiding the
implementation details of the concrete classes from the rest of your code. Why is that
important? To answer that question it’s helpful to understand a little about architecture.
Not the Taj Mahal kind of architecture, software architecture.
Software Architecture
When you’re building an app, your goal should be to keep core business logic separate
from infrastructure like the UI, database, network and third-party packages. Why? The
core business logic doesn’t change frequently, while the infrastructure often does. Mixing
unstable code with stable would cause the stable code to become unstable.
The following image shows an idealized app with the stable business logic in the middle
and the more volatile infrastructure parts surrounding it:
File I/O
3rd Party
Frameworks
Packages
Business Shared
UI
Logic Preferences
Web Database
71
Dart Apprentice: Beyond the Basics Chapter 5: Interfaces
The UI shouldn’t communicate directly with the web. You also shouldn’t scatter direct calls
to the database across your app. Everything goes through the central business logic. In
addition to that, the business logic shouldn’t know any implementation details about the
infrastructure.
This gives you a plug-in-style architecture, where you can swap one database framework
for another and the rest of the app won’t even know anything changed. You could replace
your mobile UI with a desktop UI, and the rest of the app wouldn’t care. This is useful for
building scalable, maintainable and testable apps.
Communication Rules
Here’s where interfaces come in. An interface is a description of how communication will
be managed between two parties. A phone number is a type of interface. If you want to call
your friend, you have to dial your friend’s phone number. Dialing a different number won’t
work. Another word for interface is protocol, as in Internet Protocol (IP) or Hypertext
Transfer Protocol (HTTP). Those protocols are the rules for how communication happens
among the users of the protocol.
When you create an interface in Dart, you define the rules for how one part of your
codebase will communicate with another part. As long as both parts follow the interface
rules, each part can change independently of the other. This makes your app much more
manageable. In team settings, interfaces also allow different people to work on different
parts of the codebase without worrying that they’re going to mess up someone else’s code.
Another related term you’ve probably heard before is API, or Application Programming
Interface. An API is the public-facing set of methods that allow one program or code base
to talk to another. Up to now, you’ve only been a consumer of other developers’ APIs. For
example, you’ve been using the API that came with the Dart SDK every time you write Dart
code. Or if you’ve experimented with Flutter, you might have used the Firebase API or
some other third-party API that you got from a Pub package. You’ve come to the point now,
though, where you’re ready to begin developing your own APIs.
Business
Interface Database
Login
72
Dart Apprentice: Beyond the Basics Chapter 5: Interfaces
The business logic doesn’t know anything about the database. It’s just talking to the
interface. That means you could even swap out the database for a completely different
form of storage, like cloud storage or file storage. The business logic doesn’t care.
There’s a famous adage related to this that goes, code against interfaces, not
implementations. You define an interface, and then you code your app to use that
interface only. While you must implement the interface with concrete classes, the rest of
your app shouldn’t know anything about those concrete classes, only the interface.
Note that repository is a common term to call an interface that hides the details of how
data is stored and retrieved. Also, the reason the result of fetchTemperature is nullable is
that someone might ask for the temperature in a city that doesn’t exist.
73
Dart Apprentice: Beyond the Basics Chapter 5: Interfaces
Besides the benefits mentioned previously, another great advantage of using an interface is that you can create
mock implementations to temporarily replace real implementations. In the FakeWebServer class, you’re simply
returning a random number instead of going to all the work of contacting a real server. This allows you to have a
“working” app until you get around to writing the code to contact the web server. This is also useful when you’re
testing your code and you don’t want to wait for a real connection to the server.
Speaking of waiting for a web server, a real interface would return a type of Future<double?> instead of returning
double? directly. However, you haven’t read Chapter 12, “Futures”, yet, so this example omits the Future part.
You could potentially use the FakeWebServer implementation directly like so:
But this defeats the whole point of trying to keep the implementation details separate
from the business logic. When you get around to swapping out the FakeWebServer with
another class, you’ll have to go back and make updates at every place in your business
logic that mentions it.
74
Dart Apprentice: Beyond the Basics Chapter 5: Interfaces
Ah, now your code on this side has no idea that that repository is actually FakeWebServer .
When it comes time to swap in the real implementation, you only need to update the
subclass returned by the factory constructor in the DataRepository interface.
Note: In the code above, you used a factory to return the concrete
implementation of the interface. There are other options, though. Do a search
for service locators (of which the get_it package is a good example) and
dependency injection to learn more about this topic.
This concept is key to the flexibility that Dart has as a language. The Dart VM implements
the interface one way and gives you the ability to hot-reload your Flutter apps. The dart
compile js tool implements the interface using JavaScript and gives you the ability to run
your code on the web. The dart compile exe tool implements the interface on Windows or
Linux or Mac to let you run your code on those platforms.
The implementation details are different for every platform, but you don’t have to worry
about that because your code will only talk to the interface, not to the platform. Are you
starting to see how powerful interfaces can be?
Extending vs Implementing
There are a couple of differences between extends and implements . Dart only allows you to
extend a single superclass. This is known as single inheritance, which is in contrast with
other languages that allow multiple inheritance.
75
Dart Apprentice: Beyond the Basics Chapter 5: Interfaces
But what’s the difference between just extending or implementing? That is, how are these
two lines different:
When you extend AnotherClass , SomeClass has access to any logic or variables in
AnotherClass . However, if SomeClass implements AnotherClass , SomeClass must provide its
own version of all methods and variables in AnotherClass .
Example of Extending
Assume AnotherClass looks like the following:
class AnotherClass {
int myField = 42;
void myMethod() => print(myField);
}
76
Dart Apprentice: Beyond the Basics Chapter 5: Interfaces
Check that SomeClass objects have access to AnotherClass data and methods:
Example of Implementing
Using implements in the same way doesn’t work:
The implements keyword tells Dart that you only want the field types and method
signatures. You’ll provide the concrete implementation details for everything yourself.
How you implement it is up to you, as demonstrated in the following example:
@override
void myMethod() => print('Hello');
}
This time you see your custom implementation results in 0 and Hello .
77
Dart Apprentice: Beyond the Basics Chapter 5: Interfaces
Challenges
Before moving on, here are some challenges to test your knowledge of interfaces. It’s best
if you try to solve them yourself, but solutions are available with the supplementary
materials for this book if you get stuck.
2 Create a concrete class called SodaBottle that implements Bottle and prints “Fizz fizz” when open is
called.
4 Instantiate SodaBottle by using the Bottle factory constructor and call open on the object.
Key Points
One rule of clean architecture is to separate business logic from infrastructure logic like the UI, storage, third-party
packages and the network.
Throughout the Dart Apprentice books, you’ve gotten a few ideas for writing clean code.
However, the principles of building clean architecture take clean coding to a whole new
level. You won’t master the skill all at once, but reading books and articles and watching
videos on the subject will help you grow as a software engineer.
78
Dart Apprentice: Beyond the Basics
6 Mixins
Written by Jonathan Sande
Mixins are an interesting feature of Dart that you might not be familiar with, even if you
know other programming languages. They’re a way to reuse methods or variables among
otherwise unrelated classes.
Note: For you Swift developers, Dart mixins work like protocol extensions.
Before showing you what mixins look like, you’ll first take a look at why you need them.
“It’s looking good!” you think. “I’m getting the hang of this.” So you try it out on Robin :
@override
void layEggs() {
print('Plop plop');
}
}
79
Dart Apprentice: Beyond the Basics Chapter 6: Mixins
Here’s the code you wrote for Platypus back in Chapter 3, “Inheritance”:
@override
String toString() {
return "I'm a $runtimeType";
}
}
@override
void move() {
print('Glide glide');
}
void layEggs() {
print('Plop plop');
}
}
Your layEggs code for Robin is exactly the same as it is for Platypus . That means you’re
duplicating code, which violates the DRY principle. If there are any future changes to
layEggs , you’ll have to remember to change both instances. Consider your options:
2 Birds probably shouldn’t extend Platypus , because who knows when you’re going to add the
stingWithVenomSpur method?
3 You could create an EggLayer class and have Bird and Platypus both extend that. But then what about
flying? Make a Flyer class, too? Dart only allows you to extend one class, so that won’t work.
4 You could have birds implement EggLayer and Flyer while Platypus implements only EggLayer . But then
you’re back to code duplication since implementing requires you to supply the implementation code for every
class.
80
Dart Apprentice: Beyond the Basics Chapter 6: Mixins
Mixing in Code
To make a mixin, you take whatever concrete code you want to share with different
classes, and package it in its own special mixin class.
mixin EggLayer {
void layEggs() {
print('Plop plop');
}
}
mixin Flyer {
void fly() {
print('Swoosh swoosh');
}
}
The mixin keyword indicates that these classes can only be used as mixins. You can also
use a normal class as a mixin as long as that class doesn’t extend another non- Object
class. So if you wanted to use EggLayer as a normal class, then just replace the mixin
keyword with class or abstract class .
Now refactor Robin as follows, using the with keyword to identify the mixins:
There are two mixins, so you separate them with a comma. Since those two mixins contain
all the code that Bird needs, the class body is now empty.
@override
void move() {
print('Glide glide');
}
}
81
Dart Apprentice: Beyond the Basics Chapter 6: Mixins
The layEggs logic has moved to the mixin. Now both Robin and Platypus share the code
that the EggLayer mixin contains. Just to make sure it works, run the following code:
Challenges
Before moving on, here are some challenges to test your knowledge of mixins. It’s best if
you try to solve them yourself, but solutions are available with the supplementary
materials for this book if you get stuck.
Challenge 1: Calculator
1 Create a class called Calculator with a method called sum that prints the sum of any two integers you give it.
2 Then make Platypus implement Comparable so that when you have a list of Platypus objects, calling
sort on the list will sort them by weight.
Key Points
Mixins allow you to share code between classes.
You can use any class as a mixin as long as it doesn’t extend anything besides Object .
Using the mixin keyword means that a class can only be used as a mixin.
82
Dart Apprentice: Beyond the Basics
7 Extension Methods
Written by Jonathan Sande
Up to this point in the book, you’ve been writing your own classes and methods. Often, though, you
use other people’s classes when you’re programming. Those classes may be part of a core Dart
library, or they may be from packages on Pub. In either case, you don’t have the ability to modify
them at will.
However, Dart has a feature called extension methods that allows you to add functionality to
existing classes. Even though they’re called extension methods, you can also add other members
like getters, setters or even operators.
Extension Syntax
To make an extension, you use the following syntax:
extension on SomeClass {
// your custom code
}
This should be located at the top level in a file, that is, not inside another class or function. Replace
SomeClass with whatever class you want to add extra functionality to.
You may give the extension itself a name if you like. In that case, the syntax is as follows:
You can use whatever name you like in place of YourExtensionName . The name is only used to show
or hide the extension when importing it in another library.
Have a look at a few of the following examples to see how extension methods work.
In this secret code, each letter will be bumped up to the next one. So a will be b, b will be c, and so
on. To accomplish that, you’ll increase the Unicode value of each code point in the input string by
1 . If the original message were “abc”, the encoded message should be “bcd”.
83
Dart Apprentice: Beyond the Basics Chapter 7: Extension Methods
You loop through each Unicode code point and increment it by 1 before writing it to output .
Finally, you convert the StringBuffer back to a regular String and return it.
Converting to an Extension
The next step is to convert the encode function above to an extension so that you can use it like so:
Since this extension won’t mutate the original string itself, a naming convention is to use an
adjective rather than a commanding verb. That’s the reason for choosing encoded , rather than
encode , for the extension name.
Like classes, extensions can’t be located inside of a function. So add the following code somewhere
outside of main :
extension on String {
String get encoded {
final output = StringBuffer();
for (final codePoint in runes) {
output.writeCharCode(codePoint + 1);
}
return output.toString();
}
}
84
Dart Apprentice: Beyond the Basics Chapter 7: Extension Methods
The keywords extension on are what make this an extension. You can add whatever you want inside the body. It’s as if
String were your own class now.
Rather than making a normal method, you can use a getter method. This makes it so that you can call the extension using
encoded , without the parentheses, rather than encoded() .
Since you’re inside String already, there’s no need to pass input as an argument. If you need a reference to the string
object, you can use the this keyword. Thus, instead of input.runes , you could write this.runes . However, this is
unnecessary and you can directly access runes . Remember that runes is a member of String and you’re inside
String .
If you compare this to the encoded method, though, there’s a lot of code duplication. Whenever you
see code duplication, you should think about how to make it DRY.
extension on String {
String get encoded => _code(1);
String get decoded => _code(-1);
85
Dart Apprentice: Beyond the Basics Chapter 7: Extension Methods
Now the private _code method factors out all of the common parts of encoded and decoded . That’s
better.
J!mjlf!fyufotjpot"
I like extensions!
Great! Now you can amuse your friends by giving them encoded messages. They’re actually a lot of
fun to solve.
extension on int {
int get cubed {
return this * this * this;
}
}
Notice the use of this to get a reference to the int object, which will be 5 in the example below.
print(5.cubed);
As you can see, you can do a lot with extensions. Although they can be very powerful, extensions
by definition add non-standard behavior, and this can make it harder for other developers to
understand your code. Use extensions when they make sense, but try not to overuse them.
86
Dart Apprentice: Beyond the Basics Chapter 7: Extension Methods
Uif!tfdsfu!up!mfbsojoh!Ebsu!xfmm!jt!up!dg"ewtkqwu"cpf"lwuv"vt{"vjkpiu0"Vlqfh#|rx*uh#uhdglqj#wklv/#wkdw#reylrxvo|
#ghvfulehv#|rx1#Kssh$nsf%
Challenges
Before moving on, here’s a challenge to test your knowledge of extension methods. It’s best if you
try to solve it yourself, but a solution is available with the supplementary materials for this book if
you get stuck.
Key Points
Extension methods allow you to give additional functionality to classes that are not your own.
Use extensions when they make sense, but try not to overuse them.
87
Dart Apprentice: Beyond the Basics
8 Generics
Written by Jonathan Sande
When first encountering a problem, your natural tendency is to find a solution that solves
that particular problem. You don’t worry about related problems; you care only about the
problem that’s troubling you now.
Say you want to learn French. You might begin by trying to memorize sentences from a
phrasebook. That turns out to be a slow and ineffective method. After trying several other
language-learning techniques, you discover that lots of easy listening and reading input
helps you learn much faster. Your problem is solved; you’ve learned French well enough to
understand and communicate.
Then, you decide to learn Chinese. Do you return to the phrasebooks? Of course not.
There’s no need to learn how to learn all over again. You already found a language-
learning method that worked for you with French. You can use that same method with
Chinese. You might need to pick up a few more techniques to help you learn those
characters, but the overall method remains the same: lots of easy listening and reading
input.
The more languages you learn, the better you get at learning languages. You’ve
generalized the language learning process to the point where you know exactly how you
would tackle any language.
Instead of French, Chinese or Urdu, now think String , bool and int . In Dart, generics
refers to generalizing specific types so you can handle them all similarly. This chapter will
teach not only how to use generic types but also how to create new generic classes and
collections.
Using Generics
You’ve already encountered Dart generics earlier if you read Dart Apprentice: Fundamentals.
In Chapter 12, “Lists”, you saw this example:
Whenever you see the < > angle brackets surrounding a type, you should think, “Hey,
that’s generics!” List is a generic collection. It can hold strings, integers, doubles or any
other type. By specifying <String> in angle brackets, you’re declaring this list will hold
only strings.
88
Dart Apprentice: Beyond the Basics Chapter 8: Generics
Each element in the list is a string: 'chips' is a string and so is 'nuts' . If you tried to add
the integer 42 , Dart would complain at you.
See for yourself. Replace the line above with the following:
The element type 'int' can't be assigned to the list type 'String'.
No integers are allowed in a string list! If you want to allow both integers and strings in the
same list, you can set the list type to Object , which is a supertype of both String and int .
Replace String in the line above with Object :
Because List is generic, it can contain any type. Here are some more examples:
These are all generics at work. A single type List can store an ordered collection of any
other type. There was no need to create different classes like IntList , DoubleList or BoolList
for each type. If the language had been designed like that, it would have been like
reinventing the wheel every time you needed a new list for a different type. Generics
prevents code duplication.
All Dart collections use generics, not just List . For example, Set and Map do as well:
89
Dart Apprentice: Beyond the Basics Chapter 8: Generics
Map uses generic types for both the key and the value. This means you can map int to
String , or String to int , or bool to double and so on.
Using generic classes is easy enough. Now, you’ll take your skills to the next level by
learning to create generic classes and functions.
Binary trees.
Priority Queues.
A binary tree is one of the simplest types of trees. It consists of nodes, where each node
can have up to two children. The image below illustrates this:
parent
A node with children is called a parent, and the children are differentiated by calling them
the left child and the right child.
In addition to having children, nodes also store a value. A tree that holds integers might
look like so:
1 9
0 5 8
90
Dart Apprentice: Beyond the Basics Chapter 8: Generics
The top node of the tree is called the root node. Whoever put the root at the top of the tree
was probably standing on their head that day.
The values in this particular tree are integers, but you could store any data type there.
Create the following class below the main method in your Dart project:
class IntNode {
IntNode(this.value);
int value;
IntNode? leftChild;
IntNode? rightChild;
}
IntNode has three properties. The constructor allows you to set the node’s value . leftChild
and rightChild are optional because not every node has children.
Now, create the tree you saw in the diagram above by adding the following function below
main :
IntNode createIntTree() {
final zero = IntNode(0);
final one = IntNode(1);
final five = IntNode(5);
final seven = IntNode(7);
final eight = IntNode(8);
final nine = IntNode(9);
seven.leftChild = one;
one.leftChild = zero;
one.rightChild = five;
seven.rightChild = nine;
nine.leftChild = eight;
return seven;
}
You return seven because it’s the root node and contains the links to the other nodes in
the tree. Returning any other node would only provide a portion of the tree. Parents link to
their children, not the other way around.
91
Dart Apprentice: Beyond the Basics Chapter 8: Generics
seven
one nine
For that, you would need to change the node’s data type. However, you can’t change the
data type of value in IntTree without messing up the integer tree you made earlier. So
create a new class like the one below:
class StringNode {
StringNode(this.value);
String value;
StringNode? leftChild;
StringNode? rightChild;
}
Next, add a function below main to create the tree of strings that you saw in the diagram
above:
92
Dart Apprentice: Beyond the Basics Chapter 8: Generics
StringNode createStringTree() {
final zero = StringNode('zero');
final one = StringNode('one');
final five = StringNode('five');
final seven = StringNode('seven');
final eight = StringNode('eight');
final nine = StringNode('nine');
seven.leftChild = one;
one.leftChild = zero;
one.rightChild = five;
seven.rightChild = nine;
nine.leftChild = eight;
return seven;
}
The logic is all the same as the IntNode tree you made earlier.
Create the tree in main like so:
class IntNode {
IntNode(this.value);
int value;
IntNode? leftChild;
IntNode? rightChild;
}
class StringNode {
StringNode(this.value);
String value;
StringNode? leftChild;
StringNode? rightChild;
}
And what if you wanted to make a tree of Boolean values? Here’s what the node class
would look like:
93
Dart Apprentice: Beyond the Basics Chapter 8: Generics
class BooleanNode {
BooleanNode(this.value);
bool value;
BooleanNode? leftChild;
BooleanNode? rightChild;
}
And then, if you decided you needed a tree of floating-point values, you’d have to create a
whole new class:
class DoubleNode {
DoubleNode(this.value);
double value;
DoubleNode? leftChild;
DoubleNode? rightChild;
}
And on it goes for every new data type you want to use. You must create a new class to
hold the new type, duplicating lots of code each time.
class Node<T> {
Node(this.value);
T value;
Node<T>? leftChild;
Node<T>? rightChild;
}
This time, the angle brackets show that this is a class with a generic type. The T here
represents any type. You don’t have to use the letter T, but it’s customary to use single
capital letters when specifying a generic type.
Now, replace createIntTree with the updated version that uses generics:
94
Dart Apprentice: Beyond the Basics Chapter 8: Generics
Node<int> createIntTree() {
final zero = Node(0);
final one = Node(1);
final five = Node(5);
final seven = Node(7);
final eight = Node(8);
final nine = Node(9);
seven.leftChild = one;
one.leftChild = zero;
one.rightChild = five;
seven.rightChild = nine;
nine.leftChild = eight;
return seven;
}
This time, the return type is Node<int> instead of IntNode . You specify int inside the
angle brackets so users of createIntTree know the values inside the tree are integers. Hover
your cursor over zero , and you’ll see that Dart already infers the type to be Node<int>
because it knows 0 is an integer.
Hover your cursor over intTree , and you’ll see that the inferred type is Node<int> . Dart
knows it because you wrote that as the return type of createIntTree .
Update createStringTree in the same way. Replace the previous function with the following
version:
Node<String> createStringTree() {
final zero = Node('zero');
final one = Node('one');
final five = Node('five');
final seven = Node('seven');
final eight = Node('eight');
final nine = Node('nine');
seven.leftChild = one;
one.leftChild = zero;
one.rightChild = five;
seven.rightChild = nine;
nine.leftChild = eight;
return seven;
}
95
Dart Apprentice: Beyond the Basics Chapter 8: Generics
The function return type is Node<String> this time instead of StringNode . Check that your
generic Node class works by hovering your cursor over zero . You’ll see the inferred type
is also Node<String> because 'zero' is a String . Your generic Node class works!
Generics are here to save the day because, in addition to generic classes, you can also
make generic functions.
createTree will take a list of some data type, be it int , String or whatever. Then the
function will convert the list to a binary tree. It’s possible to do that if you assume the first
element in the list is the root-node value, the second element is the left-child value, the
third element is the right-child value and so on, where the values in the list correspond to
levels in the tree. The image below shows the list values and indexes laid out in a binary
tree:
7
Level 1
1 1 9 2
Level 2
5 3 0 4 8 5
Level 3
Note: This data structure where a list stores the values of a tree is known as a
heap. Read Chapter 13, “Heaps”, in Data Structures & Algorithms in Dart to learn
more.
96
Dart Apprentice: Beyond the Basics Chapter 8: Generics
// 1
Node<E>? createTree<E>(List<E> nodes, [int index = 0]) {
// 2
if (index >= nodes.length) return null;
// 3
final node = Node(nodes[index]);
// 4
final leftChildIndex = 2 * index + 1;
final rightChildIndex = 2 * index + 2;
// 5
node.leftChild = createTree(nodes, leftChildIndex);
node.rightChild = createTree(nodes, rightChildIndex);
// 6
return node;
}
1 This time, the letter you’re using for the generic type is E instead of T . You could use T again, but it’s
customary to use E when creating a collection, which is a tree of nodes in this case. The E stands for elements.
2 When working with trees, recursive functions are very useful. A recursive function is a function that calls itself. If
a function always calls itself, though, it could go on forever. Thus, it needs a way to stop calling itself. That’s
known as the base case. The base case for this recursive function is when the list index is out of range.
3 Take the value in the list at the given index and convert it to a new node. The default value of index is 0 ,
which is the root node.
4 In a binary tree where the values are laid out in a list by level, you can calculate the index of the left child by
multiplying 2 times the parent index plus 1. The right child index is one beyond that.
5 Here’s the recursive part. The function calls itself to create the child nodes. You pass in the indexes where the child
values should be. If those indexes are out of range for the list, the base case will stop the recursion.
6 At the end of each recursion, node is the parent of some branch in the tree. And when all the recursions are
finished, node is the root node.
Did that make your brain hurt? No worries. Recursion does that to everybody. The point of
this chapter is to understand generics, not recursion. However, if you’re interested, the
best way to wrap your mind around recursion is to step through the code line by line and
track what the computer is doing. You can do that with a paper and pencil. Or in Chapter
10, “Error Handling”, you’ll learn how to use the debugger in VS Code to pause execution
and step through the code one line at a time. Feel free to jump ahead and learn how to do
that now.
97
Dart Apprentice: Beyond the Basics Chapter 8: Generics
Testing It Out
In main , write the following line:
You could write another recursive function to print the contents of the tree, but for now,
just print the values manually. Add the following code to main , below what you wrote
previously:
print(tree?.value);
print(tree?.leftChild?.value);
print(tree?.rightChild?.value);
print(tree?.leftChild?.leftChild?.value);
print(tree?.leftChild?.rightChild?.value);
print(tree?.rightChild?.leftChild?.value);
print(tree?.rightChild?.rightChild?.value);
Because the nodes of a tree could be null, you have to access the children with the ?.
operator.
7
1
9
0
5
8
null
98
Dart Apprentice: Beyond the Basics Chapter 8: Generics
1 9
0 5 8
createTree is generic, so you should be able to change the data type of the elements and
still have the function work. Replace the createTree([7, 1, 9, 0, 5, 8]) line above with the
following string version:
The numbers are the same, but this time you’re using strings.
seven
one
nine
zero
five
eight
null
In a BST, the left child must always be less than the value of its parent, while the right
child is always greater than or equal to the parent. Here’s an example:
40
18 77
1 20 70 105
4 25 45 88
99
Dart Apprentice: Beyond the Basics Chapter 8: Generics
18 is less than 40, so it goes on the left, whereas 77 is greater, so it goes on the right.
Likewise, the children of 18 and 77 follow the same pattern.
BST has applications in fast lookup. Here’s how you would search for the value 105 in a
list:
1 4 18 20 25 40 45 70 77 88 105
40
77
105
The solution is to use the extends keyword. By only allowing data types that extend
Comparable , you can guarantee the values in all the nodes will be comparable.
100
Dart Apprentice: Beyond the Basics Chapter 8: Generics
Here are a few explanatory points:
The extends keyword goes inside the angle brackets to restrict the types that E can be. Only types that extend
Comparable are allowed.
You’ll use the same Node class that you created earlier.
1 This is the base case. Create a new node if the parent node doesn’t have a child at this location.
2 compareTo is a method on types that extend Comparable . This method returns -1 if the first value is less
than the second, +1 if it’s greater, and 0 if they’re the same. If the value is smaller, insert the node at the left
child. Otherwise, insert it at the right child. Calling _insertAt recursively searches the tree until it finds an empty
child.
3 When the recursion finishes, this function will return the root node.
1 9
0 5 8
101
Dart Apprentice: Beyond the Basics Chapter 8: Generics
You specified the type as num rather than int because int doesn’t directly implement
Comparable , whereas num does.
To make printing easier, find your Node class and add the following override:
@override
String toString() {
final left = leftChild?.toString() ?? '';
final parent = value.toString();
final right = rightChild?.toString() ?? '';
return '$left $parent $right';
}
This recursively calls the toString methods of the left and right children. Because the
parent value is calculated after the left child and before the right child, this is known as in-
order traversal.
For a BST, this has the effect of printing the values in order from least to greatest:
1 7 9
9
1
0 5 8
0 5 8
102
Dart Apprentice: Beyond the Basics Chapter 8: Generics
@override
String toString() => root.toString();
root is the root node of the tree, so calling root.toString provides a string representation of
all the values in the tree, starting at the left-most node and ending with the right-most
one.
print(tree);
0 1 5 7 8 9
Your BST implementation works with integers. Because of generics, you wouldn’t have any
problem with doubles, either. Even strings would work because String is also comparable.
The insert method when used with strings, though, would determine “greater than” and
“less than” according to alphabetical order.
This completes the chapter. Having a handle on generics gives you a lot of flexibility in
your coding.
Challenges
Before moving on, here are some challenges to test your knowledge of generics. It’s best if
you try to solve them yourself, but solutions are available with the supplementary
materials for this book if you get stuck.
103
Dart Apprentice: Beyond the Basics Chapter 8: Generics
push : adds an integer to the top of the stack.
pop : removes and returns the top integer from the stack.
peek : tells the value of the top integer in the stack without removing it.
toString : returns a string representation of the stack.
Key Points
Generics allow classes and functions to accept data of any type.
The angle brackets surrounding a type tell the class or function the data type it will use.
You can restrict the range of allowable types by using the extends keyword within the angle brackets.
104
Dart Apprentice: Beyond the Basics
9 Enhanced Enums
Written by Jonathan Sande
Back in Dart Apprentice: Fundamentals, you learned about basic enums. They’re a way to
give names to a fixed number of options. An example would be the days of the week.
Under the hood, each day has an index: monday is 0 , tuesday is 1 and so on through
sunday is 6 . That’s why they’re called enums. They’re enumerated values with names.
Using enums in this way has always been useful, but with Dart 2.17, they got even better.
You can treat the new enhanced enums like classes. And that’s what they are. Dart enums
are subclasses of the Enum class. That means you can do many of the same things to
enums that you would do with a class, including adding properties and methods,
implementing interfaces and using mixins and generics.
All these topics are more or less constant. People aren’t going to be adding an eighth day
of the week any time soon. Similarly, there are only a finite number of audio playback
states. You could argue there are an indeterminate number of weather types, but if you’ve
thought through your weather app carefully, you probably have a limited set that you need
to show icons for.
On the other hand, when a category has frequent changes or an unlimited number of
possibilities, this isn’t a great choice for enums. Here are some examples of things you
probably shouldn’t represent with an enum:
105
Dart Apprentice: Beyond the Basics Chapter 9: Enhanced Enums
Users
Songs
URLs
If you add another song to your app, you’ll probably have to refactor other parts of your
code. For example, if you’re handling the enum cases with switch statements, you have to
update all the switch statements. So rather than enums, you’d be better off representing
the data types listed above with classes you can store in a list. Then, whenever you add a
new user or a new song, just add a new item to the list.
The function takes any integer, so if you had defined int VOLUME = 2 somewhere else, there would be nothing to
stop you from passing in VOLUME to the function, even though this function has nothing to do with volume.
The compiler doesn’t know there are only three possible options, so it can’t warn you if you provide a value besides
0 , 1 or 2 . This requires you to handle error cases with default .
Sometimes people used similar logic but with strings instead of integers. For example, if (lightColor == 'green') .
With that method, it was easy to accidentally misspell values, such as writing geen instead of green .
Each enum has its own namespace, so there’s no way to accidentally pass in Audio.volume when a function only
accepts TrafficLight enum values.
106
Dart Apprentice: Beyond the Basics Chapter 9: Enhanced Enums
The Dart compiler is smart enough to know how many values an enum has. That means you don’t need to use a
default in a switch statement as long as you’re already handling all the cases. Dart will also warn you if you
aren’t handling an enum case.
All in all, these features of enums make them a much better option than using integer or
string constants as option markers.
Open a Dart project and add the following enum outside of main :
enum TrafficLight {
green,
yellow,
red,
}
You use the enum keyword followed by the enum name in upper camel case. Curly braces
enclose the comma-separated enum values. Adding a comma after the last item is
optional but ensures that Dart will format the list vertically.
Dart recognizes that you’re handling all the enum values, so no default case is necessary.
Run the code above, and you’ll see Go! printed to the console.
That was a basic enum. Enhanced enums will allow you to simplify that code a lot. Keep
reading to find out how.
107
Dart Apprentice: Beyond the Basics Chapter 9: Enhanced Enums
Replace the TrafficLight enum you wrote earlier with the enhanced version:
enum TrafficLight {
green('Go!'),
yellow('Slow down!'),
red('Stop!');
const TrafficLight(this.message);
final String message;
}
The enum has a const constructor, which it uses to set the final message field in the class. Enum constructors
are always const .
green , yellow and red are the only instances of the TrafficLight enum class. They each call the
constructor and set the value of message for their instance.
The last enum case, which is red in this example, ends with a semicolon. Commas still separate the other cases.
It’s also permissible to keep the trailing comma, but you would still need to add a
semicolon:
// alternate formatting
green('Go!'),
yellow('Slow down!'),
red('Stop!'), // trailing comma
; // semicolon
The only advantage here is that you’re explicitly telling the Dart formatter to display the
enum list vertically rather than horizontally. Dart seems to do that anyway in this case,
even without the trailing comma, so there’s no need to add it. Carry on without making this
change.
Now, your previous switch statement is no longer necessary. Replace the code in main
with the following:
108
Dart Apprentice: Beyond the Basics Chapter 9: Enhanced Enums
Your enum has a message parameter, which allows you to access the message directly. No
need for switch statements. That’s much better, isn’t it?
Run your code, and you’ll see the same message as before:
Go!
Operator Overloading
This is a good opportunity to teach you an aspect of classes you might not know about yet.
The topic is operator overloading.
Arithmetic operators: + - , * , / , ~/ , %
Assignment operators: = , += , -= , *= , \= , …
These operators all have meanings in certain contexts. For example, when you use the +
operator with integers, Dart adds them together:
print(3 + 2); // 5
print('a' + 'b'); // ab
However, what would it mean if you tried to add users, as in user1 + user2 ? In this context,
Dart wouldn’t know what to do because the + operator isn’t defined for adding User
classes.
You do have the opportunity to give your own meaning to operators when the context
109
Dart Apprentice: Beyond the Basics Chapter 9: Enhanced Enums
makes sense, though. This is called operator overloading. Many, though not all, of the
operators you saw above support overloading.
The following example will show how to overload an operator in a normal class. After that,
you’ll see a second example where you can apply operator overloading to enums.
class Point {
const Point(this.x, this.y);
final double x;
final double y;
@override
String toString() => '($x, $y)';
}
This class can represent points on a two-dimensional coordinate system where points are
in the form (x, y) , such as (1, 4) or (3, 2) . The image below shows these on a graph
where y is increasing in the downward direction, the usual orientation for rendering
graphics:
0 1 2 3 4 5 6 7 8
2 3, 2
y 4 1, 4
110
Dart Apprentice: Beyond the Basics Chapter 9: Enhanced Enums
In some situations, you might want to add two points together. You’d accomplish that by
first adding the x-coordinates of the two points and then the y-coordinates. The following
image shows the result:
x
0 1 2 3 4 5 6 7 8
2 3, 2
y 4 1, 4
6 4, 6
Dart doesn’t know how to add two points, but you can tell Dart how to do it by overloading
the + operator in your Point class.
Treat the operator as a method name. Because it is the method name. The + method is invoked on the first point:
the point that comes before the + . The other point is the point that comes after the + .
The return line creates a new point with the sum of the x-coordinates and the sum of the y-coordinates.
111
Dart Apprentice: Beyond the Basics Chapter 9: Enhanced Enums
(4.0, 6.0)
Take the following enum for the days of the week as an example:
enum Day {
monday,
tuesday,
wednesday,
thursday,
friday,
saturday,
sunday,
}
It might not make sense to add Monday plus Tuesday, but it sort of makes sense to say
monday + 2 . That would be two days later, right? Wednesday.
Replace the comma after sunday with a semicolon and add the + operator overload.
enum Day {
monday,
tuesday,
wednesday,
thursday,
friday,
saturday,
sunday;
112
Dart Apprentice: Beyond the Basics Chapter 9: Enhanced Enums
// 2
final index = (this.index + days) % numberOfItems;
// 3
return Day.values[index];
}
}
1 values is a list of all the enum values, so length gives you the total number of values, which is 7 because
there are seven days in a week.
2 The index is the enumerated value of each enum value. monday is 0 , tuesday is 1 , wednesday is
2 and so on. Because this.index is an integer, you can add days to it. The % modulo operator divides the
result by 7 and gives the remainder. This makes the new index never go out of bounds, no matter how large
days is. It will start over at the beginning of the list. sunday + 1 is monday because (6 + 1) % 7 is 0 .
3 Convert your newly calculated index back to an enum, and you’re good to go.
day = day + 2;
print(day.name); // wednesday
day += 4;
print(day.name); // sunday
day++;
print(day.name); // monday
Not only does the + operator work, you get += and ++ for free!
Adding Methods
You can also add methods to an enum just as you would to a normal class. Technically,
operator overloading is already adding a method, but this section will provide an
additional example.
Because you already implemented support for the + operator, this method returns the
next day by adding 1 to whatever value this day is.
113
Dart Apprentice: Beyond the Basics Chapter 9: Enhanced Enums
Implementing Interfaces
Say you have the following interface that you use to serialize objects for storage in a
database:
Make an enum named Weather that implements the interface like so:
@override
String serialize() => name;
}
serialize directly returns the enum name, such as 'sunny' or 'cloudy' . The built-in name
property is already a string.
Optionally, you can also add a deserialize method to your enum to go the other direction:
114
Dart Apprentice: Beyond the Basics Chapter 9: Enhanced Enums
In contrast to the higher-order method where that you learned about in Chapter 2,
“Anonymous Functions”, firstWhere returns only a single value. By comparing the input
value to the enum name , you convert the string back to an enum. If the string value
doesn’t exist, orElse will give you a default of Weather.sunny .
Run this code to see the serialized string and the deserialized Weather object:
cloudy
Weather.cloudy
This example was merely to show you the syntax of implementing an interface. You could
use these same methods without the interface, and it would all still work the same. The
interface is only useful if some other part of your app requires Serializable objects.
Note: Once you’ve serialized an enum, you can never change it again. Well, you
can, but you do so at your peril. Say you’ve saved a bunch of enum values as
strings in the database or sent them across the network. At that point, your
enums have been released to the wild. You can’t get them back because they’re
stored on user devices and far-away servers. If you change your enum in the
next app update and then try to deserialize the old enum strings, you’ll get
mismatches and unexpected behavior. That’s another reason you don’t want to
make enums out of things that change frequently.
Adding Mixins
If you have a bunch of different enums where you’re repeating the same logic, you can
simplify your code by adding a mixin.
115
Dart Apprentice: Beyond the Basics Chapter 9: Enhanced Enums
Here’s an example:
Now, Fruit and Vegetable share the describe method. Using the on keyword in the mixin
gave you access to the name property of the Enum class.
fruit.describe();
vegi.describe();
OK, you English grammarians, so it shouldn’t be “a broccoli” but just “broccoli”. The author
hadn’t had dinner yet, and that’s the best example he could think of.
Using Generics
Normally, all the values in an enum will be of the same type. For example, in the Size
enum below, the value of each enum item is an int :
116
Dart Apprentice: Beyond the Basics Chapter 9: Enhanced Enums
enum Size {
small(1),
medium(5),
large(10);
const Size(this.value);
final int value;
}
However, you might want to store different types for each enum value in certain situations.
Take the following example:
enum Default {
font,
size,
weight,
}
Say you have some default values you want to associate with each enum value. The default
font is “roboto”, a String ; the default size is 17.0, a double ; and the default weight is 400,
an int . Each enum value is a different instance of Enum . And when different instances
use different types for their values, you need generics to handle them.
Replace the Default enum above with one that uses generics:
const Default(this.value);
final T value;
}
Object is the nearest common parent type of String , double and int , so the generic T
type extends Object . This allows value to take any of those types. Remember that each
enum value ( font , size and weight ) is an instance of the enum class. The type in angle
brackets tells the constructor the type for that instance.
Although there’s only one value property in your enum, it resolves to a different type
depending on the selected enum instance.
That wraps it up for this chapter. In the next chapter, you’ll learn how to handle errors.
117
Dart Apprentice: Beyond the Basics Chapter 9: Enhanced Enums
Challenges
Before moving on, here are some challenges to test your knowledge of enhanced enums.
It’s best if you try to solve them yourself, but solutions are available with the
supplementary materials for this book if you get stuck.
1 Override the - operator so you can subtract integers from enum values.
2 When you print the name of your Day enum, it prints the days of the week in all lowercase. It’s standard to
use lower camel case for enum values, but it would be nice to use uppercase for the display name. For example,
Monday instead of monday . Add a displayName property to Day for that.
Key Points
Dart enums are subclasses of Enum .
Enhanced enums support constructors, properties, methods, operator overloading, interfaces, mixins and generics.
Operator overloading allows classes to give their own definitions to some operators.
118
Dart Apprentice: Beyond the Basics
10 Error Handling
Written by Jonathan Sande
It’s natural to only code the happy path as you begin to work on a project.
Until it isn’t.
When a baker makes a mistake, cookies get burned. When an author makes a mistake,
words get mispelled. Eaters will swallow and readers overlook, but code doesn’t forgive.
Maybe you noticed misspelled was “mispelled” in the previous sentence. Or maybe you
missed it. We all make mistakes, but when a programmer makes one, the whole app
crashes. That’s the nature of a programmer’s work. The purpose of this chapter is to teach
you how to crash a little less.
Dividing by Zero
One way to crash is to divide by zero. You learned in elementary school that you can’t do
that.
1 ~/ 0;
119
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
Now, run your code without debugging. There are a few ways to do that:
In the top-right of the window, click the dropdown menu next to the Run button and make sure it says Run Without
Debugging:
Note: You’ll learn about debugging later in this chapter, but until directed to do
differently, run all the examples here without debugging. That way, VS Code
won’t pause when it reaches an error.
After running the program, check the debug console to see an error message that begins
with the following two lines:
Unhandled exception:
IntegerDivisionByZeroException
Note: Sometimes you’ll see the words “error” and “exception” used
interchangeably, but strictly speaking, an exception is typical and good when
properly handled, whereas an error is bad.
120
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
No Such Method
In the past, you often got a NoSuchMethodError when you forgot to handle null values.
Because the Null class doesn’t have many methods, almost anything you tried to do with
null caused this crash.
After Dart added sound null safety, NoSuchMethodErrors became far less common. You can
still see what it looks like by turning off type checking using dynamic .
dynamic x = null;
print(x.isEven);
Unlike integers, null doesn’t have an isEven getter method. So when you run that code,
you get the following message:
Unhandled exception:
NoSuchMethodError: The getter 'isEven' was called on null.
The error you got here was a runtime error. Dart didn’t discover it until you ran the code.
int x = null;
print(x.isEven);
Now, you have a compile-time error. VS Code puts little red squiggles under null with the
message that you’re not allowed to do that:
121
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
Compile-time errors are much better than runtime errors because they’re obvious and
immediate.
Format Exception
Another way to crash your app is to try and decode a “JSON” string that isn’t actually in
JSON format.
Replace the contents of your project file with the following code:
import 'dart:convert';
void main() {
const malformedJson = 'abc';
jsonDecode(malformedJson);
}
The string 'abc' isn’t in JSON format, so when you try to decode it, you get the following
error message:
Unhandled exception:
FormatException: Unexpected character (at character 1)
abc
^
This is a format exception, which Dart identifies with the FormatException class.
Another way to cause a format exception is to try to turn a non-numeric string into an
integer:
int.parse('42'); // OK
int.parse('hello'); // FormatException
The first line is fine. It converts the string '42' into the integer 42 . However, Dart has no
idea how to convert the string 'hello' into an integer, so it stops executing the program
with the following error:
Unhandled exception:
FormatException: Invalid radix-10 number (at character 1)
hello
^
122
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
There are many other ways to crash your app. But hopefully, the examples above gave you
a taste of how to do it. As if anyone needed help with this kind of thing.
Stack trace
A stack trace is a printout of all the methods on the call stack when an error occurs. In the
stack trace above, most methods are internal. #4 main is the only one that’s part of your
code.
Do you remember the stack data structure you made for the challenges in Chapter 8,
“Generics”? Well, it turns out stacks are pretty important in computer science. Computers
use them to keep track of the current method being executed.
To see this more clearly, write some functions that call other functions:
void main() {
functionOne();
}
void functionOne() {
functionTwo();
}
123
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
void functionTwo() {
functionThree();
}
void functionThree() {
int.parse('hello');
}
When Dart executes this program, it’ll start by calling main . Because main is the current
function, Dart adds main to the call stack. You can think of a stack like a stack of pancakes.
main is the first pancake on the stack. Then, main calls functionOne , so Dart puts
functionOne on the call stack. functionOne is the second pancake on the stack. functionOne
calls functionTwo , and functionTwo calls functionThree . Each time you enter a new function,
Dart adds it to the call stack.
functionThree
functionTwo
functionOne
main
Call stack
Normally, when functionThree finishes, Dart would pop it off the top of the stack, go back to
functionTwo , finish functionTwo , pop it off the stack and so on until main finishes. However,
in this case, there’s about to be a tragedy in functionThree , which will bring everything to a
grinding halt.
Run the code you just wrote. The app crashes when you reach the line int.parse('hello'); . Look
at the debug console, and you’ll see the stack trace that shows the call stack at the time of
the crash.
124
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
Stack trace
There, your four methods sit in the middle of the stack. The other methods above and
below them are internal to Dart. On the right side, you can see bin/starter.dart followed by
a line number. Yours might look different if your project name is different. Click the one
after functionThree , and VS Code brings you to the line number where the crash occurred in
functionThree , at int.parse('hello'); .
Stack traces look messy and intimidating, but they’re your friends. They hold some of the
first clues to what went wrong.
Debugging
It’s not always obvious from the stack trace where the bug in your code is. VS Code has
good debugging tools to help you out in this situation.
void main() {
final characters = ' abcdefghijklmnopqrstuvwxyz';
final data = [4, 1, 18, 20, 0, 9, 19, 0, 6, 21, 14, 27];
final buffer = StringBuffer();
for (final index in data) {
final letter = characters[index];
buffer.write(letter);
}
print(buffer);
}
First, run the code without debugging. You’ll get a crash with the following error message:
125
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
The stack trace tells you the error happened in the main method on line 6, which is the
following line:
To find the error, you’ll use the debugging tools available in VS Code and step through your
code line by line.
Adding a Breakpoint
Click the margin to the left of line 2. This will add a red dot:
Breakpoint on line 2
That red dot is called a breakpoint. When you run your app in debug mode, execution will
pause when it reaches that line.
126
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
In the top-right of the window, click the dropdown menu next to the Run button and make sure it says Start
Debugging:
Continue: This is the button with the line and triangle. It resumes normal execution until the next breakpoint, if
any, is reached.
Step Over: This is the clockwise arrow over the dot. Pressing it executes one line of code but doesn’t descend into
the body of any function it reaches. That’s fine because you don’t have other functions besides main in this
example.
127
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
Note: Later, when you’re debugging an app with functions, use the Step Into
button, the one with the arrow pointing down at the dot, to enter the body of
another function. For example, if you wanted to follow the logic of the recursive
functions in Chapter 8, “Generics”, you would use this button.
Press the Step Over button several times until execution reaches line 7: buffer.write(letter); .
Look at the Run and Debug panel on the left. The Variables section shows the current
values of the variables in your code.
Keep pressing Step Over for a few more iterations of the for loop while keeping an eye on
the values of the variables. You’ll begin to see the pattern of how the code works. Stepping
through code one line at a time like this will often help you discover even the hardest-to-
find bugs.
Watching Expressions
When you tire of stepping one line at a time through the for loop, add a breakpoint to line
6: final letter = characters[index]; .
Then, find the Watch section on the Run and Debug panel. Add the following two
expressions by pressing the + button:
characters[index]
buffer.toString()
128
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
After that, press the Continue button a few times, keeping an eye on the expressions
you’re watching on the left.
It’s 27 . What’s the length of the characters string? If you don’t want to count, add
characters.length to the Watch window. It’s also 27 .
Ah, that’s it! You recall that lists and string indexes are 0-based, so index 27 is one
greater than the last position in the list. That was causing the range error.
You remember that you wanted to end the message with an exclamation mark but forgot to
add it to characters .
129
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
No errors this time! You see the following output in the debug console:
dart is fun!
If you want to remove the breakpoints, click the red dots on the left of lines 2 and 6.
Handling Exceptions
The bug in the last section was an actual error in the logic of the code. There was no way to
handle that except to track down the bug and fix it.
Other types of crashes, though, are caused by valid exceptions that you aren’t properly
handling. You need to think about these and how to deal with them.
Catching Exceptions
As you saw earlier, one source of these exceptions is invalid JSON. When connected to the
internet, you can’t control what comes to you from the outside world. Invalid JSON doesn’t
happen very often, but you should write code to deal with it when you get it.
import 'dart:convert';
void main() {
const json = 'abc';
try {
dynamic result = jsonDecode(json);
print(result);
} catch (e) {
print('There was an error.');
print(e);
}
}
In Chapter 12, “Futures”, you’ll learn how to retrieve JSON strings from the internet. For now, though, you’re just
using a hard-coded string for the JSON.
130
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
The new code is a try-catch block. You put the code that might throw an exception in the try block. Yes, it’s
called “throw”, but the meaning is “cause”. If it does throw, the catch block will handle the exception without
crashing the app. In this case, all you’re doing is printing a message and the error.
Here, e is the error or exception. You can use catch (e, s) instead if you need the stack trace, s being the
StackTrace object.
Unlike when you had a FormatException earlier in the lesson, this time, the app didn’t crash.
You handled this exception.
Note: In a real app, you’d want to do more than print the error message. In fact,
you should remove print statements from production apps because they can
sometimes leak sensitive data.
How to handle this particular exception would depend on the context. Say a
user requested to see some song lyrics, so your app asked the server for them
but got back invalid JSON. What should you do in this case? Probably, you’d want
to notify the user that the song lyrics they requested aren’t currently available.
Then, rerun the code. This time, the try block finishes successfully, and you see the Dart
map that jsonFormat produced:
{name: bob}
131
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
Using a catch block will catch every exception that happens in the try block.
“Perfect!” you say. “I’ll just wrap my entire app in one big try-catch block. No more
crashes!”
Even though that might sound like a good idea, try-catch isn’t magic. It doesn’t make the
problems go away. In fact, sometimes, it can make things worse because you’re hiding your
problems rather than dealing with them. Sometimes coding can be like real life, can’t it?
Just as you can’t solve your life troubles all at once, you can’t handle every programming
exception with one catch block. It’s better to focus on one problem at a time, both in life
and in coding.
To catch a specific exception, use the on keyword. Replace the body of main with the
following code:
try {
dynamic result = jsonDecode(json);
print(result);
} on FormatException {
print('The JSON string was invalid.');
}
Now, it’s very clear that you’re only handling format exceptions.
You’re handling format exceptions, but if there are any other exceptions, your app will still
crash.
“What?” you say. “I don’t want my app to crash! Please just let me use a catch block.”
If you don’t know what you’re catching, how can you handle it? After all, the solution to no
internet is quite different than the solution to a range error. If the app crashes, that’s a
good thing. It’s a loud and clear signal that there’s an exceptional situation happening that
you need to know about.
132
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
void main() {
const numberStrings = ["42", "hello"];
try {
for (final numberString in numberStrings) {
final number = int.parse(numberString);
print(number ~/ 0);
}
} on FormatException {
handleFormatException();
} on UnsupportedError {
handleDivisionByZero();
}
}
void handleFormatException() {
print("You tried to parse a non-numeric string.");
}
void handleDivisionByZero() {
print("You can't divide by zero.");
}
This time, you’re handling two possible errors. The extra functions emphasize that you can break your handling
code into separate logical units.
IntegerDivisionByZeroException is deprecated and will probably be removed from the language in the future.
That doesn’t mean you’ll be able to divide by zero in the future. It just means you should call it UnsupportedError
when catching such an exception.
The code in the try block terminates as soon as you hit the first error. You never made it
to the format exception. But you were ready for it.
133
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
void main() {
final database = FakeDatabase();
database.open();
try {
final data = database.fetchData();
final number = int.parse(data);
print('The number is $number.');
} on FormatException {
print("Dart didn't recognize that as a number.");
} finally {
database.close();
}
}
class FakeDatabase {
void open() => print('Opening the database.');
void close() => print('Closing the database.');
String fetchData() => 'forty-two';
}
FakeDatabase represents a situation where you must clean up some resources even if the
operation in the try block is unsuccessful. Note that you “close” the database in the
finally block.
The try block failed because parsing forty-two threw a format exception. Even so, the
database still had an opportunity to close.
Now, replace 'forty-two' in the code above with '42' . Then, rerun the program.
The try block was successful, and the finally block also ran its code.
134
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
Back in Chapter 1, “String Manipulation”, you learned how to validate passwords with
regular expressions. You’ll build on that foundation now by defining some custom
exceptions for invalid passwords.
As you can see, it’s pretty easy to make a custom exception. All you need to do is
implement Exception and create a field that will hold a message describing the problem.
Those are exceptions you can throw if the potential password doesn’t include a number,
uppercase letter or lowercase letter.
Throwing Exceptions
Now, add the following validation method below main :
135
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
Use the throw keyword when you want to throw an exception. You can throw anything. For
example, you could even throw a string and it would halt program execution if you didn’t
handle it:
But it’s better that you throw classes that implement Exception .
136
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
You could have put all the earlier validation logic right here in this function. However, the
Single Responsibility Principle says that a function should do only one thing. Extracting
code into short and simple functions makes the logic easier to reason about. Writing clean
code is a step in the right direction toward preventing errors. And preventing errors is
better than handling them!
try {
validatePassword(password);
print('Password is valid');
} on ShortPasswordException catch (e) {
print(e.message);
} on NoLowercaseException catch (e) {
print(e.message);
} on NoUppercaseException catch (e) {
print(e.message);
} on NoNumberException catch (e) {
print(e.message);
}
In addition to demonstrating how to use your custom exceptions, this example shows that
you can combine the on and catch keywords. The e is an instance of your custom
exception class, giving you access to the message property you defined.
Play around with the password to confirm that the other exceptions work as well.
137
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
Challenges
Before moving on, here are some challenges to test your knowledge of error handling. It’s
best if you try to solve them yourself, but solutions are available with the supplementary
materials for this book if you get stuck.
Key Points
An error is something that crashes your app.
A stack trace is a crash report that tells you the method and line that crashed your app.
VS Code debugging tools allow you to set breakpoints and execute your code one line at a time.
try/catch blocks are one way to handle exceptions.
It’s better to handle specific exceptions with the on keyword rather than blindly handling all exceptions with
catch .
If you have a logic error in your app, don’t “handle” it with catch . Let your app crash and then fix the bug.
138
Dart Apprentice: Beyond the Basics Chapter 10: Error Handling
Another important topic you should learn about is unit testing. Unit testing is where you
write code to test your app’s individual units of logic. These units are usually classes or
functions. Systematic testing ensures that all the logic in your app behaves as expected.
Going through this process will not only help you discover hidden bugs, it’ll also keep you
from breaking things in the future that used to work in the past. That’s called a regression
bug.
139
Dart Apprentice: Beyond the Basics
11 Concurrency
Written by Jonathan Sande
Your computer does a lot of work and does it so fast that you don’t usually realize how
much it’s doing. Now and then, though — especially on an older computer or phone — you
might notice an app slow down or even freeze. This might express itself during an
animation as jank: that annoying stutter that happens when the device does so much work
that some animation frames get dropped.
Long-running tasks generally fall into two categories: I/O tasks and computationally
intensive tasks. I/O, or input-output, includes 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. On the other hand, computationally intensive
tasks happen inside the CPU. These tasks might include decrypting data, performing a
mathematical calculation or parsing JSON.
As a developer, you must consider how your app, and particularly 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 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, allowing
you to handle delays gracefully without blocking your app’s responsiveness.
Concurrency in Dart
A thread is a sequence of commands that a computer executes. Some programming
languages support multithreading — running multiple threads simultaneously — but
others don’t. Dart is a single-threaded language.
No, Dart was created in 2011, well into the age of multicore CPUs.
Ah, but no. The developers deliberately made Dart single-threaded, providing significant
advantages, as you’ll soon see.
140
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
parallelism and concurrency. In common English, these words mean about the same thing,
but a distinction exists in computer science.
Parallelism is when multiple tasks run at the same time on multiple processors or CPU
cores; concurrency is when multiple tasks take turns running on a single CPU core. When
a restaurant has a single person alternately taking orders and clearing tables, that’s
concurrency. But a restaurant that has one person taking orders and a different person
clearing tables, that’s parallelism.
It can be — when there’s a lot of work to do and that work is easily split into independent
tasks. However, parallelism has some disadvantages, too.
The same thing can happen in parallel threads with access to the same memory. One
thread saves a value in memory and expects the value to be the same when the thread
checks the value later. However, if a second thread modifies the value, the first thread gets
confused. It can be a major headache to track down those kinds of bugs because they
come from a source completely separate from the code that reports the error. A language
that supports multithreading needs to set up a system of locks so values won’t change at
the wrong time. The cognitive load of designing, implementing and debugging a system
with multiple threads can be heavy.
So the problem isn’t with parallelism but rather with multiple threads having access to the
same state in memory.
Dart Isolates
Dart’s single thread runs in what it calls an isolate. Each isolate has its own allocated
memory, ensuring that no isolate can access any other isolate’s state. That means there’s
no need for a complicated locking system. It also means sensitive data is much more
secure. Such a system greatly reduces the cognitive load on a programmer.
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’s not usually the case.
141
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
In the following image, you can see multiple tasks running on two threads in parallel. A
rectangle represents each task, and longer rectangles represent longer-running tasks. A
flat line represents an idle state where the thread isn’t doing anything:
The next image shows the same tasks running concurrently on a single thread:
The concurrent version does take a little longer, but it isn’t much longer. The reason is that
the parallel threads were idle for much of the time. A single thread is usually more than
enough to accomplish what needs to be done.
Flutter has to update the UI 60 times a second. Each update timeslice is called a frame.
That leaves about 16 milliseconds to redraw the UI on each frame. It typically doesn’t take
that long, giving you time to perform other work while the thread is idle. The user won’t
notice any problems as long as that work doesn’t block Flutter from updating the UI on the
next frame. The trick is to schedule tasks during the thread’s downtimes.
This contrasts with asynchronous code, which means not together in time. Asynchronous
code reschedules certain tasks to run in the future when the thread isn’t busy.
All the code you’ve written so far in the book has been synchronous. For example:
print('first');
print('second');
print('third');
142
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
first
second
third
Because the code executes synchronously, it’ll never print in a different order like third first
second .
You have to open the bottle before you can take a drink.
You have to turn on the car before you can drive it.
It doesn’t matter if you brush your teeth first or wash your face first.
It doesn’t matter if you put a sock on the right foot first or the left foot first.
As in life, so it is with Dart. Although some code must execute in order, other tasks can be
temporarily postponed. The postponable tasks are where the Dart event loop comes in.
The event loop uses a data structure called a queue. Think of a queue like waiting in line at
the grocery store. When you first join the line, you stand at the back of the line. Then, you
slowly move to the front of the line as people before you leave. The first one in line is the
first to leave. For that reason, developers call a queue a first-in-first-out, or FIFO, data
structure. Dart uses queues to schedule tasks to execute on the main isolate.
The event loop has two queues: an event queue and a microtask queue. The event queue
is for events like a user touching the screen or data coming in from a remote server. Dart
primarily uses the microtask queue internally to prioritize certain small tasks that can’t
wait for the tasks in the event queue to finish.
Microtask queue
4
Synchronous code
143
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
Synchronous tasks in the main isolate thread are always run immediately. You can’t interrupt them.
If Dart finds any long-running tasks that agree to be postponed, Dart puts them in the event queue.
When Dart finishes running the synchronous tasks, the event loop checks the microtask queue. If the microtask
queue has any tasks, the event loop puts them on the main thread to execute next. The event loop keeps checking the
microtask queue until it’s empty.
If the synchronous tasks and microtask queue are both empty, the event loop sends the next waiting task in the
event queue to run on the main thread. Once it gets there, the code executes synchronously. Like any other
synchronous code, nothing can interrupt it after it starts.
If new microtasks enter the microtask queue, the event loop handles them before the next event in the event queue.
Typically, if all the tasks are finished, this would indicate that it’s time to exit the main
isolate and terminate the application. However, the isolate will stay around if it’s waiting
for a response from the outside world. Maybe that’s a timer that the isolate previously
started, or perhaps it’s listening for a response from a user or remote server.
Another way to perform work on other threads is to create a new Dart isolate. The new
isolate has its own memory and thread working in parallel with the main isolate. The two
isolates are only able to communicate through messages, though. They have no access to
each other’s memory state. The idea is like messaging a friend. Sending your friend a text
message doesn’t give you access to the internal memory of their mobile device. They
simply check their messages and reply to you when they feel like it.
You won’t often need to create a new isolate. However, if you have a task that’s taking too
long on your main isolate thread, which you’ll notice as unresponsiveness or jank in the
UI, then this work is likely a good candidate for handing it off to another isolate. Chapter
14, “Isolates”, will teach you how to do that.
144
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
print('first');
Future(
() => print('second'),
);
print('third');
The constructor of Future takes an anonymous function. Future then adds that function to
the event queue.
first
third
second
1 Dart always runs the synchronous code first. print('first') is synchronous, so Dart executes it immediately on the
main isolate.
2 Then Dart comes to Future . Dart takes the function inside Future and adds it to the event queue. The event
queue code has to wait for all the synchronous code to finish before it can go.
4 Finally, there’s no more synchronous code, so Dart takes print('second') off the event queue and executes it.
Like, literally.
Count how many times you’ve seen a blue moon in your life. How many did you get? Zero?
Yeah, that’s about how often you’ll need to explicitly put something on the microtask
queue in your Dart career. Unless you’re writing some low-level library, you can forget
about this queue and trust Dart to handle the events there.
145
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
However, should the need arise, scheduling tasks on the microtask queue is possible.
print('first');
Future(
() => print('second'),
);
Future.microtask(
() => print('third'),
);
print('fourth');
Adding an anonymous function to the Future.microtask constructor puts this code on the
microtask queue.
first
fourth
third
second
1
Dart always runs synchronous code first, so that’s why first and fourth are first.
2
Dart added print('second') to the event queue and print('third') to the microtask queue.
3
Once the synchronous code finishes, Dart prioritizes any code in the microtask
queue. That’s why third is next.
4
Finally, when the microtask queue is empty, Dart gives the code in the event queue
a chance. That’s why second is last.
146
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
print('first');
Future(
() => print('second'),
).then(
(value) => print('third'),
);
Future(
() => print('fourth'),
);
print('fifth');
The then method of a Future instance will execute an anonymous function immediately after the future
completes. This code is synchronous.
When futures complete successfully, they return a value. You’ll learn more about that in the next chapter. For now,
ignore value in the then callback.
You’ll also learn about async/await syntax in Chapter 12, “Futures”. It’s a little easier to use than then .
first
fifth
second
third
fourth
By now, you should know why first and fifth are first. They’re both synchronous, and
synchronous code always goes first. second and fourth both get added to the event queue,
but because then runs its code synchronously after second finishes, third jumps in
before fourth has a chance to come off the event queue.
147
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
print('first');
Future.delayed(
Duration(seconds: 2),
() => print('second'),
);
print('third');
Future.delayed takes two parameters. The first is the duration of time you want to wait
before starting the task. The second is the function you want to run after completing the
duration. Dart adds the function to the event queue at that point.
Run the code above. First, you only see the following:
first
third
first
third
second
By the time the duration passed, all the synchronous code was long finished, so
print('second') didn’t have to wait very long on the event queue. Dart executed it right away.
Is this all starting to make sense? If not, the challenge below and its accompanying
explanation should help you.
Challenge
Before moving on, here’s a challenge to test your understanding of how Dart handles
asynchronous tasks. An explanation follows the challenge, but try to figure out the solution
yourself before looking.
148
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
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');
}
Write your answer down before reading the solution that follows.
Solution to Challenge 1
For brevity, the explanations below will refer to each task by its number. For example,
print('1 synchronous') is abbreviated as 1 .
Step 0
void main() {
// ...
}
Dart creates the main isolate and calls your main function:
main()
Step 1
print('1 synchronous');
149
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
Step 2
Step 3
2 4
Step 4
2 5 4
150
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
Step 5
Future.delayed(
Duration(seconds: 1),
() => print('6 event queue'),
);
Dart starts an internal timer for one second. The queues remain unchanged:
2 5 4
Step 6
7 2 5 4
Step 7
9 7 2 5 4
151
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
Step 8
print('11 synchronous');
9 7 2 5 4 11
Step 9
All the synchronous tasks have finished, so Dart executes the first task in the microtask
queue:
9 7 2 5 4
Step 10
9 7 2 5
Step 11
152
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
The microtask queue is empty now, so Dart takes the first task off of the event queue and
executes it in the main isolate:
9 7 2
Step 12
9 7 3
Step 13
9 7
153
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
Step 14
8 9
Step 15
8 9
Step 16
8 10
154
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
Step 17
The microtask queue has priority over the event queue, so Dart executes 10 before 8 :
8 10
Step 18
The microtask queue is empty now, so Dart takes 8 off of the event queue and executes
it:
Step 19
However, Dart is still waiting on the Future.delayed timer it set back in Step 5, so the isolate
doesn’t exit yet.
Step 20
Future.delayed(
Duration(seconds: 1),
() => print('6 event queue'),
);
155
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
Sometime later, the duration finally completes, so Dart adds 6 to the event queue:
Step 21
There’s nothing to wait for, so Dart takes 6 off the event queue and executes it:
Step 22
Dart isn’t waiting for anything either, so the isolate exits and the application terminates.
Result
1 synchronous
11 synchronous
4 microtask queue
5 microtask queue
2 event queue
3 synchronous
7 event queue
9 event queue
10 microtask queue
8 event queue
6 event queue
If you wrote down the correct answer, give yourself a well-deserved pat on the back!
156
Dart Apprentice: Beyond the Basics Chapter 11: Concurrency
Key Points
Dart is single-threaded and handles asynchronous programming through concurrency rather than parallelism.
Concurrency refers to rescheduling tasks to run later on the same thread, whereas parallelism refers to running
tasks simultaneously on different threads.
Synchronous code always runs first and cannot be interrupted. After this comes anything in the microtask queue,
and when these finish, any tasks in the event queue.
157
Dart Apprentice: Beyond the Basics
12 Futures
Written by Jonathan Sande
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:
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.
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,
158
Dart Apprentice: Beyond the Basics Chapter 12: Futures
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.”
Uncompleted.
Value
Uncompleted
Error
Future states
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,
);
159
Dart Apprentice: Beyond the Basics Chapter 12: Futures
Here’s what’s happening:
2 The first argument is a Duration . After a delay of 1 second, Dart will add the anonymous function in the
second argument to the event queue.
3 When the event loop gets to () => 42 , it will run that function in the main isolate, causing the function to return
the integer 42 .
In the previous future, the value you want is the 42 , but how do you get it? Your variable
myFuture isn’t 42 ; it’s a future that’s a promise to return an int or an error. You can see
that if you try to print myFuture :
print(myFuture);
Instance of 'Future<int>'
So how do you access the value? And what if the future completes with an error?
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.
Replace the body of the main function with the following code:
160
Dart Apprentice: Beyond the Basics Chapter 12: Futures
A future will either give you a value or an error. If it completes with a value, you can get the
value by adding a callback to the then method. The anonymous function provides the
value as an argument so you can access it. On the other hand, if the future completes with
an error, you can handle it in catchError . But regardless of whether the future completes
with a value or an error, you can run any final code in whenComplete .
If you worked carefully through Chapter 11, “Concurrency”, you weren’t surprised that
Dart printed “After the future” before the future results. That print statement is
synchronous, so it ran immediately. Even if the future didn’t have a one-second delay, it
would still have to go to the event queue and wait for all the synchronous code to finish.
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.
// 1
Future<void> main() async {
print('Before the future');
// 2
final value = await Future<int>.delayed(
Duration(seconds: 1),
() => 42,
);
print('Value: $value');
161
Dart Apprentice: Beyond the Basics Chapter 12: Futures
1 If a function uses the await keyword anywhere in its body, it must return a Future and add the async
keyword before the opening curly brace. Using async clearly tells Dart this is an asynchronous function and
that the results will go to the event queue. Because main doesn’t return a value, you use Future<void> .
2 In front of the future, you added the await keyword. Once Dart sees await , the rest of the function won’t run
until the future completes. If the future completes with a value, there are no callbacks; you have direct access to that
value. Thus, the type of the value variable above isn’t Future , but int .
This time, “After the future” gets printed last. That’s because everything after the await
keyword is sent to the event queue.
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:
try {
final value = await Future<int>.delayed(
Duration(seconds: 1),
() => 42,
);
print('Value: $value');
} catch (error) {
print(error);
} finally {
print('Future is complete');
}
162
Dart Apprentice: Beyond the Basics Chapter 12: Futures
The catch and finally blocks correspond to the catchError and whenComplete callbacks you
saw earlier. If the future completes with an error, Dart will immediately abort the try
block and call the catch block. Error or not, Dart will always call the finally block.
The future finished with a value, so Dart didn’t call the catch block.
As useful as Future.delayed is, though, eventually, you’ll need to implement the real network
request class. The following example will show how to make an HTTP request to access a
REST API. This example will use many concepts you’ve learned in the Dart Apprentice
books.
163
Dart Apprentice: Beyond the Basics Chapter 12: Futures
class Todo {
Todo({
required this.userId,
required this.id,
required this.title,
required this.completed,
});
@override
String toString() {
return 'userId: $userId\n'
'id: $id\n'
'title: $title\n'
'completed: $completed';
}
}
You could have also used a named constructor or a static method instead of a factory constructor for
Todo.fromJson . Review Dart Apprentice: Fundamentals if you need a refresher on classes, constructors and static
methods.
Rather than dynamic , you could have written Object? . But Dart’s JSON decoding library returns dynamic
values, so it’s common to see people use dynamic in the fromJson input parameter.
dependencies:
http: ^0.13.5
164
Dart Apprentice: Beyond the Basics Chapter 12: Futures
Save the file, and if necessary, run dart pub get in the terminal to pull the http package
from Pub.
Then, at the top of the file with your main function, add the following imports:
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
As you recall from Chapter 10, “Error Handling”, the dart:convert library gives you jsonDecode , a function for
converting a raw JSON string to a Dart map.
The dart:io library has HttpException and SocketException , which you’ll use shortly.
The final import is the http library you just added to pubspec.yaml . Note the as http at the end. This isn’t
necessary, but the as keyword lets you prefix any functions from the library with the name http . You don’t need
to call it http — any arbitrary name is fine. Feel free to change the name to pinkElephants if you so desire.
Providing a custom name can be useful for avoiding naming conflicts with other libraries or functions.
There are a few new things here, so have a look at each of them:
165
Dart Apprentice: Beyond the Basics Chapter 12: Futures
The URL address is for a server that provides an API that returns sample JSON for developers. It’s much like the
1 type of API you would make as a backend for a client app. Uri.parse converts the raw URL string to a format that
http.get recognizes.
2 You use http.get to make a GET request to the URL. Change http to pinkElephants if that’s what you
called it earlier. GET requests are the same requests browsers make when you type a URL in the address bar.
3 Because it takes time to contact a server that might exist on another continent, http.get returns a future. Dart
passes the work of contacting the remote server to the underlying platform, so you won’t need to worry about it
blocking your app while you wait. Because you’re using the await keyword, the rest of the main method will be
added to the event queue when the future completes. If the future completes with a value, the value will be an object
of type Response , which includes information from the server.
4 HTTP defines various three-digit status codes. A status code of 200 means OK — the request was successful,
and the server did what you asked. On the other hand, the common status code of 404 means the server couldn’t
find what you were asking for. If that happens, you’ll throw an HttpException .
5 The response body from this URL address includes a string in JSON format. You use jsonDecode from the
dart:convert library to convert the raw JSON string into a Dart map. The type is dynamic because JSON
strings are untyped by nature. You’re assuming that it’s a map, but theoretically, it might not be. You can do some
extra type checking or error checking if you want to be sure.
6 Once you have a Dart map , you can pass it into the fromJson factory constructor of your Todo class that
you wrote earlier.
Make sure you have an internet connection, then run the code above. You’ll see a printout
from your Todo object’s toString method:
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';
// ...
}
Then, below the try block, add the following catch blocks:
166
Dart Apprentice: Beyond the Basics Chapter 12: Futures
HttpException : You’re throwing this exception yourself if the status code isn’t 200
OK .
FormatException : jsonDecode throws this exception if the JSON string from the server
isn’t in proper JSON format. It would be unwise to blindly trust whatever the server
gives you.
Remember, it’s good to be specific in your error-catching. That way, if a different kind of
error comes up that you weren’t expecting, your app will crash. That allows you to fix the
error right away instead of silently ignoring it, as a generic catch block would do.
Turn off your internet and rerun the code. You should see the following output:
In an actual app, instead of just printing a message to the console, you’d probably want to
remind the user to turn on their internet.
HttpException: 404
167
Dart Apprentice: Beyond the Basics Chapter 12: Futures
In a real app, you’d inform the user that whatever they were looking for isn’t available.
You’ve already had practice throwing a FormatException in Chapter 10, “Error Handling”, so
you can skip that test.
Nice work! You now know how to get the value from a future and handle any errors.
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.
Now, fetchTemperature returns a type of Future<double> rather than just double? . There’s no
need for the nullable type anymore. The only reason you allowed null in the first place
was as a default value if there was a problem fetching the temperature. Now that you’re
using a future, you can just throw an exception if you can’t get the temperature.
168
Dart Apprentice: Beyond the Basics Chapter 12: Futures
A value of type 'double' can't be returned from the method 'fetchTemperature' because it has a return type of 'Future<double>'.
Unnamed Constructor
@override
Future<double> fetchTemperature(String city) {
return Future(() => 42.0);
}
This time fetchTemperature returns a future that always completes with a value of 42.0 .
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);
}
169
Dart Apprentice: Beyond the Basics Chapter 12: Futures
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."));
}
When this future completes, it’ll give an argument error. This is still your FakeWebServer
implementation. In a real web server implementation, you would only return the error if
there was a problem with the HTTP request.
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,
);
}
This future will complete after two seconds and then return a value.
If you want to return an error instead, throw the error in the callback:
@override
Future<double> fetchTemperature(String city) {
return Future.delayed(
Duration(seconds: 2),
() => throw ArgumentError('City does not exist.'),
);
}
This causes the future to complete with an argument error after two seconds.
170
Dart Apprentice: Beyond the Basics Chapter 12: Futures
@override
Future<double> fetchTemperature(String city) async {
return 42.0;
}
The function directly returns the value 42.0 . But because it contains the async keyword,
Dart automatically makes the return value a future.
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.
Completer is a part of the dart:async library, so first import that at the top of your project
file:
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;
}
171
Dart Apprentice: Beyond the Basics Chapter 12: Futures
1 Create a new instance of Completer . The future’s return value is a double , so the completer’s generic type is
also double .
2 You control whether the future will complete with a value or an error. If you want the future to complete with a value,
then call complete with the value as a parameter.
3 Alternatively, call completeError if you want to complete the future with an error.
4 Return the future. In this example, the future has already finished by the time you’ve reached this point because
you’re calling complete and completeError synchronously. You could wrap them in Future.delayed if you
wanted to see an example of this method returning an uncompleted future.
That completes the chapter. Knowing how to use futures opens up a whole new world to
you. There are many public web APIs you can access to gather data for your apps. Your
future has arrived!
172
Dart Apprentice: Beyond the Basics Chapter 12: Futures
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.
Free Code Camp has a weather API that takes the following form:
https://fcc-weather-api.glitch.me/api/current?lat=45.5&lon=-122.7
You can change the numbers after lat and lon to specify latitude and longitude for the
weather.
2 Use the http package to make a GET request. This will give you a Response object.
6 Extract the temperature and the city name from the map.
https://jsonplaceholder.typicode.com/comments
Create a Comment data class and convert the raw JSON to a Dart list of type List<Comment> .
173
Dart Apprentice: Beyond the Basics Chapter 12: Futures
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 .
174
Dart Apprentice: Beyond the Basics
13 Streams
Written by Jonathan Sande
A future represents a single value that will arrive in the future. On the other hand, a
stream represents multiple values that will arrive in the future. Think of a stream as a list
of futures.
You can imagine a stream meandering through the woods as the autumn leaves fall onto
the water’s surface. Each time a leaf floats by, it’s like the value that a Dart stream
provides.
Stream of values
Streaming music online rather than downloading the song before playing it is another
good comparison. When you stream music, you get many little chunks of data, but when
you download the whole file, you get a single value, which is the entire file — a little like
what a future returns. The http.get command you used in the last section was
implemented as a stream internally. However, Dart just waited until the stream finished
and then returned all the data at once as a completed future.
Streams, which are of type Stream , are used extensively in Dart and Dart-based
frameworks. Here are some examples:
Reading a large file stored locally where new data from the file comes in chunks.
Although it’s possible to build streams from scratch, you usually don’t need to do that. You
only need to use the streams that Dart or a Dart package provides. The first part of this
chapter will teach you how to do that. The chapter will finish by teaching you how to make
your own steams.
Using a Stream
Reading and writing files are important skills to learn in Dart. This will also be a good
opportunity to practice using a stream.
175
Dart Apprentice: Beyond the Basics Chapter 13: Streams
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 file’s
contents as a future. Then, you’ll do it again by reading the data as a stream of bytes.
Create a new folder named assets in the root of your project. In that folder, create a file
named text.txt. Add some text to the file. Although any text will work, Lorem Ipsum is a good
standby:
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.
Note: Lorem Ipsum is often used as filler text by graphic designers and app
developers when the meaning of the text doesn’t matter. The Latin words were
taken from the writings of the Roman statesman and philosopher Cicero but
modified to become essentially meaningless.
Reading as a String
Now that you’ve created the text file, replace your Dart code with the following:
import 'dart:io';
File takes the relative path to your text file as the argument.
readAsString returns Future<String> , but by using await , you’ll receive the string itself when it’s ready.
File also has a readAsStringSync method, which would run synchronously and avoid
awaiting a future. However, doing so would block your app if the reading takes a while.
Many of the methods on File have synchronous versions, but to prevent blocking your
app, you should generally prefer the asynchronous versions.
176
Dart Apprentice: Beyond the Basics Chapter 13: Streams
Run the code above, and you’ll see the contents of text.txt printed to the console.
When you read a file as a stream, Dart reads the file in chunks. The size of the chunks
depends on how Dart is implemented on the system you’re using, but it’s probably 65,536
bytes per chunk as it was on the local machines used when writing this chapter. The
text.txt file with Lorem Ipsum that you created earlier is only 445 bytes, so trying to stream
that file would be no different than simply reading the whole thing as you did before.
To get a text file large enough to stream in chunks, create a new file in the assets folder
called text_long.txt. Copy the Lorem Ipsum text and paste it in text_long.txt as new lines so
that there are 1000 Lorem Ipsum copies. You can, of course, select all and recopy from time
to time, unless you find it therapeutic to paste things a thousand times. Save the file, and
you’re ready to proceed.
Alternatively, you can find text_long.txt in the assets folder of the final project that comes
with this chapter.
Instead of calling readAsString on file , this time you’re calling openRead , which returns an object of type
Stream<List<int>> . That’s a lot of angle brackets, but Stream<List<int>> simply means it’s a stream that
periodically produces a list, and that list is a list of integers. The integers are the byte values, and the list is the
chunk of data being passed in.
To subscribe for notifications whenever new data comes in the stream, you call listen and pass it an anonymous
function that takes a single parameter. The data parameter here is of type List<int> , which gives you access to
the chunk of data coming in from the file.
Because each integer in the list is one byte, calling data.length will tell you the number of bytes in the chunk.
177
Dart Apprentice: Beyond the Basics Chapter 13: Streams
Note: By default, only a single object can listen to a stream. This is known as a
single-subscription stream. If you want more than one object to be notified of
stream events, you need to create a broadcast stream, which you could do like
so:
Run the code in main , and you’ll see something like the following:
65536
65536
65536
65536
65536
65536
52783
At least on the computer used while writing this chapter, the data was all in 65,536-byte
chunks until the final one, which was smaller because it didn’t quite fill up the 65,536-byte
buffer size. Your final chunk might be a different size than the one shown here, depending
on how therapeutic your copy-and-paste session was.
The await for keywords cause the loop to pause until the next data event comes in. Run
this, and you’ll see the same results as before.
Error Handling
Like futures, stream events can also include an error rather than a value.
178
Dart Apprentice: Beyond the Basics Chapter 13: Streams
Be a responsible programmer and plan how to handle errors. Callbacks and try-catch
blocks both work.
Using a Callback
One way to handle errors is to use the onError callback like so:
When an error occurs, it won’t cancel the stream, and you’ll continue to receive more data events. If you want to
cancel the stream after an error, listen also has a cancelOnError parameter that you can set to true .
When a stream finishes sending all its data, it’ll fire a done event. This gives you a chance to respond with an
onDone callback.
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');
}
179
Dart Apprentice: Beyond the Basics Chapter 13: Streams
In this example, you’re catching all exceptions. A more robust solution would check for
specific errors like FileSystemException , which Dart would throw if the file didn’t exist.
Run either the callback version or the try-catch version, and you’ll see the same chunk
sizes as before, with the additional text “All finished” printed at the end.
Change the filename to something nonexistent, like pink_elephants.txt , and rerun the code.
Confirm that you have a FileSystemException .
FileSystemException: Cannot open file, path = 'assets/pink_elephants.txt' (OS Error: No such file or directory, errno = 2)
All finished
Even with the exception, the finally block (or onDone callback if that’s what you used) still
printed “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. But 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';
void main() {
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');
},
);
}
180
Dart Apprentice: Beyond the Basics Chapter 13: Streams
Calling listen returns a StreamSubscription , which is part of the dart:async library. Keeping a
reference to that in the subscription variable allows you to cancel the subscription
whenever you want. In this case, you cancel it after the first data event.
Run the code, and you’ll only see 65536 printed once. The onDone callback was never
called because the stream never completed.
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 bytes
list. Those bytes represent text, though, so you’re going to transform the data from
numbers to text.
For this demonstration, there’s no need to use a large text file, so you’ll switch back to the
445-byte version of Lorem Ipsum in text.txt.
Run that, and you’ll see a long list of bytes in decimal form:
Although different computers encode text files using different encodings, the abbreviated
list above is from a computer that uses UTF-8 encoding. You might recall that UTF-16 uses
16-bit, or 2-byte, code units to encode Unicode text. UTF-8 uses one to four 8-bit code
units to encode Unicode text. Because for values of 127 and below, UTF-8 and Unicode
code points are the same, English text only takes one byte per letter. This makes file sizes
smaller than UTF-16 encoding. The smaller size helps when saving to disk or sending data
over a network.
If you look up 76 in Unicode, you see that it’s the capital letter L, 111 is o, and on it goes
with Lorem ipsum dolor sit ….
181
Dart Apprentice: Beyond the Basics Chapter 13: Streams
32 48 0 64 @ 80 P 96 ` 112 p
33 ! 49 1 65 A 81 Q 97 a 113 q
34 '' 50 2 66 B 82 R 98 b 114 r
35 # 51 3 67 C 83 S 99 c 115 s
36 $ 52 4 68 D 84 T 100 d 116 t
37 % 53 5 69 E 85 U 101 e 117 u
40 ( 56 8 72 H 88 X 104 h 120 x
41 ) 57 9 73 I 89 Y 105 i 121 y
42 * 58 : 74 J 90 Z 106 j 122 z
43 + 59 ; 75 K 91 [ 107 k 123 {
45 - 61 = 77 M 93 ] 109 m 125 }
Make sure you have the following imports and main method:
import 'dart:convert';
import 'dart:io';
The main difference here is that you’re using transform . This method takes the input from
the original stream, transforms it with a StreamTransformer and outputs a new stream, which
you can listen to or loop over as before. In this case, the stream transformer was the
dart:convert library’s utf8.decoder , which takes a list of bytes and converts them to a string.
Run the code, and you’ll see the Lorem Ipsum passage printed in plain text.
182
Dart Apprentice: Beyond the Basics Chapter 13: Streams
Exercise
The following code produces a stream that outputs an integer every second and stops after
the tenth time.
Stream<int>.periodic(
Duration(seconds: 1),
(value) => value,
).take(10);
2 Use await for to print the value of the integer on each data event coming from the stream.
Say, for example, you’re writing an audio player plugin. You need to take the events that
the underlying platform provides and pass them on to Dart. Using a stream is a natural
choice for continuous events like playback state changes or the current play position.
Because the data comes from outside of Dart, though, you have to create the stream
yourself. The rest of this chapter will show you how to do that.
Stream.empty : A stream with no values or errors. It’s done as soon as you listen to it.
Stream.value : A stream with a single value.
Stream.error : A stream with a single error.
Stream.fromFuture : Converts a future to a stream.
183
Dart Apprentice: Beyond the Basics Chapter 13: Streams
Feel free to try them all out. The example below will demonstrate building a stream with
the fromFutures constructor.
First, create a few futures by replacing the contents of main with the following code:
stream.listen((data) {
print(data);
});
Note: Be sure to add the comma after the last future in the list so they’re
formatted vertically. That way, they go gently down the stream. :]
Row
row
row
your boat
184
Dart Apprentice: Beyond the Basics Chapter 13: Streams
A generator is a function that produces multiple values in a sequence. As you may recall,
Dart has two types of generators: synchronous and asynchronous.
You learned about synchronous generators in Chapter 15, “Iterables”, of Dart Apprentice:
Fundamentals. But to review, a synchronous generator returns its values as an iterable.
These values are available on demand. You can get them as soon as you need them. That’s
why they’re called synchronous.
Here’s an example of a synchronous generator function that provides the squares of all the
integers from 1 to 100 as an iterable:
Recall that sync* , read “sync star”, is what defines the function as a synchronous
generator and that yield provides the values to the iterable.
By comparison, an asynchronous generator returns its values as a stream. You can’t get
them whenever you want. You have to wait for them. That’s why it’s called asynchronous.
When creating an asynchronous generator, use the async* keyword, which you can read
as “async star”.
Note: It’s easy to forget the difference between async and async* . Here’s a
reminder: Functions with async return futures, and functions with async*
return streams.
185
Dart Apprentice: Beyond the Basics Chapter 13: Streams
…this is an async* function, so the function returns a stream, and like synchronous functions, this
function also uses the yield keyword to return values in the stream, but unlike synchronous functions,
you don’t get all the values on demand, so here you’re waiting for 500 milliseconds every time you loop
because the data here is just simulating data you might get from a database or the user’s device or the
web or something…
stream.listen((data) {
print(data);
});
consciousness gives you the stream, which you listen to in the usual way.
Run that, and you’ll see the following text written to the console one line every half second:
con
scious
ness
Before diving into the code, it would help to understand how streams work.
The way to add data or errors to a stream is with what’s called a sink. You can think of this
like your kitchen sink with water flowing out of it into a pipe. The water pipe is like a
stream. Throwing a grape into the sink is like adding a data value event to the stream. The
grape gets washed through the sink’s drain and enters the water stream flowing through
the pipe. Alternatively, you could throw a cherry in the sink, and it will have the same fate
as the grape. Putting in a cherry is like adding an error event. You can also close the sink.
186
Dart Apprentice: Beyond the Basics Chapter 13: Streams
Think of that like putting a plug in the hole. No more data or errors can enter the stream.
value
Sink
Stream
Because adding data and errors are events, a sink is also called an event sink.
When you use a stream controller, it creates and manages the sink and stream internally.
// 1
final controller = StreamController<String>();
final stream = controller.stream;
final sink = controller.sink;
// 2
stream.listen(
(value) => print(value),
onError: (Object error) => print(error),
onDone: () => print('Sink closed'),
);
// 3
sink.add('grape');
sink.add('grape');
sink.add('grape');
sink.addError(Exception('cherry'));
sink.add('grape');
sink.close();
1 Create a stream controller of type String . Internally, the controller will take care of creating a string stream and
also a sink.
2 Listen for and handle data, error and done events on the stream.
3 Add some data values and errors to the sink. These will flow into the stream. Finally, close the sink. sink is of
type StreamSink , which implements EventSink . The EvenSink interface ensures you have the add ,
addError and close methods.
187
Dart Apprentice: Beyond the Basics Chapter 13: Streams
Testing It Out
Run your code, and you’ll see the following lines in the console:
grape
grape
grape
Exception: cherry
grape
Sink closed
It works! As you can see, even the low-level solution wasn’t very difficult to implement.
If you were making a library package for other people to use, you would probably make the
stream controller and the sink private. Just expose the stream to the library users. See the
solution to Challenge 2 for an example.
Challenges
Before going on to the next chapter, here are some challenges to test your knowledge of
streams. 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.
Your challenge is to transform the stream from bytes to strings and see how many bytes
each data chunk is. Add error handling, and when the stream finishes, close the client.
188
Dart Apprentice: Beyond the Basics Chapter 13: Streams
coinFlipper.onFlip.listen((coin) {
print(coin);
});
coinFlipper.start();
Key Points
A stream, which is of type Stream , is like 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 create streams with Stream constructors, asynchronous generators or stream controllers.
189
Dart Apprentice: Beyond the Basics
14 Isolates
Written by Jonathan Sande
Most of the time, running your code synchronously is fine, and for long-running I/O tasks,
you can use Dart libraries that return futures or streams. But sometimes, you might
discover your code is too computationally expensive and degrades your app’s
performance. That’s when you should offload that code to a separate thread so it can run
in parallel.
As you recall from Chapter 11, “Concurrency”, the way to achieve parallelism in Dart is to
create a new isolate. Isolates are so named because their memory and code are isolated
from the outside world. An isolate’s memory isn’t accessible from another isolate, and each
isolate has its own thread for running Dart code in an event loop. The only way to
communicate from one isolate to another is through message passing. Thus, when a
worker isolate finishes a task, it passes the results back to the main isolate as a message.
Isolate Isolate
Message
Memory Memory
Message
Event Loop Event Loop
The description above is fine from the developer’s perspective. That’s all you need to
know. The internal implementation, though, is somewhat more complex. When you create
a new isolate, Dart adds it to an isolate group. The isolate group shares resources between
the isolates, so creating a new isolate is fast and memory efficient. This includes sharing
the available memory, also called a heap. Isolates still can’t modify the mutable objects in
other isolates, but they can share references to the same immutable objects. In addition to
sharing the heap, isolate groups have helper threads to work with all the isolates. This is
more efficient than performing these tasks separately for each isolate. An example of this
is garbage collection.
190
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
Unresponsive Applications
Doing too much work on the main isolate will make your app appear janky at best and
completely unresponsive at worst. This can happen with both synchronous and
asynchronous code.
String playHideAndSeekTheLongVersion() {
var counting = 0;
for (var i = 1; i <= 10000000000; i++) {
counting = i;
}
return '$counting! Ready or not, here I come!';
}
Counting to 10 billion takes a while — even for a computer. If you run that function in a
Flutter app, your app’s UI will freeze until the function finishes.
Run the function now from the body of main like so:
Unless you have a reeeeally nice computer, you’ll notice a significant pause until the
counting finishes. That was the CPU doing a lot of work.
191
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
Another long wait. That would be a quick uninstall followed by a one-star rating if it
happened on your phone app.
Since for all practical purposes isolates don’t share any memory, they can only
communicate by sending messages. To send a message, you need a send port and a
receive port. Picture a receive port like an audio speaker that listens for messages and
plays them when they come. Every receive port has a send port, which you can picture as
a microphone connected by a long cord back to the receive port. Message communication
happens only in one direction. You send messages with the send port and listen to them
with the receive port. There’s no way to use the receive port to send messages to the send
port.
192
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
Send
Port
Receive
Port
Normally, before you spawn a new isolate, you first create a ReceivePort object. Then, when
you spawn the isolate, you pass it a reference to the SendPort property of your receive
port. That way, the new isolate can send messages over the send port back to the main
isolate’s receive port.
Message
Receive Send
Port Port
This type of one-way communication is useful for one-off tasks. You give an isolate some
work to do, and when it’s finished, it returns the result over the send port.
Performing a scientific calculation.
Processing an image.
In the next section, you’ll move your hide-and-seek function over to an isolate. This will
demonstrate the steps you need to take when spawning an isolate and setting up the
communication ports.
import 'dart:isolate';
193
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
// 1
void playHideAndSeekTheLongVersion(SendPort sendPort) {
var counting = 0;
for (var i = 1; i <= 1000000000; i++) {
counting = i;
}
final message = '$counting! Ready or not, here I come!';
// 2
Isolate.exit(sendPort, message);
}
2 Calling Isolate.exit sends your message over the send port and then shuts the isolate down.
// 2
await Isolate.spawn<SendPort>(
// 3
playHideAndSeekTheLongVersion,
// 4
receivePort.sendPort,
);
// 5
final message = await receivePort.first as String;
print(message);
}
1 You created a receive port to listen for messages from the new isolate.
2 Next, you spawned a new isolate and gave it two arguments. Specifying SendPort as the generic type tells Dart
the type of the entry-point function parameter.
194
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
3 The first argument of Isolate.spawn is the entry-point function. That function must be a top-level or static
function. It must also take a single argument.
4 The second argument of Isolate.spawn is the argument for the entry-point function. In this case, it’s a
SendPort object.
5 ReceivePort implements the Stream interface, so you can treat it like a stream. Calling await
receivePort.first waits for the first message coming in the stream and then cancels the stream subscription.
playHideAndSeekTheLongVersion only sends a single message; that’s all you need to wait for.
Run the code above, and after a pause, you’ll see the following output:
You counted to only a billion this time, so the pause was shorter. Because this was done on
another isolate, though, even 10 billion wouldn’t freeze your app’s UI.
Note: Flutter has a highly simplified way of starting a new isolate, performing
some work and then returning the result using a function called compute .
Rather than passing the function a send port, you just pass it any needed values.
In this case, you could just pass it the number to count to:
var counting = 0;
for (var i = 1; i <= 1000000000; i++) {
counting = i;
}
195
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
SendPort.send is the way to send a message over the send port. Calling it three times means you send three
messages.
This time, you don’t shut down the isolate here. Instead, you send null as a signal that you’re finished. null
doesn’t need to be your signal. You could also send the string 'done' or 'finished' . You just have to agree on the
signal with the main isolate that’s listening.
Note: In addition to strings and null , Dart allows you to send almost any data
type with SendPort.send as long as you’re sending a message to another isolate in
the same isolate group. This even includes user-defined data types like User or
Person , but it comes with some restrictions. For example, you can’t send Socket
or ReceivePort objects. If the isolate is in a different isolate group, which you can
create using the Isolate.spawnUri constructor, you can send only a few basic data
types.
Dart sends immutable objects like strings by reference, which makes passing
them very fast. Mutable objects, on the other hand, are copied. That can take
longer for large objects with many properties which in turn have other
properties. For example, person might include properties like person.home.address
and person.job.duties .
receivePort.listen((Object? message) {
if (message is String) {
print(message);
} else if (message == null) {
receivePort.close();
isolate.kill();
}
});
Because receivePort is a stream, you can listen to it like any other stream. If the message is
a string, you just print it. But if the message is null , that’s your signal to close the receive
port and shut down the isolate.
196
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
An easy way to accomplish this is to make the function parameter a list or a map instead of
a send port. Then, you can make the first element the send port and add as many other
elements as you need for additional arguments.
var counting = 0;
for (var i = 1; i <= countTo; i++) {
counting = i;
}
The parameter now is List<Object> arguments . This isn’t quite as readable as having
separately named parameters, but it allows you to pass in as many arguments as you like.
With a list, you access the arguments by index. Your code assumes that arguments[0] is the
send port and arguments[1] is the integer you’re counting to.
Note: If you want to use a map instead, write Map<String, Object> arguments as the
function parameter. Then, you could extract the send port with
arguments['sendPort'] and the integer with arguments['countTo']; . This offers the
advantage of being somewhat more readable than arguments[0] and arguments[1] .
You’ve updated the entry-point function, but you also must modify how you create the
isolate.
197
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
Rerun the code, and you should see the new result:
Here are some examples of long-running tasks where two-way communication may be
necessary:
For two-way communication, both sides need a send port and a receive port:
Two-way communication isn’t built into isolates by default, but you can set it up in a two-
step process:
1 Create a receive port in the parent isolate and pass its send port to the child isolate. This allows the child to send
messages to the parent.
2 Create a receive port in the child isolate and send that receive port’s send port back to the parent isolate. This
allows the parent to send messages to the child.
198
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
Message
Receive Send
Port Port
Message
Send Receive
Port Port
The sections below will guide you through setting up two-way communication. In the
example, Earth will represent the main or parent isolate, and Mars will represent the
worker or child isolate. The example will demonstrate two-way communication as Earth
and Mars communicate back and forth.
import 'dart:io';
This will give you access to the sleep function, which you’ll use below.
class Work {
Future<int> doSomething() async {
print('doing some work...');
sleep(Duration(seconds: 1));
return 42;
}
199
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
You can use both sleep and Future.delayed to pause your program. But sleep is
synchronous, so it will block all execution of other code for the full duration you specify. If
you used it in an app with a user interface, your app would become unresponsive during
that time. Here, sleep represents some computationally intensive task that you need to
run on another isolate. In the example that follows, this is work that Earth requires but is
offloading to Mars.
First, make sure you still have the dart:isolate import at the top of the project file:
import 'dart:isolate';
// 1
Future<void> _entryPoint(SendPort sendToEarthPort) async {
// 2
final receiveOnMarsPort = ReceivePort();
sendToEarthPort.send(receiveOnMarsPort.sendPort);
// 3
final work = Work();
1 sendToEarthPort is the send port that belongs to Earth’s receive port. The Mars isolate can use this port to send
messages back to the Earth isolate.
2 In the second step of setting up two-way communication, you create a receive port in the child isolate and send its
send port back to the parent isolate. Thus, the first “message” you send back to Earth is
receiveOnMarsPort.sendPort .
3 You create an instance of Work inside _entryPoint . Now, you’re ready to perform your heavy work on the Mars
isolate.
The diagram below pictures what you’re trying to accomplish. You’ve begun setting up the
ports on the Mars side, and you’ll create the Earth ports later.
200
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
Earth Mars
Replace the comment // TODO: add listener from above with the following listener:
201
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
It takes at least five minutes for messages from Earth to reach Mars in real life.
1
Pausing for a second here will make the final result feel a bit like interplanetary
communication.
2
Depending on the message from Earth, you can respond in different ways. And
because you have Earth’s send port, you can send messages back to Earth.
3
Generally, what you’ll do when you listen to messages from the parent isolate is to
map some string message to its corresponding method on your worker class. For
example, you match the string 'doSomething' to the method work.doSomething and
'doSomethingElse' to work.doSomethingElse . This is a more realistic scenario than saying,
“Hey”.
4
When these tasks have completed, you pass the result back to Earth over its send
port. Remember that these methods will complete asynchronously. Including the
method name in the message will help Earth know which method call this is coming
from.
// 1
class Earth {
// 2
final _receiveOnEarthPort = ReceivePort();
SendPort? _sendToMarsPort;
Isolate? _marsIsolate;
// 3
void dispose() {
_receiveOnEarthPort.close();
_marsIsolate?.kill();
_marsIsolate = null;
}
}
202
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
1 Earth encapsulates all your isolate communication code. It represents the main isolate.
2 You’ve defined a receive port to listen to messages from the Mars child isolate and a send port to send messages
back to Mars. Mars will give you this send port later after you’ve spawned the isolate.
3 When you finish the work on Mars, you can call dispose to shut the isolate down and clean up the resources.
Isolate.spawn assigns a value to _marsIsolate . You provide the Mars _entryPoint function with
a send port as an argument.
Replace the comment // TODO: add listener above with the following listener on the
_receiveOnEarthPort stream:
203
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
// 3
else if (messageFromMars is Map) {
final method = messageFromMars['method'] as String;
final result = messageFromMars['result'] as int;
print('The result of $method is $result');
}
// 4
else if (messageFromMars == 'done') {
print('shutting down');
dispose();
}
});
1 Recall that the first message you sent back to Earth from Mars was the send port for Mars’ receive port. Thus, the
first message you receive in this message stream should be of type SendPort . This is your messaging link to
Mars, so save a reference to it in _sendToMarsPort .
2 Respond to messages from Mars. You can’t directly call functions in the Mars isolate, but you can send strings that
will trigger function calls. You’ve already mapped those strings to their respective functions when you wrote
_entryPoint earlier.
3 Because you can’t directly call functions, you also don’t directly get a function’s return value. However, you can
listen for a message you know takes the form of a return value. In the case of _entryPoint , you defined the return
value to be of type Map where the keys are method and result . As you’ve seen, maps and lists are useful
when you want to pass multiple values.
4 The isolate sends a message that it’s all finished with its work now, so you can shut it down.
Note: As an alternative, you could use a StreamQueue rather than calling listen on
the receive port stream. This would make the code easier to read in some ways.
The example here didn’t use it, because that would have required more
background explanation. StreamQueue is worth looking into, though.
204
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
Run your code, and you should see the following exchange take place in one-second
increments as your main isolate on Earth communicates with the Mars isolate:
Note that the result of doSomething doesn’t come directly after Earth sends the message,
and the same is true for doSomethingElse . Isolate communication is inherently
asynchronous.
Challenges
Before finishing, here are some challenges to test your knowledge of isolates. 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.
If you worked through the challenges in Dart Apprentice: Fundamentals, Chapter 6, “Loops”,
you’ve already solved this. Repeat the 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.
205
Dart Apprentice: Beyond the Basics Chapter 14: Isolates
Key Points
You can run Dart code on another thread by spawning a new isolate.
Dart isolates don’t share any mutable memory state and communicate only through messages.
You can pass multiple arguments to an isolate’s entry-point function using a list or a map.
For long-running isolates, you can set up two-way communication by creating a send port and receive port for both
isolates.
One great thing about isolates is that when a child isolate crashes, it doesn’t need to bring
your whole app down. For example, you could have hundreds or even thousands of
separate isolates handling user connections on a server. One malicious user who finds a
way to crash the isolate wouldn’t affect the other users on the server. Learning how to
listen for and handle isolate errors would be a great next step.
206
Dart Apprentice: Beyond the Basics
15 Conclusion
Congratulations! You’ve reached the end of Dart Apprentice: Beyond the Basics. We hope
you’ve enjoyed reading this book and that the skills you’ve acquired will help you in all
your future Dart projects.
At this point, you can consider yourself a solid intermediate-level developer in the Dart
programming language. You understand the principles of object-oriented programming,
string manipulation, generics, error handling and asynchronous programming. If you go
on to develop Flutter apps or build backend servers in Dart, you shouldn’t have any
trouble with the language-related aspects of app development.
On your path to becoming an expert Dart developer, here are some topics you might
explore next:
Devtools: Profile your apps to measure their performance and find errors.
Don’t know where to start? The best way to learn is by making something useful, so choose
frontend development with Flutter or backend development with server-side Dart. After
that, learn unit testing. Unit testing is so important that we should probably add a chapter
on it in the next edition of this book. After unit testing, let the needs of your Flutter or
server app guide you in the next topic of study. Little by little, you’ll become proficient in a
wide variety of skills.
If you had any questions or comments as you worked through this book, please stop by our
forums at https://forums.kodeco.com and look for the particular forum category for this
book.
Thank you again for purchasing this book. Your continued support is what makes the
books, tutorials, videos and other things we do at Kodeco possible. We truly appreciate it!