An excerpt from Dart: Up and Running

Chapter 3. A Tour of the Dart Libraries

This chapter shows you how to use the major features in Dart’s libraries. It’s just an overview, and by no means comprehensive. Whenever you need more details about a class, consult the Dart API reference. Expect major changes to the Dart libraries before Dart’s first production release.

dart:core - Numbers, Collections, Strings, and More

The Dart core library provides a small but critical set of built-in functionality. This library is automatically imported into every Dart program.

Numbers

The dart:core library defines the num, int, and double classes, which have some basic utilities for working with numbers.

You can convert a string into an integer or double with the parse() methods of int and double, respectively:

lang-dart
ch03/number-tests.dart
assert(int.parse('42') == 42);
assert(double.parse('0.50') == 0.5);

Use the toString() method (defined by Object) to convert an int or double to a string. To specify the number of digits to the right of the decimal, use toStringAsFixed() (defined by num). To specify the number of significant digits in the string, use toStringAsPrecision() (also in num):

lang-dart
ch03/number-tests.dart
// Convert an int to a string.
assert(42.toString() == '42');

// Convert a double to a string.
assert(123.456.toString() == '123.456');

// Specify the number of digits after the decimal.
assert(123.456.toStringAsFixed(2) == '123.46');

// Specify the number of significant figures.
assert(123.456.toStringAsPrecision(2) == '1.2e+2');
assert(double.parse('1.2e+2') == 120.0);

For more information, see the API documentation for int, double, and num. Also see the section called “dart:math - Math and Random”.

Strings and Regular Expressions

A string in Dart is an immutable sequence of UTF-16 code units. The language tour has more information about strings. You can use regular expressions (RegExp objects) to search within strings and to replace parts of strings.

The String class defines such methods as split(), contains(), startsWith(), endsWith(), and more.

Searching inside a string

You can find particular locations within a string, as well as check whether a string begins with or ends with a particular pattern. For example:

lang-dart
ch03/string-tests.dart
// Check whether a string contains another string.
assert('Never odd or even'.contains('odd'));

// Does a string start with another string?
assert('Never odd or even'.startsWith('Never'));

// Does a string end with another string?
assert('Never odd or even'.endsWith('even'));

// Find the location of a string inside a string.
assert('Never odd or even'.indexOf('odd') == 6);

Extracting data from a string

You can get the individual characters or UTF-16 code units from a string as Strings or ints, respectively.

You can also extract a substring or split a string into a list of substrings:

lang-dart
ch03/string-tests.dart
// Grab a substring.
assert('Never odd or even'.substring(6, 9) == 'odd');

// Split a string using a string pattern.
var parts = 'structured web apps'.split(' ');
assert(parts.length == 3);
assert(parts[0] == 'structured');

// Get a character (as a string) by index.
assert('Never odd or even'[0] == 'N');

// Use split() with an empty string parameter to get a list of
// all characters (as Strings); good for iterating.
for (var char in 'hello'.split('')) {
  print(char);
}

// Get all the characters in the string as a list of UTF-16 code units.
// Some characters might require two code units.
var codeUnitList = 'Never odd or even'.codeUnits.toList();
assert(codeUnitList[0] == 78);

Converting to uppercase or lowercase

You can easily convert strings to their uppercase and lowercase variants:

lang-dart
ch03/string-tests.dart
// Convert to uppercase.
assert('structured web apps'.toUpperCase() == 'STRUCTURED WEB APPS');

// Convert to lowercase.
assert('STRUCTURED WEB APPS'.toLowerCase() == 'structured web apps');

Trimming and empty strings

Remove all leading and trailing white space with trim(). To check whether a string is empty (length is zero), use isEmpty.

lang-dart
ch03/string-tests.dart
// Trim a string.
assert('  hello  '.trim() == 'hello');

// Check whether a string is empty.
assert(''.isEmpty);

// Strings with only white space are not empty.
assert(!'  '.isEmpty);

Replacing part of a string

Strings are immutable objects, which means you can create them but you can’t change them. If you look closely at the String API docs, you’ll notice that none of the methods actually changes the state of a String. For example, the method replaceAll() returns a new String without changing the original String:

lang-dart
ch03/string-tests.dart
var greetingTemplate = 'Hello, NAME!';
var greeting = greetingTemplate.replaceAll(new RegExp('NAME'), 'Bob');

assert(greeting != greetingTemplate); // greetingTemplate didn't change.

Building a string

To programmatically generate a string, you can use StringBuffer. A StringBuffer doesn’t generate a new String object until toString() is called.

lang-dart
ch03/string-tests.dart
var sb = new StringBuffer();

sb..write('Use a StringBuffer ')
  ..writeAll(['for ', 'efficient ', 'string ', 'creation'])
  ..write('.');

var fullString = sb.toString();

assert(fullString ==
    'Use a StringBuffer for efficient string creation.');

Regular expressions

The RegExp class provides the same capabilities as JavaScript regular expressions. Use regular expressions for efficient searching and pattern matching of strings.

lang-dart
ch03/string-tests.dart
// A regular expression for one or more digits
var numbers = new RegExp(r'\d+');

var allCharacters = 'llamas live fifteen to twenty years';
var someDigits = 'llamas live 15 to 20 years';

// Contains() can use a regular expression.
assert(!allCharacters.contains(numbers));
assert(someDigits.contains(numbers));

// Replace every match with another string.
var exedOut = someDigits.replaceAll(numbers, 'XX');
assert(exedOut == 'llamas live XX to XX years');

You can work directly with the RegExp class, too. The Match class provides access to a regular expression match.

lang-dart
ch03/string-tests.dart
var numbers = new RegExp(r'\d+');
var someDigits = 'llamas live 15 to 20 years';

// Check whether the reg exp has a match in a string.
assert(numbers.hasMatch(someDigits));

// Loop through all matches.
for (var match in numbers.allMatches(someDigits)) {
  print(match.group(0)); // 15, then 20
}

More information

Refer to the String API docs for a full list of methods. Also see the API docs for StringBuffer, Pattern, RegExp, and Match.

Collections

Dart ships with a core collections API, which includes classes for lists, sets, and maps.

Lists

As the language tour shows, you can use literals to create and initialize lists. Alternatively, use one of the List constructors. The List class also defines several methods for adding items to and removing items from lists.

lang-dart
ch03/list-tests.dart
// Use a List constructor.
var vegetables = new List();

// Or simply use a list literal.
var fruits = ['apples', 'oranges'];

// Add to a list.
fruits.add('kiwis');

// Add multiple items to a list.
fruits.addAll(['grapes', 'bananas']);

// Get the list length.
assert(fruits.length == 5);

// Remove a single item.
var appleIndex = fruits.indexOf('apples');
fruits.removeAt(appleIndex);
assert(fruits.length == 4);

// Remove all elements from a list.
fruits.clear();
assert(fruits.length == 0);

Use indexOf() to find the index of an object in a list:

lang-dart
ch03/list-tests.dart
var fruits = ['apples', 'oranges'];

// Access a list item by index.
assert(fruits[0] == 'apples');

// Find an item in a list.
assert(fruits.indexOf('apples') == 0);

Sort a list using the sort() method. You must provide a sorting function that compares two objects. This sorting function must return < 0 for smaller, 0 for the same, and > 0 for bigger. The following example uses compareTo(), which is defined by Comparable and implemented by String.

lang-dart
ch03/list-tests.dart
var fruits = ['bananas', 'apples', 'oranges'];

// Sort a list.
fruits.sort((a, b) => a.compareTo(b));
assert(fruits[0] == 'apples');

Lists are parameterized types, so you can specify the type that a list should contain:

lang-dart
ch03/list-tests.dart
// This list should contain only strings.
var fruits = new List<String>();

fruits.add('apples');
var fruit = fruits[0];
assert(fruit is String);

// Generates static analysis warning, num is not a string.
fruits.add(5);  // BAD: Throws exception in checked mode.

Refer to the List API docs for a full list of methods.

Sets

A set in Dart is an unordered collection of unique items. Because a set is unordered, you can’t get a set’s items by index (position).

lang-dart
ch03/set-tests.dart
var ingredients = new Set();
ingredients.addAll(['gold', 'titanium', 'xenon']);
assert(ingredients.length == 3);

// Adding a duplicate item has no effect.
ingredients.add('gold');
assert(ingredients.length == 3);

// Remove an item from a set.
ingredients.remove('gold');
assert(ingredients.length == 2);

Use contains() and containsAll() to check whether one or more objects are in a set:

lang-dart
ch03/set-tests.dart
var ingredients = new Set();
ingredients.addAll(['gold', 'titanium', 'xenon']);

// Check whether an item is in the set.
assert(ingredients.contains('titanium'));

// Check whether all the items are in the set.
assert(ingredients.containsAll(['titanium', 'xenon']));

An intersection is a set whose items are in two other sets.

lang-dart
ch03/set-tests.dart
var ingredients = new Set();
ingredients.addAll(['gold', 'titanium', 'xenon']);

// Create the intersection of two sets.
var nobleGases = new Set.from(['xenon', 'argon']);
var intersection = ingredients.intersection(nobleGases);
assert(intersection.length == 1);
assert(intersection.contains('xenon'));

Refer to the Set API docs for a full list of methods.

Maps

A map, commonly known as a dictionary or hash, is an unordered collection of key-value pairs. Maps associate a key to some value for easy retrieval. Unlike in JavaScript, Dart objects are not maps.

You can declare a map using a terse literal syntax, or you can use a traditional constructor:

lang-dart
ch03/map-1.dart
// Map literals use strings as keys.
var hawaiianBeaches = {
  'oahu' : ['waikiki', 'kailua', 'waimanalo'],
  'big island' : ['wailea bay', 'pololu beach'],
  'kauai' : ['hanalei', 'poipu']
};

// Maps can be built from a constructor.
var searchTerms = new Map();

// Maps are parameterized types; you can specify what types
// the key and value should be.
var nobleGases = new Map<int, String>();

You add, get, and set map items using the bracket syntax. Use remove() to remove a key and its value from a map. [PENDING: It’s questionable that this example uses containsKey before containsKey is discussed.]

lang-dart
ch03/map-1.dart
var nobleGases = new Map<int, String>();

// Maps from constructors can use any object as a key.
// Integers and strings are common key types.
nobleGases[54] = 'xenon';

// Retrieve a value with a key.
assert(nobleGases[54] == 'xenon');

// Check whether a map contains a key.
assert(nobleGases.containsKey(54));

// Remove a key and its value.
nobleGases.remove(54);
assert(!nobleGases.containsKey(54));

You can retrieve all the values or all the keys from a map:

lang-dart
ch03/map-1.dart
var hawaiianBeaches = {
  'oahu' : ['waikiki', 'kailua', 'waimanalo'],
  'big island' : ['wailea bay', 'pololu beach'],
  'kauai' : ['hanalei', 'poipu']
};

// Get all the keys as an unordered collection (an Iterable).
var keys = hawaiianBeaches.keys;

assert(keys.length == 3);
assert(new Set.from(keys).contains('oahu'));

// Get all the values as an unordered collection (an Iterable of Iterables).
var values = hawaiianBeaches.values;
assert(values.length == 3);
assert(values.any((v) => v.indexOf('waikiki') != -1));

To check whether a map contains a key, use containsKey(). Because map values can be null, you cannot rely on simply getting the value for the key and checking for null to determine the existence of a key.

lang-dart
ch03/map-1.dart
var hawaiianBeaches = {
  'oahu' : ['waikiki', 'kailua', 'waimanalo'],
  'big island' : ['wailea bay', 'pololu beach'],
  'kauai' : ['hanalei', 'poipu']
};

assert(hawaiianBeaches.containsKey('oahu'));
assert(!hawaiianBeaches.containsKey('florida'));

Use the putIfAbsent() method when you want to assign a value to a key if and only if the key does not already exist in a map. You must provide a function that returns the value.

lang-dart
ch03/map-1.dart
var teamAssignments = {};
teamAssignments.putIfAbsent('Catcher', () => pickToughestKid());
assert(teamAssignments['Catcher'] != null);

Refer to the Map API docs for a full list of methods.

Common collection methods

List, Set, and Map share common functionality found in many collections. Some of this common functionality is defined by the Iterable class, which List and Set implement.

Note

Although Map doesn’t implement Iterable, you can get Iterables from it using the Map keys and values properties.

Use isEmpty to check whether a list, set, or map has no items:

lang-dart
ch03/collection-isEmpty.dart
var teas = ['green', 'black', 'chamomile', 'earl grey'];
assert(!teas.isEmpty);

To apply a function to each item in a list, set, or map, you can use forEach():

lang-dart
ch03/collection-apply-function.dart
var teas = ['green', 'black', 'chamomile', 'earl grey'];

teas.forEach((tea) => print('I drink $tea'));

When you invoke forEach() on a map, your function must take two arguments (the key and value):

lang-dart
ch03/map-1.dart
// NOTE: Do not depend on iteration order.
hawaiianBeaches.forEach((k,v) {
  print('I want to visit $k and swim at $v');
  // I want to visit oahu and swim at [waikiki, kailua, waimanalo], etc.
});

An alternative for Iterables is the map() method, which gives you all the results in a single object:

lang-dart
ch03/collection-apply-function.dart
var teas = ['green', 'black', 'chamomile', 'earl grey'];

var loudTeas = teas.map((tea) => tea.toUpperCase());
loudTeas.forEach(print);

Note

The object returned by map() is an Iterable that’s lazily evaluated: your function isn’t called until you ask for an item from the returned object.

To force your function to be called immediately on each item, use map().toList() or map().toSet():

lang-dart
ch03/collection-apply-function.dart
var loudTeaList = teas.map((tea) => tea.toUpperCase()).toList();

Use Iterable’s where() method to get all the items that match a condition. Use Iterable’s any() and every() methods to check whether some or all items match a condition.

lang-dart
ch03/collection-any-every.dart
var teas = ['green', 'black', 'chamomile', 'earl grey'];

// Chamomile is not caffeinated.
bool isDecaffeinated(String teaName) => teaName == 'chamomile';

// Use where() to find only the items that return true
// from the provided function.
var decaffeinatedTeas = teas.where((tea) => isDecaffeinated(tea));
// or teas.where(isDecaffeinated)

// Use any() to check whether at least one item in the collection
// satisfies a condition.
assert(teas.any(isDecaffeinated));

// Use every() to check whether all the items in a collection
// satisfy a condition.
assert(!teas.every(isDecaffeinated));

For a full list of methods, refer to the Iterable API docs, as well as those for List, Set, and Map.

Dates and Times

A DateTime object is a point in time. The time zone is either UTC or the local time zone.

You can create DateTime objects using several constructors:

lang-dart
ch03/date.dart
// Get the current date and time.
var now = new DateTime.now();

// Create a new DateTime with the local time zone.
var y2k = new DateTime(2000);   // January 1, 2000

// Specify the month and day.
y2k = new DateTime(2000, 1, 2); // January 2, 2000

// Specify the date as a UTC time.
y2k = new DateTime.utc(2000);   // January 1, 2000, UTC

// Specify a date and time in milliseconds since the Unix epoch.
y2k = new DateTime.fromMillisecondsSinceEpoch(946684800000, isUtc: true);

// Parse an ISO 8601 date.
y2k = DateTime.parse('2000-01-01T00:00:00Z');

The millisecondsSinceEpoch property of a date returns the number of milliseconds since the “Unix epoch”—January 1, 1970, UTC:

lang-dart
ch03/date.dart
var y2k = new DateTime.utc(2000);           // 1/1/2000, UTC
assert(y2k.millisecondsSinceEpoch == 946684800000);
var unixEpoch = new DateTime.utc(1970); // 1/1/1970, UTC
assert(unixEpoch.millisecondsSinceEpoch == 0);

Use the Duration class to calculate the difference between two dates and to shift a date’s time forward or backwards:

lang-dart
ch03/date.dart
var y2k = new DateTime.utc(2000);

// Add one year.
var y2001 = y2k.add(const Duration(days: 366));
assert(y2001.year == 2001);

// Subtract 30 days.
var december2000 = y2001.subtract(const Duration(days: 30));
assert(december2000.year == 2000);
assert(december2000.month == 12);

// Calculate the difference between two dates.
// Returns a Duration object.
var duration = y2001.difference(y2k);
assert(duration.inDays == 366); // y2k was a leap year.

Refer to the API docs for DateTime and Duration for a full list of methods.

Utility Classes

The core library contains various utility classes, useful for sorting, mapping values, and iterating.

Comparing objects

Implement the Comparable interface to indicate that an object can be compared to another object, usually for sorting. The compareTo() method returns < 0 for smaller, 0 for the same, and > 0 for bigger.

lang-dart
ch03/comparable.dart
class Line implements Comparable {
  final length;
  const Line(this.length);
  int compareTo(Line other) => length - other.length;
}

main() {
  var short = const Line(1);
  var long = const Line(100);
  assert(short.compareTo(long) < 0);
}

Implementing map keys

Each object in Dart automatically provides an integer hash code, and thus can be used as a key in a map. However, you can override the hashCode getter to generate a custom hash code. If you do, be sure to override the == operator, as well. Objects that are equal (via ==) must have identical hash codes. A hash code doesn’t have to be unique, but it should be well distributed.

Note: There’s disagreement over whether to include identical() in the == implementation. It might improve speed. They don’t do identical() automatically because, by convention, NaN != NaN.

lang-dart
ch03/map-keys.dart
class Person {
  String firstName, lastName;

  Person(this.firstName, this.lastName);

  // Override hashCode using strategy from Effective Java, Chapter 11.
  int get hashCode {
    int result = 17;
    result = 37 * result + firstName.hashCode;
    result = 37 * result + lastName.hashCode;
    return result;
  }

  // Always implement operator== if you override hashCode.
  bool operator==(other) {
    if (identical(other, this)) return true;
    return (other.firstName == firstName && other.lastName == lastName);
  }
}

main() {
  var p1 = new Person('bob', 'smith');
  var p2 = new Person('bob', 'smith');
  assert(p1.hashCode == p2.hashCode);
}

Iteration

The Iterable and Iterator classes support for-in loops. Extend (if possible) or implement Iterable whenever you create a class that can provide Iterators for use in for-in loops. Implement Iterator to define the actual iteration ability.

lang-dart
ch03/iterator.dart
class Process {
  // Represents a process...
}

class ProcessIterator implements Iterator<Process> {
  Process current;
  bool moveNext() {
    return false;
  }
}

// A mythical class that lets you iterate through all processes.
// Extends an Iterable subclass.
class Processes extends DoubleLinkedQueue<Process> {
  final Iterator<Process> iterator = new ProcessIterator();
}

main() {
  // Iterable objects can be used with for-in.
  for (var process in new Processes()) {
    // Do something with the process.
  }
}

Exceptions

The Dart core library defines many common exceptions and errors. Exceptions are considered conditions that you can plan ahead for and catch. Errors are conditions that you don’t expect or plan for.

A couple of the most common errors are:

NoSuchMethodError

Thrown when a receiving object (which might be null) does not implement a method.

ArgumentError

Can be thrown by a method that encounters an unexpected argument.

Throwing an application-specific exception is a common way to indicate that an error has occurred. You can define a custom exception by implementing the Exception interface:

lang-dart
ch03/exceptions.dart
class FooException implements Exception {
  final String msg;
  const FooException([this.msg]);
  String toString() => msg == null ? 'FooException' : msg;
}

For more information, see the section called “Exceptions” and the Exception API docs.

dart:async - Asynchronous Programming

Asynchronous programming often uses callback functions, but Dart provides an alternative: Future objects. A Future is like a promise for a result to be provided sometime in the future.

You have the option of using a Completer to produce a Future and, later, to supply a value to the Future. Completer, Future, and more are in the dart:async library.

The dart:async library works in both web apps and command-line apps. To use it, import dart:async.

Using Future and Completer

In the following example, the longExpensiveSearch() function schedules a query and immediately returns a Future object. Later, when the query finishes in some future event loop iteration, the completer completes with the results. The completion signals that the Future has a value, which runs the callback registered in Future’s then().

lang-dart
ch03/futures.dartFuture longExpensiveSearch() {
var completer = new Completer();

// Perform exhaustive search.
dbQuery('select * from table', onSuccess: (results) {
  // Sometime later,
  // found it!!
  completer.complete(results);
});

return completer.future;
}

Future doSearch() {
var future = longExpensiveSearch(); // Returns immediately.

return future.then((results) {
  // The following code executes when the operation is complete.
  print('Here are the results: $results');
});
}

Error Handling with Futures

If an error or exception is thrown from within a Future, you can catch it with catchError(). Be sure to invoke catchError() on the result of then()—not on the result of the original Future.

lang-dart
ch03/futures.dartFuture runSearch() {
var future = longExpensiveSearch(); // Returns immediately.

return future.then((results) {
  print('Here are the results: $results');
}).catchError((e) {
  print("Oops! Encountered $e");
});
}

The then().catchError() pattern is the asynchronous version of try-catch.

Chaining Multiple Asynchronous Methods

The then() method returns a Future, which is a useful way to chain multiple asynchronous methods to run in a certain order. If the callback registered with then() returns a Future, then it is simply returned as is by then(). If the callback returns a value of any other type, a new Future instance is created and is completed with the value.

lang-dart
ch03/futures.dartFuture runQuery() {
Future result = costlyQuery();

return result.then((value) => expensiveWork())
             .then((value) => lengthyComputation())
             .then((value) => print('done!'))
             .catchError((exception) => print('DOH!'));
}

In the above example, the methods run in the following order:

  1. costlyQuery()

  2. expensiveWork()

  3. lengthyComputation()

Waiting for Multiple Futures

Sometimes your algorithm needs to initiate many asynchronous methods and wait for each one to complete before continuing. Use the Future.wait() static method to manage multiple Futures and wait for them all to complete:

lang-dart
Future deleteDone = deleteLotsOfFiles();
Future copyDone = copyLotsOfFiles();
Future checksumDone = checksumLotsOfOtherFiles();

new Future.wait([deleteDone, copyDone, checksumDone]).then((List values) {
  print('Done with all the long steps');
});

More Information

For examples of using Future, see the section called “dart:io - I/O for Command-Line Apps”.

dart:math - Math and Random

The Math library provides common functionality such as sine and cosine, maximum and minimum, and constants such as pi and e. Most of the functionality in the Math library is implemented as top-level functions.

To use the Math library in your app, import dart:math. The following examples use the prefix math to make clear which top-level functions and constants are from the Math library:

lang-dart
ch03/math-tests.dart
import 'dart:math' as math;

Trigonometry

The Math library provides basic trigonometric functions:

lang-dart
ch03/math-tests.dart
// Cosine
assert(math.cos(math.PI) == -1.0);

// Sine
var degrees = 30;
var radians = degrees * (math.PI / 180);
// radians is now 0.52359.
var sinOf30degrees = math.sin(radians);

// Truncate the decimal places to 2.
assert(double.parse(sinOf30degrees.toStringAsPrecision(2)) == 0.5);

Note

These functions use radians, not degrees!

Maximum and Mininum

The Math library provides optimized max() and min() methods:

lang-dart
ch03/math-tests.dart
assert(math.max(1, 1000) == 1000);
assert(math.min(1, -1000) == -1000);

Math Constants

Find your favorite constants—pi, e, and more—in the Math library:

lang-dart
ch03/math-tests.dart
// See the Math library for additional constants.
print(math.E);     // 2.718281828459045
print(math.PI);    // 3.141592653589793
print(math.SQRT2); // 1.4142135623730951

Random Numbers

Generate random numbers with the Random class. You can optionally provide a seed to the Random constructor.

lang-dart
ch03/math-tests.dart
var random = new math.Random();
random.nextDouble(); // Between 0.0 and 1.0: [0, 1)
random.nextInt(10);  // Between 0 and 9.

You can even generate random booleans:

lang-dart
ch03/math-tests.dart
var random = new math.Random();
random.nextBool();  // true or false

More Information

Refer to the Math API docs for a full list of methods. Also see the API docs for num, int, and double.

dart:html - Browser-Based Apps

Use the dart:html library to program the browser, manipulate objects and elements in the DOM, and access HTML5 APIs. DOM stands for Document Object Model, which describes the hierarchy of an HTML page.

Other common uses of dart:html are manipulating styles (CSS), getting data using HTTP requests, and exchanging data using WebSockets. HTML5 (and dart:html) has many additional APIs that this section doesn’t cover. Only web apps can use dart:html, not command-line apps.

Note

For a scalable, higher level approach to web app UIs, see the Web UI package.

To use the HTML library in your web app, import dart:html:

lang-dart
ch03_html/ch03_html.dart
import 'dart:html';

Manipulating the DOM

To use the DOM, you need to know about windows, documents, elements, and nodes.

A Window object represents the actual window of the web browser. Each Window has a document property (a Document object), which points to the document currently loaded. The Window object also has accessors to various APIs such as IndexedDB (for storing data), requestAnimationFrame() (for animations), and more. In tabbed browsers, each tab has its own Window object.

With the Document object, you can create and manipulate Elements within the document. Note that the document itself is an element and can be manipulated.

The DOM models a tree of Nodes. These nodes are often elements, but they can also be attributes, text, comments, and other DOM types. Except for the root node, which has no parent, each node in the DOM has one parent and might have many children.

Finding elements

To manipulate an element, you first need an object that represents it. You can get this object using a query.

Find one or more elements using the top-level functions query() and queryAll(). You can query by ID, class, tag, name, or any combination of these. The CSS Selector Specification guide defines the formats of the selectors such as using a # prefix to specify IDs and a period (.) for classes.

The query() function returns the first element that matches the selector, while queryAll() returns a collection of elements that match the selector.

lang-dart
ch03_html/ch03_html.dart
Element elem1 = query('#an-id');           // Find an element by id (an-id).
Element elem2 = query('.a-class');         // Find an element by class (a-class).
List<Element> elems1 = queryAll('div');    // Find all elements by tag (<div>).
List<Element> elems2 = queryAll('input[type="text"]'); // Find all text inputs.

// Find all elements with the CSS class 'class' inside of a <p>
// that is inside an element with the ID 'id'.
List<Element> elems3 = queryAll('#id p.class');

Manipulating elements

You can use properties to change the state of an element. Node and its subtype Element define the properties that all elements have. For example, all elements have classes, hidden, id, innerHtml, style, text, and title properties. Subclasses of Element define additional properties, such as the href property of AnchorElement.

Consider this example of specifying an anchor element in HTML:

lang-dart
ch03_html/ch03_html.dart
<a id="example" href="http://example.com">link text</a>

This <a> tag specifies an element with an href attribute and a text node (accessible via a text property) that contains the string “linktext”. To change the URL that the link goes to, you can use AnchorElement’s href property:

lang-dart
ch03_html/ch03_html.dart
query('#example').href = 'http://dartlang.org';

Often you need to set properties on multiple elements. For example, the following code sets the hidden property of all elements that have a class of “mac”, “win”, or “linux”. Setting the hidden property to true has the same effect as adding display:none to the CSS.

lang-dart
ch03_html/ch03_html.dart
<!-- In HTML: -->
<p>
  <span class="os linux">Words for Linux</span>
  <span class="os mac">Words for Mac</span>
  <span class="os win">Words for Windows</span>
</p>

// In Dart:
final osList = ['mac', 'win', 'linux'];

var userOs = 'linux'; // In real code you'd programmatically determine this.

for (var os in osList) {            // For each possible OS...
  bool shouldShow = (os == userOs); // Does this OS match the user's OS?
  for (var elem in queryAll('.$os')) { // Find all elements for this OS.
    elem.hidden = !shouldShow;      // Show or hide each element.
  }
}

When the right property isn’t available or convenient, you can use Element’s attributes property. This property has the type AttributeMap, which implements a map with keys that are strings (attribute names) and values that it automatically converts to strings. For a list of attribute names and their meanings, see the MDN Attributes page. Here’s an example of setting an attribute’s value:

lang-dart
ch03_html/ch03_html.dart
elem.attributes['someAttribute'] = 'someValue';

Creating elements

You can add to existing HTML pages by creating new elements and attaching them to the DOM. Here’s an example of creating a paragraph (<p>) element:

lang-dart
ch03_html/ch03_html.dart
var elem = new ParagraphElement();
elem.text = 'Creating is easy!';

You can also create an element by parsing HTML text. Any child elements are also parsed and created.

lang-dart
ch03_html/ch03_html.dart
var elem2 = new Element.html('<p>Creating <em>is</em> easy!</p>');

Note that elem2 is a ParagraphElement in the above example.

Attach the newly created element to the document by assigning a parent to the element. You can add an element to any existing element’s children. In the following example, body is an element, and its child elements are accessible (as a List<Element>) from the children property.

lang-dart
ch03_html/ch03_html.dart
document.body.children.add(elem2);

Adding, replacing, and removing nodes

Recall that elements are just a kind of node. You can find all the children of a node using the nodes property of Node, which returns a List<Node>. Once you have this list, you can use the usual List methods and operators to manipulate the children of the node.

To add a node as the last child of its parent, use the List add() method:

lang-dart
ch03_html/ch03_html.dart
// Find the parent by ID, and add elem as its last child.
query('#inputs').nodes.add(elem);

To replace a node, use the Node replaceWith() method:

lang-dart
ch03_html/ch03_html.dart
// Find a node by ID, and replace it in the DOM.
query('#status').replaceWith(elem);

To remove a node, use the Node remove() method:

lang-dart
ch03_html/ch03_html.dart
// Find a node by ID, and remove it from the DOM.
query('#expendable').remove();

Manipulating CSS styles

CSS, or cascading style sheets, is used to define the presentation styles of DOM elements. You can change the appearance of an element by attaching ID and class attributes to it.

Each element has a classes field, which is a list. Add and remove CSS classes simply by adding and removing strings from this collection. For example, the following sample adds the warning class to an element:

lang-dart
ch03_html/ch03_html.dart
var element = query('#message');
element.classes.add('warning');

It’s often very efficient to find an element by ID. You can dynamically set an element ID with the id property:

lang-dart
ch03_html/ch03_html.dart
var message = new DivElement();
message.id = 'message2';
message.text = 'Please subscribe to the Dart mailing list.';

You can reduce the redundant text in this example by using method cascades:

lang-dart
ch03_html/ch03_html.dart
var message = new DivElement()
    ..id = 'message2'
    ..text = 'Please subscribe to the Dart mailing list.';

While using IDs and classes to associate an element with a set of styles is best practice, sometimes you want to attach a specific style directly to the element:

lang-dart
ch03_html/ch03_html.dart
message.style
    ..fontWeight = 'bold'
    ..fontSize = '3em';

Handling events

To respond to external events such as clicks, changes of focus, and selections, add an event listener. You can add an event listener to any element on the page. Event dispatch and propagation is a complicated subject; research the details if you’re new to web programming.

Add an event handler using element.onEvent.listen(function), where Event is the event name and function is the event handler.

For example, here’s how you can handle clicks on a button:

lang-dart
ch03_html/ch03_html.dart
// Find a button by ID and add an event handler.
query('#submitInfo').onClick.listen((e) {
  // When the button is clicked, it runs this code.
  submitData();
});

Events can propagate up and down through the DOM tree. To discover which element originally fired the event, use e.target:

lang-dart
ch03_html/ch03_html.dart
document.body.onClick.listen((e) {
  var clickedElem = e.target;
  print('You clicked the ${clickedElem.id} element.');
});

To see all the events for which you can register an event listener, consult the API docs for ElementEvents and its subclasses. Some common events include:

  • change

  • blur

  • keyDown

  • keyUp

  • mouseDown

  • mouseUp

Using HTTP Resources with HttpRequest

Formerly known as XMLHttpRequest, the HttpRequest class gives you access to HTTP resources from within your browser-based app. Traditionally, AJAX-style apps make heavy use of HttpRequest. Use HttpRequest to dynamically load JSON data or any other resource from a web server. You can also dynamically send data to a web server.

The following examples assume all resources are served from the same web server that hosts the script itself. Due to security restrictions in the browser, the HttpRequest class can’t easily use resources that are hosted on an origin that is different from the origin of the app. If you need to access resources that live on a different web server, you need to either use a technique called JSONP or enable CORS headers on the remote resources.

Getting data from the server

The HttpRequest static method getString() is an easy way to get data from a web server. Use then() after getString() to specify the function that handles the string data.

lang-dart
ch03_html/ch03_2_html.dart
import 'dart:html';
import 'dart:async';

// A JSON-formatted file in the same location as this page.
var uri = 'data.json';

main() {
  // Read a JSON file.
  HttpRequest.getString(uri).then(processString);
}

processString(String jsonText) {
  parseText(jsonText);
}

The function you specify (in the example, processString()) runs when the data at the specified URI is successfully retrieved. In this case, we are dynamically loading a JSON file. Information about the JSON API is in the section called “dart:json - Encoding and Decoding Objects”.

Add .catchError() after the call to .then() to specify an error handler:

lang-dart
ch03_html/ch03_2_html.dart
...
HttpRequest.getString(uri)
    .then(processString)
    .catchError(handleError);
...
handleError(error) {
  print('Uh oh, there was an error.');
  print(error.toString());
}

If you need access to the HttpRequest, not just the text data it retrieves, you can use the request() static method instead of getString(). Here’s an example of reading XML data:

lang-dart
ch03_html/ch03_2_html.dart
import 'dart:html';
import 'dart:async';

// An XML-formatted file in the same location as this page.
var xmlUri = 'data.xml';

main() {
  // Read an XML file.
  HttpRequest.request(uri)
      .then(processRequest)
      .catchError(handleError);
}

processRequest(HttpRequest request) {
  var xmlDoc = request.responseXml;
  try {
    var license = xmlDoc.query('license').text;
    print('License: $license');
  } catch(e) {
    print('$xmlUri doesn\'t have correct XML formatting.');
  }
}
...

You can also use the full API to handle more interesting cases. For example, you can set arbitrary headers.

The general flow for using the full API of HttpRequest is as follows:

  1. Create the HttpRequest object.

  2. Open the URL with either GET or POST.

  3. Attach event handlers.

  4. Send the request.

For example:

lang-dart
tutorial/servers/web/client.dart
import 'dart:html';
...
var httpRequest = new HttpRequest();
httpRequest.open('POST', dataUrl);
httpRequest.onLoadEnd.listen((e) => loadEnd(httpRequest));
httpRequest.send(encodedData);

Sending data to the server

HttpRequest can send data to the server using the HTTP method POST. For example, you might want to dynamically submit data to a form handler. Sending JSON data to a RESTful web service is another common example.

Submitting data to a form handler requires you to provide name-value pairs as URI-encoded strings. (Information about the URI API is in the section called “dart:uri - Manipulating URIs”.) You must also set the Content-type header to application/x-www-form-urlencode if you wish to send data to a form handler.

lang-dart
ch03/ch03_3_html/*
import 'dart:html';
import 'dart:uri';
...
String encodeMap(Map data) {
  return data.keys.map((k) {
    return '${encodeUriComponent(k)}=${encodeUriComponent(data[k])}';
  }).join('&');
}

loadEnd(HttpRequest request) {
  if (request.status != 200) {
    print('Uh oh, there was an error of ${request.status}');
  } else {
    print('Data has been posted');
  }
}

main() {
  var dataUrl = '/registrations/create';
  var data = {'dart': 'fun', 'editor': 'productive'};
  var encodedData = encodeMap(data);

  var httpRequest = new HttpRequest();
  httpRequest.open('POST', dataUrl);
  httpRequest.setRequestHeader('Content-type', 
                               'application/x-www-form-urlencoded');
  httpRequest.onLoadEnd.listen((e) => loadEnd(httpRequest));
  httpRequest.send(encodedData);
}

Sending and Receiving Real-Time Data with WebSockets

A WebSocket allows your web app to exchange data with a server interactively—no polling necessary. A server creates the WebSocket and listens for requests on a URL that starts with ws://—for example, ws://127.0.0.1:1337/ws. The data transmitted over a WebSocket can be a string, a blob, or an ArrayBuffer. Often, the data is a JSON-formatted string.

To use a WebSocket in your web app, first create a WebSocket object, passing the WebSocket URL as an argument:

lang-dart
var webSocket = new WebSocket('ws://127.0.0.1:1337/ws');

Sending data

To send string data on the WebSocket, use the send() method:

lang-dart
sendMessage(String data) {
  if (webSocket.readyState == WebSocket.OPEN) {
    webSocket.send(data);
  } else {
    throw 'WebSocket not connected, message $data not sent';
  }
}

Receiving data

To receive data on the WebSocket, register a listener for message events:

lang-dart
webSocket.onMessage.listen((MessageEvent e) {
  receivedMessage(e.data);
});

The message event handler receives a MessageEvent object. This object’s data field has the data from the server. Here’s an example of decoding a JSON string sent on a WebSocket, where the JSON string has two fields, “from” and “content”:

lang-dart
// Called from the message listener like this: receivedMessage(e.data)
receivedMessage(String data) {
  Map message = json.parse(data);
  if (message['from'] != null) {
    print('Message from ${message['from']}: ${message['content']}');
  }
}

Handling WebSocket events

WebSocketEvents defines the WebSocket events your app can handle: open, close, error, and (as shown above) message. Here’s an example of a method that creates a WebSocket object and handles message, open, close, and error events:

lang-dart
connectToWebSocket([int retrySeconds = 2]) {
  bool reconnectScheduled = false;
  webSocket = new WebSocket(url);
  
  scheduleReconnect() {
    print('web socket closed, retrying in $retrySeconds seconds');
    if (!reconnectScheduled) {
      new Timer(new Duration(seconds:retrySeconds),
        () => connectToWebSocket(retrySeconds*2));
    }
    reconnectScheduled = true;
  }
  
  webSocket.onOpen.listen((e) {
    print('Connected');
  });
  
  webSocket.onClose.listen((e) => scheduleReconnect());
  webSocket.onError.listen((e) => scheduleReconnect());
  
  webSocket.onMessage.listen((MessageEvent e) {
    _receivedEncodedMessage(e.data);
  });
}

For an example of using WebSockets, see Chapter 5, Walkthrough: Dart Chat.

More information

This section barely scratched the surface of using the dart:html library. For more information, see the documentation for dart:html. Dart has additional libraries for more specialized web APIs, such as web audio. Finally, don’t miss the Web UI package.

dart:isolate - Concurrency with Isolates

Dart has no shared-memory threads. Instead, all Dart code runs in isolates, which communicate via message passing. Messages are copied before they are received, ensuring that no two isolates can manipulate the same object instance. Because state is managed by individual isolates, no locks or mutexes are needed, greatly simplifying concurrent programming.

Isolate Concepts

To use isolates, you should understand the following concepts:

  • No two isolates ever share the same thread at the same time. Within an isolate, callbacks execute one at a time, making the code more predictable.

  • All values in memory, including globals, are available only to their isolate. No isolate can see or manipulate values owned by another isolate.

  • The only way isolates can communicate with each other is by passing messages.

  • Isolates send messages using SendPorts, and receive them using ReceivePorts.

  • The content of a message can be any of the following:

    • A primitive value (null, num, bool, double, String)

    • An instance of SendPort

    • A list or map whose elements are any of the above, including other lists and maps

    • In special circumstances, an object of any type

  • Each isolate has a ReceivePort, which is available as the port variable. Because all Dart code runs inside an isolate, even main() has access to a port object.

  • When a web application is compiled to JavaScript, its isolates can be implemented as Web workers. When running in Dartium, isolates run in the VM.

  • In the standalone VM, the main() function runs in the first isolate (also known as the root isolate). When the root isolate terminates, it terminates the whole VM, regardless of whether other isolates are still running. For more information, see the section called “Keeping the root isolate alive”.

Using Isolates

To use an isolate, you import the dart:isolate library, spawn a new isolate, and then send and receive messages.

Spawning isolates

Any top-level function or static method[2] is a valid entry point for an isolate. The entry point should not expect arguments and should return void. It is illegal to use a function closure as an entry point to an isolate. Pass the entry point to spawnFunction().

lang-dart
import 'dart:isolate';

runInIsolate() {
  print('hello from an isolate!');
}

main() {
  spawnFunction(runInIsolate);

  // Note: incomplete.
  // Use a ReceivePort (details below) to keep the root isolate alive
  // long enough for runInIsolate() to perform its work.
}

We plan to support spawning an isolate from code at a URI.

Sending messages

Send a message to an isolate via a SendPort. The spawnFunction() method returns a handle to the newly created isolate’s SendPort.

To simply send a message, use send():

lang-dart
import 'dart:isolate';

echo() {
  // Receive messages here. (See the next section.)
}

main() {
  var sendPort = spawnFunction(echo);
  sendPort.send('Hello from main');

  // Note: incomplete.
  // Use a ReceivePort (details below) to keep the root isolate alive
  // long enough for echo() to perform its work.
}

Sending any type of object

In special circumstances (such as when using spawnFunction() inside the Dart VM), it is possible to send any type of object to an isolate.[3] The object message is copied when sent.

Receiving messages

Use a ReceivePort to receive messages sent to an isolate. Obtain a handle to the default ReceivePort from the top-level port property. You can also create new instances of ReceivePort, if you want to route messages to different ports and callbacks.

Handle an incoming message with a callback function passed to the receive() method:

lang-dart
import 'dart:isolate';
echo() {
  port.receive((msg, reply) {
    print('I received: $msg');
  });
}

main() {
  var sendPort = spawnFunction(echo);
  sendPort.send('Hello from main');

  // Note: incomplete.
  // Use a ReceivePort (details below) to keep the root isolate alive
  // long enough for echo() to perform its work.
}

Receiving replies

Use the call() method on SendPort as a simple way to send a message and receive a reply. The call() method returns a Future for the reply.

lang-dart
import 'dart:isolate';

echo() {
  port.receive((msg, reply) {
    reply.send('I received: $msg');
  });
}

main() {
  var sendPort = spawnFunction(echo);
  sendPort.call('Hello from main').then((reply) {
    print(reply);    // I received: Hello from main
  });
}

Under the covers, the call() method creates and manages a SendPort and a ReceivePort, which are necessary for a call-and-response message exchange.

Keeping the root isolate alive

In the VM, an isolate continues to run as long as it has an open ReceivePort inside the isolate. If the main() function only starts other isolates, doing no work itself, you must keep the root isolate alive to keep the program alive.

To keep a root isolate alive, open a ReceivePort in the root isolate. When all the child isolates have finished their work, you can send a message to the root isolate to close its ReceivePort, thus stopping the program.

You can coordinate isolates with message passing, sending a message to inform the root isolate when a child isolate finishes. Here is an example:

lang-dart
import 'dart:isolate';

childIsolate() {
  port.receive((msg, replyTo) {
    print('doing some work');
    if (replyTo != null) replyTo.send('shutdown');
  });
}

main() {
  var sender = spawnFunction(childIsolate);
  var receiver = new ReceivePort();
  receiver.receive((msg, _) {
    if (msg == 'shutdown') {
      print('shutting down');
      receiver.close();
    }
  });
  sender.send('do work please', receiver.toSendPort());
}

In the above example, the child isolate runs to completion because the root isolate keeps a ReceivePort open. The root isolate creates a ReceivePort to wait for a shutdown message. The term shutdown is arbitrary; the ReceivePort simply needs to wait for some signal.

Once the root isolate receives a shutdown message, it closes the ReceivePort. With the ReceivePort closed and nothing else to do, the root isolate terminates, causing the app to exit.

More Information

See the API docs for the dart:isolate library, as well as for SendPort and ReceivePort.

dart:io - I/O for Command-Line Apps

The dart:io library provides APIs to deal with files, directories, processes, sockets, and HTTP connections. Only command-line apps can use dart:io—not web apps.

In general, the dart:io library implements and promotes an asynchronous API. Synchronous methods can easily block the event loop, making it difficult to scale server applications. Therefore, most operations return results via callbacks or Future objects, a pattern common with modern server platforms such as Node.js.

The few synchronous methods in the dart:io library are clearly marked with a Sync suffix on the method name. We don’t cover them here.

Note

Only command-line apps can import and use dart:io.

Files and Directories

The I/O library enables command-line apps to read and write files and browse directories. You have two choices for reading the contents of a file: all at once, or streaming. Reading a file all at once requires enough memory to store all the contents of the file. If the file is very large or you want to process it while reading it, you should use an Stream, as described in the section called “Streaming file contents”.

Reading a file as text

When reading a text file, you can read the entire file contents with readAsString(). When the individual lines are important, you can use readAsLines(). In both cases, a Future object is returned that provides the contents of the file as one or more strings.

lang-dart
ch03/textRead.dart
import 'dart:io';

main() {
  var config = new File('config.txt');

  // Put the whole file in a single string.
  config.readAsString().then((String contents) {
    print('The entire file is ${contents.length} characters long');
  });

  // Put each line of the file into its own string.
  config.readAsLines().then((List<String> lines) {
    print('The entire file is ${lines.length} lines long');
  });
}

Reading a file as binary

The following code reads an entire file as bytes into a list of ints. The call to readAsBytes() returns a Future, which provides the result when it’s available.

lang-dart
ch03/binaryRead.dart
import 'dart:io';

main() {
  var config = new File('config.txt');

  config.readAsBytes().then((List<int> contents) {
    print('The entire file is ${contents.length} bytes long');
  });
}

Handling errors

Errors are thrown as exceptions if you do not register an explicit handler. If you want to capture an error, you can register a catchError handler with the Future object:

lang-dart
ch03/fileErrors.dart
import 'dart:io';

main() {
  var config = new File('config.txt');
  config.readAsString().then((String contents) {
    print(contents);
  }).catchError((e) {
    print(e);
  });
}

Streaming file contents

Use a Stream to read a file, a little at a time. The listen() method specifies a handler to be called when data is available. When the Stream is finished reading the file, the onDone callback executes.

lang-dart
ch03/readFile.dart
import 'dart:io';
import 'dart:async';

main() {
  var config = new File('config.txt');
  Stream<List<int>> inputStream = config.openRead();

  inputStream
    .transform(new StringDecoder())
    .transform(new LineTransformer())
    .listen(
      (String line) { 
        print('Read ${line.length} bytes from stream');
      },
      onDone: () { print('file is now closed'); },
      onError: (e) { print(e.toString()); });
}

To decode from bytes into characters, wrap the stream with a StringDecoder. You can read the strings either as data becomes available or one line at a time.

Writing file contents

You can use an IOSink to write data to a file. Use the File openWrite() method to declare a mode and get an IOSink that you can write to. The default mode, FileMode.WRITE, completely overwrites existing data in the file.

lang-dart
ch03/writeFile.dart
var logFile = new File('log.txt');
var sink = logFile.openWrite();
sink.write('FILE ACCESSED ${new DateTime.now()}\n');
sink.close();

To add to the end of the file, use the optional mode parameter to specify FileMode.APPEND:

lang-dart
ch03/writeFile.dart
var sink = logFile.openWrite(mode: FileMode.APPEND); 

To write binary data, use add(List<int> buffer).

Listing files in a directory

Finding all files and subdirectories for a directory is an asynchronous operation. The list() method returns a Stream on which you can register handlers (using listen()) to be notified when a file or directory is encountered.

lang-dart
ch03/listFiles.dart
import 'dart:io';
import 'dart:async';

main() {
  var dir = new Directory('/tmp');
  var contentsStream = dir.list(recursive:true);
  contentsStream.listen(
    (FileSystemEntity f) {
      if (f is File) {
        print('Found file ${f.path}');
      } else if (f is Directory) {
        print('Found dir ${f.path}');
      }
    },
    onError: (e) { print(e.toString()); }
  );
}

Other common functionality

The File and Directory classes contain other functionality, including but not limited to:

  • Creating a file or directory: create() in File and Directory

  • Deleting a file or directory: delete() in File and Directory

  • Getting the length of a file: length() in File

  • Getting random access to a file: open() in File

Refer to the API docs for File and Directory for a full list of methods.

Besides the APIs discussed in this section, the dart:io library also provides APIs for processes, sockets, and web sockets.

For more examples of using dart:io, see Chapter 5, Walkthrough: Dart Chat.

HTTP Clients and Servers

The dart:io library provides classes that command-line apps can use for accessing HTTP resources, as well as running HTTP servers.

HTTP server

The HttpServer class provides the low-level functionality for building web servers. You can match request handlers, set headers, stream data, and more.

The following sample web server can return only simple text information. This server listens on port 8888 and address 127.0.0.1 (localhost), responding to requests for the path /languages/dart. All other requests are handled by the default request handler, which returns a response code of 404 (not found).

lang-dart
ch03/httpServer.dart
import 'dart:io';

main() {
  dartHandler(HttpRequest request) {
    print('New request');
    request.response.write('Dart is optionally typed');
    request.response.close();
  };

  HttpServer.bind('127.0.0.1', 8888).then((HttpServer server) {
    server.listen((request) { 
      print("got request");
      if (request.uri.path == '/languages/dart') {
        dartHandler(request);
      } else {
        request.response.write('Not found');
        request.response.statusCode = HttpStatus.NOT_FOUND;
        request.response.close();
      }
    });
  });
}

You can see a more comprehensive HTTP server in the section called “The Server’s Code”.

HTTP client

The HttpClient class helps you connect to HTTP resources from your Dart command-line or server-side application. You can set headers, use HTTP methods, and read and write data. The HttpClient class does not work in browser-based apps. When programming in the browser, use the HttpRequest class. Here’s an example of using HttpClient:

lang-dart
ch03/httpClient.dart
import 'dart:io';
import 'dart:uri';

main() {
  var url = new Uri('http://127.0.0.1:8888/languages/dart');
  var httpClient = new HttpClient();
  httpClient.getUrl(url)
    .then((HttpClientRequest request) {
      print('have request');
      return request.close();
    })
    .then((HttpClientResponse response) {
      print('have response');
      response.transform(new StringDecoder()).toList().then((data) {
        var body = data.join('');
        print(body);
        httpClient.close();
      });
    });
}

dart:json - Encoding and Decoding Objects

JSON is a simple text format for representing structured objects and collections. The JSON library decodes JSON-formatted strings into Dart objects, and encodes objects into JSON-formatted strings.

The Dart JSON library works in both web apps and command-line apps. To use the JSON library, import dart:json.

Decoding JSON

Decode a JSON-encoded string into a Dart object with json.parse():

lang-dart
import 'dart:json' as json;

main() {
  // NOTE: Be sure to use double quotes ("), not single quotes ('),
  // inside the JSON string. This string is JSON, not Dart.
  var jsonString = '''
  [
    {"score": 40},
    {"score": 80}
  ]
  ''';

  var scores = json.parse(jsonString);
  assert(scores is List);

  var firstScore = scores[0];
  assert(firstScore is Map);
  assert(firstScore['score'] == 40);
}

Encoding JSON

Encode a supported Dart object into a JSON-formatted string with json.stringify().

Only objects of type int, double, String, bool, null, List, or Map can be encoded into JSON. List and Map objects are encoded recursively.

If any object that isn’t an int, double, String, bool, null, List, or Map is passed to stringify(), the object’s toJson() method is called. If toJson() returns an encodable value, that value is encoded in the object’s place.

lang-dart
import 'dart:json' as json;

main() {
  var scores = [
    {'score': 40},
    {'score': 80},
    {'score': 100, 'overtime': true, 'special_guest': null}
  ];

  var jsonText = json.stringify(scores);
  assert(jsonText == '[{"score":40},{"score":80},'
                     '{"score":100,"overtime":true,'
                     '"special_guest":null}]');
}

dart:uri - Manipulating URIs

The URI library provides functions to encode and decode strings for use in URIs (which you might know as URLs). These functions handle characters that are special for URIs, such as & and =.

Another part of the URI library is the Uri class, which parses and exposes the components of a URI—domain, port, scheme, and so on.

The URI library works in both web apps and command-line apps. To use it, import dart:uri.

Encoding and Decoding Fully Qualified URIs

To encode and decode characters except those with special meaning in a URI (such as /, :, &, #), use the top-level encodeUri() and decodeUri() functions. These functions are good for encoding or decoding a fully qualified URI, leaving intact special URI characters.

lang-dart
import 'dart:uri';

main() {
  var uri = 'http://example.org/api?foo=some message';
  var encoded = encodeUri(uri);
  assert(encoded == 'http://example.org/api?foo=some%20message');

  var decoded = decodeUri(encoded);
  assert(uri == decoded);
}

Notice how only the space between some and message was encoded.

Encoding and Decoding URI Components

To encode and decode all of a string’s characters that have special meaning in a URI, including (but not limited to) /, &, and :, use the top-level encodeUriComponent() and decodeUriComponent() functions.

lang-dart
import 'dart:uri';

main() {
  var uri = 'http://example.org/api?foo=some message';
  var encoded = encodeUriComponent(uri);
  assert(encoded == 'http%3A%2F%2Fexample.org%2Fapi%3Ffoo%3Dsome%20message');

  var decoded = decodeUriComponent(encoded);
  assert(uri == decoded);
}

Notice how every special character is encoded. For example, / is encoded to %2F.

Parsing URIs

You can parse a URI into its parts with the Uri() constructor:

lang-dart
import 'dart:uri';

main() {
  var uri = new Uri('http://example.org:8080/foo/bar#frag');

  assert(uri.scheme == 'http');
  assert(uri.domain == 'example.org');
  assert(uri.path == '/foo/bar');
  assert(uri.fragment == 'frag');
  assert(uri.origin == 'http://example.org:8080');
}

See the Uri API docs for more URI components that you can get.

Building URIs

You can build up a URI from individual parts using the Uri.fromComponents() constructor:

lang-dart
import 'dart:uri';

main() {
  var uri = new Uri.fromComponents(scheme: 'http', domain: 'example.org', 
                     path: '/foo/bar', fragment: 'frag');
  assert(uri.toString() == 'http://example.org/foo/bar#frag');
}

dart:utf - Strings and Unicode

The UTF library helps bridge the gap between strings and UTF-8/UTF-16/UTF-32 encodings.

This library works in both web apps and command-line apps. To use it, import dart:utf.

Decoding UTF-8 Characters

Use decodeUtf8() to decode UTF8-encoded bytes to a Dart string:

lang-dart
import 'dart:utf';

main() {
  var string = decodeUtf8([0xc3, 0x8e, 0xc3, 0xb1, 0xc5, 0xa3, 0xc3, 0xa9,
                           0x72, 0xc3, 0xb1, 0xc3, 0xa5, 0xc5, 0xa3, 0xc3,
                           0xae, 0xc3, 0xb6, 0xc3, 0xb1, 0xc3, 0xa5, 0xc4,
                           0xbc, 0xc3, 0xae, 0xc5, 0xbe, 0xc3, 0xa5, 0xc5,
                           0xa3, 0xc3, 0xae, 0xe1, 0xbb, 0x9d, 0xc3, 0xb1]);
  print(string); // 'Îñţérñåţîöñåļîžåţîờñ'
}

Encoding Strings to UTF-8 Bytes

Use encodeUtf8() to encode a Dart string as a list of UTF8-encoded bytes:

lang-dart
import 'dart:utf';

main() {
  List<int> expected = [0xc3, 0x8e, 0xc3, 0xb1, 0xc5, 0xa3, 0xc3, 0xa9, 0x72,
                        0xc3, 0xb1, 0xc3, 0xa5, 0xc5, 0xa3, 0xc3, 0xae, 0xc3,
                        0xb6, 0xc3, 0xb1, 0xc3, 0xa5, 0xc4, 0xbc, 0xc3, 0xae,
                        0xc5, 0xbe, 0xc3, 0xa5, 0xc5, 0xa3, 0xc3, 0xae, 0xe1,
                        0xbb, 0x9d, 0xc3, 0xb1];

  List<int> encoded = encodeUtf8('Îñţérñåţîöñåļîžåţîờñ');

  assert(() {
    if (encoded.length != expected.length) return false;
    for (int i = 0; i < encoded.length; i++) {
      if (encoded[i] != expected[i]) return false;
    }
    return true;
  });
}

Other Functionality

The UTF library can decode and encode UTF-16 and UTF-32 bytes. The library can also convert directly to and from Unicode code points and UTF8-encoded bytes. For details, see the API docs for the UTF library.

dart:crypto - Hash Codes and More

The Dart crypto library contains functions useful for cryptographic applications, such as creating cryptographic hashes and generating hash-based message authentication codes.

The crypto library works in both web apps and command-line apps. To use the crypto library, import dart:crypto.

Generating Cryptographic Hashes

With the crypto library, you can use SHA256, SHA1, or MD5 objects to generate hashes (also known as digests or message digests). We recommend using SHA256, but we have included SHA1 and MD5 for compatibility with older systems. All these types implement Hash, which defines the Dart interface for cryptographic hash functions.

lang-dart
ch03/sha256.dart
import 'dart:crypto';

main() {
  var sha256 = new SHA256();
  sha256.add('message'.codeUnits);
  var hexString = CryptoUtils.bytesToHex(sha256.close());
  assert(hexString ==
      'ab530a13e45914982b79f9b7e3fba994cfd1f3fb22f71cea1afbf02b460c6d1d');
}

Generating Message Authentication Codes

Use a hash-based message authentication code (HMAC) to combine a cryptographic hash function with a secret key:

lang-dart
ch03/hmac.dart
import 'dart:crypto';
main() {
  var hmac = new HMAC(new SHA256(), 'secretkey'.codeUnits);
  hmac.add('message'.codeUnits);
  var hmacHex = CryptoUtils.bytesToHex(hmac.close());
  assert(hmacHex ==
      '5c3e2f56de9411068f675ef32ffa12735210b9cbfee2ba521367a3955334a343');
}

Generating Base64 Strings

You can represent binary data as a character string by using the Base64 encoding scheme. Use the CryptoUtils.bytesToBase64() utility method to convert a list of bytes into a Base64-encoded string:

lang-dart
import 'dart:crypto';
import 'dart:io';

main() {
  var file = new File('icon.ico');
  var bytes = file.readAsBytesSync();
  var base64 = CryptoUtils.bytesToBase64(bytes);
  assert(base64 ==
    'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38G'
    'IAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==');
}

Summary

This chapter introduced you to the most commonly used functionality in many of Dart’s built-in libraries. It didn’t cover all the built-in libraries, however. Other libraries you might want to look into include dart:collection, dart:mirrors, logging, mock, and unittest. You can use the pub tool, discussed in the next chapter, to install additional Dart libraries.



[2] The dart2js compiler and the Dart VM do not yet support static methods as isolate entry points. For details, see http://dartbug.com/3011.

[3] Support for sending an arbitrary object to an isolate is not yet available when compiling to JavaScript.