Dart

Overview

Dart logo

Dart is a general purpose, strongly typed, programming language from Google. It has a "sound type system" which means it can never be in an unknown state. Dart was first announced in October, 2012.

The original goal of Dart was to be an alternative to JavaScript for running code in web browsers. The Chrome browser planned to include a Dart VM for this purpose, but that plan has been abandoned in favor of compiling to JavaScript.

Currently Dart is primarily used by the Flutter framework for building mobile, web, and desktop applications.

The syntax of Dart is somewhat similar to Java. Statements are terminated by semi-colons. Parentheses surround conditions. Curly braces surround blocks of code. Indentation is typically two spaces.

Dart supports operator overloading to define the meaning of operators when applied to instances of custom classes.

Resources

Editors

Many code editors can be used for writing Dart programs. Popular options include VS Code, Intellij IDEA, and Android Studio. DartPad is an online editor for experimenting with Dart features.

Recommended VS Code extensions for Dart include:

pub

The official Dart package manager is pub. This is similar to npm for Node.js. In the same way that npm relies on the files package.json and package-lock.json to manage dependencies, pub relies on the files pubspec.yaml and pubspec.lock (also a YAML file).

pub.dev

pub.dev is the official package repository for Dart and Flutter apps. Packages here are listed in five catagories: "Flutter Favorites", "Most popular packages", "Top Flutter packages", "Top Dart packages", and "Package of the Week".

There are two kinds of packages. "Library packages" are used as dependencies of other packages. "Application packages" are meant to be run and can depend on library packages.

Some notable packages to consider using include:

Creating and Running Programs

Dart source files have a .dart file extension. It is recommended that their names be all lowercase with multiple words separated by underscores.

The main function defines the starting point of a program. It is passed a List of command-line arguments. The type can be omitted, specified as just List, or specified as List<String>.

main(args) {
args.forEach((arg) => print('arg = $arg'));
}

To run a .dart file that defines a main function, enter dart {name}.dart [arguments].

It is not necessary to create a Dart "project" in order to write and run Dart code, but doing so provides many benefits. It gives a project a standard directory structure, makes it easy to add dependencies, enables writing tests, makes it easy to publish packages to pub.dev, and much more.

To create a new Dart project, enter dart create {project-name}. Dart prefers underscores over hyphens in both project names and source file names. For example:

dart create hello_world

To run a Dart project, cd to the project directory and enter dart run. For example:

cd hello_world`
dart run # outputs "Hello world!"

To analyze a Dart project for syntax errors and lint rule violations, enter dart analyze.

The dart create command creates the following files and directories:

A Dart project is actually a Dart package. This means it is possible for it to be deployed to pub.dev and its public API can be shared with other Dart packages.

Dart SDK

The Dart SDK includes all the tools needed to create, build, and run Dart applications. It can be downloaded from get-dart which provides instructions for installing in Windows, Linux, and macOS.

The Flutter SDK includes the Dart SDK.

After installing the Dart SDK, enter "dart" in a terminal to see all the subcommand options.

To create a new Dart project, enter dart create -t {type} {project-name} where type is console-simple, console-full, package-simple, server-shelf, or web-simple.

The Dart SDK contains three compilers.

  1. Just In Time (JIT) compiler
    This compiles Dart code to an intermediate format and runs it in a virtual machine, which is ideal during development.

  2. Ahead Of Time (AOT) compiler
    This compiles and builds an executable for a Dart program, which is ideal for releasing a finished application. It performs type flow analysis and removes unused code in order to produce a smaller executable. It also performs optimizations such as inlining code in order to produce a faster executable.

  3. JavaScript (JS) compiler
    This compiles a Dart program to JavaScript, which allows it to be run in a web browser. The ability to do this is an important goal of the Dart language and influenced many of its syntax decisions. Some Dart libraries only run on the server or in Flutter and cannot be compiled to JavaScript.

Keywords

For a list of keywords in the Dart language with links to their descriptions, see Language Tour - Keywords.

Comments

Comments in Dart are written like in many other programming languages.

Variables

There are four kinds of variables in Dart.

  1. top-level, global variables declared outside any function or class
  2. local variables in a function or method of a class, including parameters
  3. static properties in a class
  4. instance properties in a class

There are three ways to declare variables.

  1. With a type

    For example, int score;

  2. With the var keyword

    The type is obtained through type inference. For example, these are equivalent:

    int n = 19;
    var n = 19;

    When a var declaration is not initialized, it is treated like dynamic which is described next.

  3. With the dynamic keyword

    The type can change throughout the lifetime of the variable based on the value currently assigned. For example:

    dynamic n = 19;
    n = 3.14;
    n = "changed";

By default variables do not have a default value and cannot be set to null. To allow setting a variable to null its type must be followed by ?. When this is done, the variable is initialized to null by default.

Non-nullable top-level variables (not declared inside a function or class) and non-nullable static class properties must be initialized. Non-nullable local variables in functions do not need to be given a value until their first use.

The late keyword has two primary uses.

First, it can be applied to a variable declaration with a non-null type that is not yet initialized. This states that the variable will be initialized before its first use. This allows a nested function to refer to the variable as long as the function is not called until after the variable is initialized. See an example of this in the "Streams" section.

In a Flutter StatefulWidget subclass, this typically happens in the initState method.

If a late variable is used before it is initialized, a LateInitializationError is thrown.

Second, it causes a value used for initialization to be lazily evaluated, regardless of whether it is a function call, method call, or computed property. The means the initialization function is not called until the first time the property is accessed. The following code demonstrates this:

int myFunction() {
print('in myFunction');
return 2;
}

class Demo {
// Instance properties like computedProperty cannot be used to initialize
// another instance property unless the "late" keyword is used.
late int a = computedProperty;

late int b = myFunction();

// Instance methods like myMethod cannot be used to initialize
// another instance property unless the "late" keyword is used.
late int c = myMethod();

int get computedProperty {
print('in computedProperty');
return 1;
}

int myMethod() {
print('in myMethod');
return 3;
}
}

void main() {
var demo = Demo();
print('created object');
print(demo.a); // triggers initialization of the "a" property
print('retrieved a');
print(demo.b); // triggers initialization of the "b" property
print('retrieved b');
print(demo.c); // triggers initialization of the "c" property
print('retrieved c');
}

The code above produces the following output:

created object
in computedProperty
1
retrieved a
in myFunction
2
retrieved b
in myMethod
3
retrieved c

To get the runtime type of any object, access its runtimeType property. This has a type of Type which has a toString method.

Immutability with const and final

Variables whose values are known at compile-time should be declared with the keyword const. Class properties whose values are known at compile-time should be declared with the keywords static const. This prevents assigning a new value, and also prevents modifying the value (for example, adding elements to a List).

The const keyword can be applied to top-level variables, local variables in functions, and class static properties. It cannot be applied to class instance properties.

Variables and class properties whose values should be set once at run-time and never changed should be declared with the keyword final. This prevents assigning a new value, but does not prevent modifying the value.

In general, final is used in Dart everywhere const would be used in JavaScript and var is used in Dart everywhere let would be used in JavaScript.

The meaning of the const and final keywords depends on whether they are applied to a variable or its initial value. There are two aspects to consider, whether a new value can be assigned to the variable and whether its value can be modified (ex. adding elements to a list).

The following code demonstrates using the const and final keywords in conjunction with declaring and creating List collection objects. The same principles apply to other kinds of collections.

List<int> l1 = [1, 2];
//var l1 = <int>[1, 2]; // alternate way to write previous line
l1.add(3); // can modify current value
l1 = [3, 4]; // can assign a new value
print(l1); // [3, 4]

// const here makes the variable AND its value immutable.
// Adding const after "=" and before the value is redundant.
const List<int> l2 = [1, 2];
//l2.add(3); // throws UnsupportedError at runtime
//l2 = [3, 4]; // cannot assign a new value
print(l2); // [1, 2]

// Adding const only before the value makes the value immutable,
// but a new value can be assigned to the variable.
List<int> l3 = const [1, 2];
//l3.add(3); // throws UnsupportedError at runtime
l3 = [3, 4]; // can assign a new value
print(l3); // [3, 4]

// final here means the variable can only be assigned a value once,
// but its value can be modified.
final List<int> l4 = [1, 2];
l4.add(3); // can modify current value
//l4 = [3, 4]; // cannot assign a new value
print(l4); // [1, 2, 3]

// This is the same as the l2 declaration
// which is preferred because it is shorter.
final List<int> l5 = const [1, 2];
//l5.add(3); // throws UnsupportedError at runtime
//l5 = [3, 4]; // cannot assign a new value
print(l5); // [1, 2]

In order to create instances of a class that are compile-time constants, all fields must be final and the constructor must be marked const. When this is done, constructor calls with same argument values return references to the same object. The following code demonstrates this:

class Point {
final double x;
final double y;

const Point(this.x, this.y);
}

main() {
var p1 = const Point(1, 2); // compile-time constant
const p2 = Point(1, 2); // alternate way to create a compile-time constant
var p3 = Point(1, 2); // not a constant
print(p1 == p2); // true - same object
print(identical(p1, p2)); // true - same object
print(identical(p1, p3)); // false - not same object
}

The identical function tests whether two values refer to the same object. By default the == operator performs the same test, but classes can override this to have a different meaning such as comparing object properties. The collection classes List, Set, and Map do not override the == operator.

To test whether two objects contain the same properties and values, consider using the equatable library. Custom classes can extend Equatable to override the == operator and the hashCode method so instances are compared by value instead of by reference. For classes that already have a superclass, use EquatableMixin.

Importing packages

A .dart file can import packages using the import statement. The following imports the dart:math package and places all its top-level public names (such as pi and sin) in the current namespace:

import 'dart:math';

To import only a subset of the names defined in another file, use the show keyword.

import 'dart:math' show pi, sqrt;

To import all names defined in another file except some, use the hide keyword.

import 'dart:math' hide acos, asin, atan, atan2;

To avoid conflicting with names already in the current namespace, use the as keyword.

import 'dart:math' as math;

Now names like pi can be referred to with math.pi and functions like sqrt can be referred to with math.sqrt.

Types

All values in Dart are objects from some class. This is even true for basic types like bool, int, double, and String. For example, the literal values true, 7, 3.14, and 'test' are all objects.

These and many other classes are defined in the dart:core package. Classes defined here do not need to be imported.

All types except Null are subclasses of the Object class.

Dart supports the following built-in basic types:

The void type is used at the return type of functions that do not return anything.

The keyword null refers to the only instance of the Null type.

The Never type is the type of throw expressions, and is the return type of functions that always throw.

Note that there is no float type. The double type is used for all floating point numbers.

Dart supports the following built-in collection types:

Dart supports type inference, so types do not need to specified if they can be inferred from values.

By default no type allows the value null. To allow this prepend ? to the type name. For example, a variable of type String cannot be set to null, but a variable of type String? can. The compiler requires handling cases where a nullable value might be null. This results in detecting errors involving null values at compile-time rather than runtime.

The is operator tests whether a variable currently holds a value of a given type and evaluates to a bool. For example:

void evaluate(num n) {
if (n is int) {
// n is cast to int inside this block.
print('$n is ${n.isOdd ? 'odd' : 'even'}.');
}
}

void main() {
num n = 3.1;
evaluate(n); // outputs nothing
n = 7;
evaluate(n); // outputs "7 is odd."
}

The is! operator performs the opposite test.

bool type

Instances of the bool class represent a boolean value. The only values are the literal values true and false.

The bool class doesn't add any interesting properties or methods, but it defines the following bitwise operators: & (and), | (or), and ^ (xor).

In addition, the logical operators && (and), || (or), and ! (not) can be applied to bool values. These have lower precedence than bitwise operators. TODO: Where are these defined? Why aren't they defined by the bool class?

Number types

The int and double classes are the only number types supported by Dart. Both inherit from the num class and both represent 64-bit values. Custom classes are not allowed to inherit from the num class.

num Class

Variables declared with the num type can be assigned both int and double values.

The num class defines the following properties:

PropertyDescription
isFinitebool indicating whether the number is finite
isInfinitebool indicating whether the number is infinite
inNaNbool indicating whether this is the Not-a-Number value
isNegativebool indicating whether the number is negative
sign-1, 0, or 1

The num class defines the following instance methods:

MethodDescription
abs()returns absolute value
ceil()returns least integer not smaller
ceilToDouble()same as ceil, but returns a double
clamp(num lower, num upper)returns n < lower ? lower : n > upper ? upper : n
compareTo(num other)returns comparator int value
floor()returns greatest integer not greater
floorToDouble()same as floor, but returns a double
remainder()returns remainder of truncating division (modulo)
round()returns closest integer
roundToDouble()same as round, but returns a double
toDouble()returns number as a double
toInt()returns number as an int
toString()returns number as a String
toStringAsExponential(int? fractionDigits)returns String representation in exponential form
toStringAsFixed(int fractionDigits)returns String representation with fixed number of fraction digits
toStringAsPrecision(int precision)returns String representation with precision significant digits
truncate()returns int result of truncating fractional digits
truncateToDouble()same as truncate, but returns a double

The num class defines the following static methods:

MethodDescription
parse(String input, [num onError(String input)?])returns num obtained by parsing a String
tryParse(String input)same as parse, but returns null if parsing fails

The parse method throws if parsing fails or calls onError if supplied.

The num class defines the following operators:

CategoryOperators
unary arithmetic-, ++ (increment by 1), -- (decrement by 1)
binary arithmetic+, -, *, /, ~/ (truncating division), % (modulo)
relational<, <=, ==, !=, >, >=
shorthand arithmetic assignment+=, -=, *=, /=, ~/= (truncating division), %= (modulo)

The ++ and -- operators can be placed on either side of a num value. When on the left, a new value is computed before it is used. When on the right, the value is used before a new value is computed.

Note that there is no ^ or ** for exponentiation like in many other programming languages. Instead, import the dart:math package and use the pow(number, exponent) function.

The fact that these operators take operands of type num means that they can be applied to mixed operands where one is an int and the other is a double.

Even though operator definitions look like methods, they cannot be called like methods. For example, the documentation for + operator in the num class shows num operator +(num other);.

var n = 2;
n.+(3); // This does not work!
n += 3; // This works.
print(n);

Dividing by zero with the / operator results in the double.infinity constant rather than throwing an exception. When the integer division operator ~/ is used instead, an UnsupportedError is thrown. Note that IntegerDivisionByZeroException is deprecated.

int Class

The int class adds the following properties to those defined in the num class:

PropertyDescription
bitLengthminimum number of bits required to store
isEvenbool indicating if even
isOddbool indicating if odd

The int class adds the following methods to those defined in the num class (some omitted):

MethodDescription
gcd(int other)returns greatest common divisor
toRadixString(int radix)returns String representation of number with a given base
toSigned(int width)returns least significant bits, retaining sign bit
toUnsigned(int width)returns least significant bits, not retaining sign bit

The toRadixString method can be used to convert decimal values to hexadecimal. For example, 255.toRadixString(16) returns the String ff.

The int class adds the following operators to those defined in the num class:

CategoryOperators
bitwise& (and), |(or), ^(xor), ~ (complement)
bit shift<< (signed shift left), >> (signed shift right), and >>> (unsigned shift right)
shorthand bitwise assignment&= (and), |=(or),^= (xor)
shorthand bit shift assignment<<=, >>-, and >>>==

Note that there is no operator for unsigned bit shift left.

double Class

The double class adds no properties, instance methods, static methods, or operators beyond those defined in the num class.

The double class defines the following constants that must be prefixed with double.:

ConstantDescription
infinityrepresents an infinite positive number
maxFinitelargest positive floating point number in 64 bits
minPositivesmallest positive floating point number in 64 bits
nanrepresents a value that is not a valid number
negativeInfinityrepresents an infinite negative number

math Package

The Dart math package does not need to be installed, but it must be imported in order to use the items it defines.

import 'dart:math';

The Dart math package class defines the following classes:

ClassDescription
MutableRectangle<T extends num>represents a mutable rectangle that is axis-aligned (not rotated)
Point<T extends num>represents an immutable 2D point
Randomprovides methods that generate random bool, int, and double values
Rectangle<T extends num>represents an immutable rectangle that is axis-aligned (not rotated)

The Dart math package class defines the following constants:

ConstantDescription
ebase of natural logarithms; 2.718...
ln2natural log of 2; 0.693...
ln10natural log of 10; 2.30...
log2ebase 2 log of e; 1.44...
log10ebase 10 log of e; 0.434...
piPI; 3.14...
sqrt1_2square root of 1/2; 0.707...
sqrt2square root of 2; 1.41...

The Dart math package defines the following functions:

FunctionDescription
acos(num x)returns arc cosine of x
asin(num x)returns arc sine of x
atan(num x)returns arc tangent of x
atan2(num x, num y)returns arc tangent of y/x
cos(num radians)returns cosine of radians
exp(num x)returns e to the power x
log(num x)returns log base e of x
max<T extends num>(num T, num T)returns larger of two numbers
min<T extends num>(num T, num T)returns smaller of two numbers
pow(num x, num exponent)returns x to the power exponent
sin(num radians)returns sine of radians
sqrt(num x)returns square root of x
tan(num radians)returns tangent of radians

String Class

Instances of the String class hold an immutable sequence of UTF-16 characters.

TODO: Why does the name of this type start uppercase, TODO: but the bool, int, and double types start lowercase?

Literal single line strings are delimited by single or double quotes. Literal multi-line strings are delimited by a pair of three single or double quotes. All newlines, except a leading newline if it exists, are retained.

Literal strings can use interpolations. To include the value of a variable, include $variableName. To include the value of an expression, include ${expression}. To include a literal $ in a string, escape it with \$.

Strings can be concatenated with the + operator. The + operator is not needed to concatenate literal strings. Literal strings can be written next to each other, separated only by a space, to concatenate them.

An individual character (code point) can be accessed with square brackets and an index. For example, the first character of a string s is obtained with s[0].

There are two ways to embed newline characters in a string. The following code demonstrates these:

  var s1 = 'first\nsecond';
print(s1);
var s2 = '''first
second'''
;
print(s2);

Unicode character codes can be embedded in a String with \u{hex-code}. For example:

print('Do you prefer a \u{1F436} or \u{1F431}?'); // Do you prefer a 🐶 or 🐱?

Raw strings are strings where backslash character is not treated specially. They are indicated by preceding the opening delimiter with "r". For example, r'first\nsecond' does not treat \n as a newline character. Raw strings useful when defining regular expressions.

Defining custom classes that extend the String class is not allowed.

The String class defines the following properties:

PropertyDescription
codeUnitsa List<int> of the characters (code points)
isEmptybool indicating if empty
isNotEmptybool indicating if not empty
lengthnumber of characters (code points)
runesIterable over the characters (code points)

The String class defines the following instance methods:

MethodDescription
allMatches(String s, [int start = 0])returns Iterable<Match> over matching substrings
codeUnitAt(int index)returns code point at a given index; same as [index]
compareTo(String other)returns comparator value typically used for sorting
contains(Pattern other, [int start = 0])returns bool indicating if a pattern is contained
endsWith(String other)returns bool indicating if ends with another String
indexOf(Pattern other, [int start = 0])returns int index where a Pattern is first found
lastIndexOf(Pattern other, [int start = 0])returns int index where a Pattern is last found
matchAsPrefix(String other, [int start = 0])returns first Match or null of a String
padLeft(int width, [String padding = ' '])returns new String padded on left to a given width
padRight(int width, [String padding = ' '])returns new String padded on right to a given width
replaceAll(Pattern from, String replace)returns new String where all Pattern matches are replaced by a String
replaceAllMapped(Pattern from, String replace(Match match), [int start = 0])returns new String where all Pattern matches are replaced by a computed value
replaceFirst(Pattern from, String to, [int start = 0])returns new String where first Pattern match is replaced by a String
replaceFirstMapped(Pattern from, String replace(Match match), [int start = 0])returns new String where first Pattern match is replaced by a computed value
replaceRange(int start, int? end, String replacement)returns new String where a substring range is replaced by a String
split(Pattern pattern)returns a List<String> of substrings obtained by splitting on a Pattern
splitMapJoin(Pattern pattern, {String onMatch(Match)?, String onNonMatch(String)?})returns new String formed by splitting on a Pattern, converting parts, and concatenating results
startsWith(Pattern pattern, [int index = 0])returns bool indicating if starts with another String
substring(int start, [int? end])returns new String that is a substring defined by indexes
toLowerCase()returns new String where all characters are converted to lowercase
toUpperCase()returns new String where all characters are convert to uppercase
trim()returns new String formed by removing leading and trailing whitespace
trimLeft()returns new String formed by removing leading whitespace
trimRight()returns new String formed by removing trailing whitespace

To sort a List of String values in place, use the List sort method and the String compareTo method. For example:

var names = <String>['Maisey', 'Ramsay', 'Oscar', 'Comet'];
names.sort((a, b) => a.compareTo(b));
// names now contains ['Comet', 'Maisey', 'Oscar', 'Ramsay']

The String class defines the following binary operators: + (concatenation), * (repeated n times), == (same code points), [index] (gets code point at index). For example 'ho ' * 3 creates the String 'ho ho ho '.

The table below summarized converting between numbers and strings.

ConversionCode
int to Stringi.toString()
double to Stringd.toString()
double to Stringd.toStringAsFixed(decimalPlaces)
String to intint.parse(s)
String to doubledouble.parse(s)

Enumerations

Enumerations are a special kind of class defined with the enum keyword. The are often used in switch statements.

Each value in an enum is assigned an index starting from zero. They cannot be assigned different numeric values.

Enumerations cannot be defined inside a function.

For example:

enum Color { red, green, blue }

void printColor(Color c) {
print('$c, index=${c.index}');
}

void main() {
printColor(Color.values[1]); // Color.blue, index=1

for (var color in Color.values) {
printColor(color);
}
}

The toString method on enum values returns a String that contains the enum name followed by a period and the enum value. For example, Color.blue.toString() returns 'Color.blue'. To get a String with the enum name and period in Flutter, add import 'package:flutter/foundation.dart'; and pass an enum value to the describeEnum function. For example: describeEnum(Color.blue) returns the String 'blue'.

There is a proposal to allow omitting enum names when referring to their values if the enum type can be inferred. For example, in Flutter it is common to see arguments whose type is MaterialColor which extends ColorSwatch which extends Color. One example is the primarySwatch argument to the ThemeData constructor. With this proposal, primarySwatch: Colors.blue could be shortened to primarySwatch: .blue because the Dart compiler can infer that the value can be a Color enum value.

Type Aliases

To define a name for a type, use a typedef statement. For example:

typedef StringToAnyMap = Map<String, dynamic>;

typedef Callback = void Function(String);

Type Casts

The as keyword casts a value of one type to another. For example, if a variable of type Object currently holds a String value, it can be cast to a String so that methods from that class can be called on the value.

Object obj = 'test';
print((obj as String).length);

This throws if the cast is not valid. For example:

Object obj = 7;
print((obj as String).length); // throws "Script error."

Generic Types

Generics, a.k.a parameterized types, allow writing functions, classes, and methods that accept values of multiple types. Their functionality can differ based on the types of values provided. Using generics often reduces code duplication in cases where multiple blocks of code only differ in the types on which they operate.

Generics are often used to implement new kinds of collections. The core collection classes, described in the next section, do exactly this. For example, when creating a List, the type of the elements it can hold are specific using generics. The following are equivalent:

List<int> numbers = [1, 2, 3]; // type is specified on the variable
var numbers = <int>[1, 2, 3]; // type is specified on the value
var numbers = [1, 2, 3]; // type is inferred

To create a collection that can hold values of any type, use dynamic. For example:

List<dynamic> anyList = [
null, true, 7, 3.14, 'hello', <String>{'red', 'green', 'blue'}
];

for (var value in anyList) {
print(value.runtimeType);
}

Typically parameterized types have single-letter, uppercase names like T. Common names for type parameters include:

Longer, more descriptive names, like Event and State can also be used.

Any number of type parameters can be specified inside angle brackets. They can be constrained to only types that extend a given class using the extends keyword, or left unconstrained in which case values of any type can be used.

The following example demonstrates implementing a generic function. It takes any Iterable containing num values and returns its sum. This means it works with both int and double values since their superclass is num.

N sum<N extends num>(Iterable<N> numbers) {
return numbers.reduce((acc, n) => (acc + n) as N);
}

void main() {
print(sum([1, 2, 3])); // List; 6
print(sum({1, 2, 3})); // Set; 6
}

Collection Types

Dart provides many generic collection classes. Built-in collection classes that can be used without importing include List, Set, and Map. Other collection classes are defined in the package dart:collection and must be imported. These include DoubleLinkedQueue, HashMap, HashSet, LinkedHashMap, LinkedHashSet, LinkedList, ListQueue, Queue, and SplayTreeMap, SplayTreeSet.

For even more collection functions, classes, and extensions, see package:collection which is included in the Flutter SDK. Some of the capabilities of this package are described later in this section.

Iterable

The Iterable generic class represents a collection of values that are accessed sequentially. The values can be lazily computed when requested and an infinite number of values can be generated.

The following generic collection classes all have Iterable as a superclass: DoubleLinkedQueue, IterableBase, IterableMixin, LinkedList, List, ListQueue, Queue, Runes, and Set.

Any Iterable collection can be used in a for-in loop to iterate over its elements.

The Iterable class provides the following constructors:

ConstructorDescription
Iterable()used as superclass of classes that are iterable
Iterable.empty()contains no values
Iterable.generate(int count, E generator(int index)?)generates values on request

The Iterable class provides the following read-only properties:

PropertyDescription
firstfirst element
hashCodehash code
isEmptybool indicating if there are no elements
isNotEmptybool indicating if there is at least one element
iteratorIterator object used to iterate over elements
lastlast element
lengthnumber of elements
singleif only one element, that element; otherwise throws

The Iterable class provides the following methods (some omitted):

MethodDescription
any(bool test(E element))returns a bool indicating if any element passes the test
contains(Object? element)returns a bool indicating if a given element is present
elementAt(int index)returns the element at a given index or throws RangeError if not found
expand<T>(Iterable<T> toElements(E element))returns an Iterable over the flatten elements
every(bool test(E element))returns a bool indicating if every element passes the test
firstWhere(bool test(E element), {E orElse()?})returns the first element that passes the test or the orElse value
fold<T>(T initialValue, T combine(T previousValue, E element))reduces a collection to a single value
forEach(void action(E element))invokes action on each element
join([String separator = ""])returns a string formed by concatenating the string representation of each element
lastWhere(bool test(E element), {E orElse()?})returns the last element that passes the test or the orElse value
map<T>(T toElement(E e))returns a new collection of the results of calling a function on each element
reduce(E combine(E value, E element))same as fold but uses the first element as the initial value
singleWhere(bool test(E element), {E orElse()?})similar to firstWhere, but throws if more than one element passes the test
skip(int count)returns an Iterable that begins after count elements
skipWhile(bool test(E value))returns an Iterable that begins after the initial elements that pass the test
take(int count)returns an Iterable over the first count elements (opposite of skip)
takeWhile(bool test(E value))returns an Iterable that ends after the initial elements that pass the test
toList(bool growable = true)creates and returns a List containing the same elements
toSet()creates and returns a Set containing the same elements
toString()returns the String representation
where(bool test(E element))returns an Iterable over all the elements that pass the test
whereType<T>()returns a new collection of elements with a given type

The differences between the List methods fold and map is that fold takes an initial value and map does not. In the first iteration of the fold method, the combine function is called with initialValue and the first element. In the first iteration of the reduce method, the combine function is called with the first and second elements.

The Iterator generic class provides the read-only instance property current which holds the current element and the instance method moveNext() which sets current to the next element and returns a bool indicating if there is another element. Instances of this class are not typically used directly. Instead, syntax like the for-in loop are used to iterate over an Iterable.

List Class

Arrays in Dart are represented by List objects. A literal array is written as a comma-separated list of values surrounded by square brackets. For example, var numbers = [3, 7, 19];

There are three ways to declare and initialize a variable that holds a List.

// type List<int> is inferred
var l1 = [1, 2, 3];

// type List<int> is specified on the value
var l1 = <int>[1, 2, 3];

// type List<int> is specified on the variable
List<int> l1 = [1, 2, 3];

To iterate over the elements of a List, use a for/in loop or the forEach method. For example:

const dogs = ['Maisey', 'Ramsay', 'Oscar', 'Comet'];

for (var dog in dogs) {
print(dog);
}

// Same as above.
dogs.forEach((dog) => print(dog));

// Same as above.
dogs.forEach(print);

Two lists can be concatenated to create a new List with the + operator. For example:

var list1 = [1, 2];
var list2 = [3, 4];
var list3 = list1 + list2; // [1, 2, 3, 4]

Literal lists can include logic to determine the elements to include. This is referred to as "list comprehension". The following examples demonstrate this:

import 'dart:math';

// Creates a List of int values from start to end - 1
// using the List.generate constructor.
Iterable<int> range(int start, int end) =>
List.generate(end - start, (i) => start + i);

void main() {
// Get the squares of a odd numbers in the range [0, 10).
var squares = [for (var i = 1; i < 10; i += 2) pow(i, 2)];

// Same using a different approach.
// Get the squares of a odd numbers in the range [0, 10).
squares = [for (var i in range(0, 10)) if (i % 2 == 1) pow(i, 2)];
print(squares); // [1, 9, 25, 49, 81]

// Find all the [x, y, z] values where
// x^2 + y^2 = z^2 up to a maximum value of 20.
var pythagorean = [
for (var x in range(1, 20))
for (var y in range(x, 20)) // start at x
for (var z in range(y, 20)) // start at y
if (x * x + y * y == z * z) [x, y, z]
];
print(pythagorean); // [[3, 4, 5], [5, 12, 13], [6, 8, 10], [8, 15, 17], [9, 12, 15]]
}

The List class is generic and implements extends from the Iterable class. It provides many constructors that support creating lists that are empty, filled to a given length with the same value, filled from another Iterable (either modifiable or unmodifiable), filled by a generator function,

In addition to the properties provided by the Iterable class, List objects support the following properties:

PropertyDescription
reversedan Iterable over the elements in reverse order

In addition to the methods provided by the Iterable class, List objects support the following methods (some omitted):

MethodDescription
add(E value)adds an element
addAll(Iterable<E> iterable)adds all the elements in another collection
clear()removes all the elements
fillRange(int start, int end, [E? fillValue])sets the elements in a range to a given value or null
getRange(int start, int end)returns an Iterable over a range of elements
indexOf(E element, [int start = 0])return the first index of a given element
indexWhere(bool test(E element), [int start = 0])returns the index of the first element that passes the test
insert(int index, E element)inserts an element at a given index
insertAll(int index, Iterable<E> iterable)inserts all the elements in an Iterable at a given index
lastIndexOf(E element, [int? start])returns the last index of a given element
lastIndexWhere(bool test(E element), [int? start])returns the index of the last element that passes the test
remove(Object? value)removes the first occurrence of an element
removeAt(int index)removes the element at a given index
removeLast()removes the last element; use removeAt(0) to remove first
removeRange(int start, int end)removes the elements in a given range
removeWhere(bool test(E element))removes all elements that pass a test
replaceRange(int start, int end, Iterable<E> replacements)replaces elements in a given range with those in an Iterable
retainWhere(bool test(E element))removes all elements that do not pass a test
shuffle([Random? random])randomly reorders the elements in place
sort([int compare(E a, E b)?])sorts the elements in place using a Comparator function
sublist(int start, [int? end])returns a new List that is a subset

The List class supports the following operators:

OperatorDescription
+returns a new List formed by concatenating two
==returns bool indicating if operands are the same List in memory, not just contain same elements
[index]returns the element at a given index
[index]=sets the element at a given index

Set Class

A Set is an unordered collection of unique values. A literal set is written as a comma-separated list of values surrounded by curly braces.

There are several ways to declare and initialize a variable that holds a Set.

// type Set<String> is inferred
var colors = {'red', 'green', 'blue'};

// type Set<String> is specified on the value
var colors = <String>{'red', 'green', 'blue'};

// type Set<String> is specified on the variable
Set<String> colors = {'red', 'green', 'blue'};

var colors = Set<String>();
colors.add('red');
colors.add('green');
colors.add('blue');

// The following creates an empty Map, not an empty Set.
// var colors = {};

The literal syntax <value-type>{} creates an empty Set. The literal syntax <key-type, value-type>{} creates an empty Map. The literal syntax {} creates an empty Map with unspecified key and value types whose types must be inferred from the variable type. It does not create an empty Set.

No properties are added beyond those provided by the Iterable class.

In addition to the methods provided by the Iterable class, Set objects support the following methods (some omitted):

MethodDescription
add(E value)adds an element
addAll(Iterable<E> iterable)adds all the elements in another collection
clear()removes all the elements
contains(Object? value)returns a bool indicating if an element is present
containsAll(Iterable<Object>? other)returns a bool indicating if all the elements in an Iterable are present
difference(Set<Object?> other)returns a new Set containing all elements in this one not found in other
lookup(Object? object)returns object if found in the Set or null
remove(Object? value)removes the first occurrence of an element
removeAll(Iterable<Object?> elements)removes all the elements in an Iterable
removeWhere(bool test(E element))removes all elements that pass a test
retainAll(Iterable<Object?> elements)removes all the elements not in an Iterable
retainWhere(bool test(E element))removes all elements that do not pass a test
union(Set<E> other)returns a new Set containing all elements in this one and other

Map Class

A Map is a collection of key/value pairs. The keys and values can have any type. Unlike List and Set, this class is not a subclass of Iterable.

A literal map is written as a comma-separated list of pairs surrounded by curly braces. Each pair is written as a key followed by a colon and a value. When a key is a string, it must be delimited by single or double quotes.

There are several ways to declare and initialize a variable that holds a Map.

// type Map<String, int> is inferred
var colorMap = {'red': 1, 'green': 2, 'blue': 3};

// type Map<String, int> is specified on the value
var colorMap = <String, int>{'red': 1, 'green': 2, 'blue': 3};

// type Map<String, int> is specified on the variable
Map<String, int> colorMap = {'red': 1, 'green': 2, 'blue': 3};

// This map has String keys and values of any type.
Map<String, dynamic> dog = {'name': 'Comet', 'age': 1, 'isFast': true};

// The following creates an empty map where
// keys and values both have the type dynamic.
var myMap = {}; // same as Map()

Values of Map keys are retrieved with the [] operator. This returns a nullable value. They cannot be retrieved with dot syntax like in JavaScript. For example:

var colorMap = {'red': 1, 'green': 2, 'blue': 3};
int? number = colorMap['blue']; // not colorMap.blue
print(number ?? 'not found'); // 3
if (number != null) {
print(number); // 3
}

The spread operator ... can be used to spread Map objects into others. For example:

var map1 = {'apple': 'red', 'banana': 'yellow'};
var map2 = {'cherry': 'red', 'grape': 'green'};
var map3 = {
'lemon': 'yellow',
...map1,
'blueberry': 'blue',
...map2,
'watermelon': 'pink'
};
print(map3); // all the key/value pairs

The Map class provides the following properties:

PropertyDescription
entriesIterable over MapEntry objects (have key and value properties)
isEmptybool indicating if there are no key/value pairs
isNotEmptybool indicating if there is at least one key/value pair
keysIterable over keys
lengthnumber of key/value pairs
valuesIterable over values

The Map class provides the following methods (some omitted):

MethodDescription
addAll(Map<K, V> other)adds all key/value pairs in another Map to this one
addEntries(Iterable<MapEntry<K, V>> newEntries)adds all MapEntry objects in an Iterable
clear()removes all key/value pairs
containsKey(Object? key)returns a bool indicating if a given key is present
containsValue(Object? value)returns a bool indicating if a given value is present
forEach(void action(K key, V value)executes a function on each key/value pair
map<K2, V2>(MapEntry<K2, V2> convert(K key, V value))returns a new Map created by calling a function on each key/value pair
putIfAbsent(K key, V ifAbsent()returns the value for a given key and adds a value if not present
remove(Object? key)removes a key/value pair with a given key if present
removeWhere(bool test(K key, V value)removes all key/value pairs that pass a test
update(K key, V update(V value)updates the value for a given key to the value returned by a function
updateAll(V update(K key, V value)updates all values to the value returned by a function

The MapEntry class represents a single key/value pair from a Map. To create one, call the MapEntry constructor passing it a key and a value. These objects have key and value properties and a toString method.

package:collection

The collection library is a collection of functions, classes, and extensions that extend the capabilities of the collections provided by Dart. These are briefly described in this Flutter Package of the Week YouTube video.

To uses this package, add the following import:

import 'package:collection/collection.dart';

A few examples of the added functionality are described below. Browse the documentation for many more.

To test whether two List objects contain the same elements in the same order, use ListEquality().equals(list1, list2).

To test whether two collections contain the contents, use DeepCollectionEquality().equals(collection1, collection2).

The IterableIntegerExtension and IterableDoubleExtension extensions add the methods average and sum to Iterables of int and double values. Just importing the collection package adds all the extensions it defines. To get the sum of a List of numbers, use myList.sum. To get the average of a List of numbers, use myList.average.

The IterableExtension adds properties and methods to all objects that implement the Iterable interface.

To get the first element of an Iterable or null if it is empty, use the firstOrNull property.

To get the last element of an Iterable or null if it is empty, use the lastOrNull property.

The Iterable methods foldIndexed, forEachIndexed, mapIndexed, reduceIndexed, and whereIndexed are like their counterparts fold, forEach, map, reduce, and where but differ in that they pass the index of each element to the function passed to them.

There are many more Iterable methods described in the documentation.

Print

The print function takes a single String argument and writes it to stdout.

If the argument is not a String, it will be converted to a String by calling the toString method of the value. For example:

class Dog {
String name;
String breed;

// Shorthand constructor - see Class section
Dog(this.name, this.breed);

// Methods that override a superclass method should
// be annotated with @override. In this case we are
// overriding the toString method in the Object class.

String toString() => '$name is a $breed.';
}

void main() {
var d = Dog('Comet', 'Whippet');
print(d); // Comet is a Whippet.
}

Additional Core Classes

The dart:core package defines all the basic types like bool, int, double, and String. It also defines collection types like List, Set, and Map. Highlights of other classes defined in dart:core are described below.

DateTime class

The DateTime class is used to create objects that represent an instant in time. The constructor requires and int value for the year and optionally accepts int values for the month, day, hour, minute, second, and milliseconds. The month and day default to one. The minute, second, and milliseconds default to zero. Other ways to create DateTime objects include the static parse method and the constructors DateTime.fromMillisecondsSinceEpoch, DateTime.now, and DateTime.utc.

The following code demonstrates various ways to create DateTime objects.

var birthday = DateTime(1961, 4, 16); // in local time
var birthdayUtc = DateTime.utc(1961, 4, 16); // in UTC
var nowLocal = DateTime.now(); // in local time

// Defaults to local time, but can request UTC.
var epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
print(epoch); // 1970-01-01 00:00:00.000Z

There isn't an easy way to create DateTime objects for timezones other than the local one and UTC.

The DateTime.parse constructor takes a string that matches a subset of ISO 8601 of the standard. Examples include '1961-04-16 10:19:00' (local time zone), '19610416T101900' (same with T separating date from time), and '1961-04-16 10:19:00Z' (UTC).

The DateTime class defines the following constants:

The DateTime class defines the following properties:

PropertyDescription
dayday of month; 1 to 31
hour0 to 23
isUtcbool
millisecond0 to 999
millisecondsSinceEpochmilliseconds since 1970-01-01T00:00:00Z
minute0 to 59
month1 to 12
second0 to 59
timeZoneNametimezone abbreviation such as CST
timeZoneOffsetdifference from UTC (ex. -6:00:00.00-0.0)
weekdayMONDAY (1) to SUNDAY (7)
yearincludes all digits (4 for current year)

The DateTime class defines the following instance methods:

MethodDescription
add(Duration duration)returns new DateTime with duration added
compareTo(DateTime other)returns comparator value, often used for sorting
difference(DateTime other)returns Duration between receiver and other
isAfter(DateTime other)returns bool indicating if receiver is after other
isAtSameMomentAs(DateTime other)returns bool indicating if receiver is at same moment as other, regardless of time zone
isBefore()returns bool indicating if receiver is before other
subtract()returns new DateTime with duration subtracted
toIso8601String()returns String in format 'yyyy-MM-ddTHH:mm:ss.sssZ, omitting Z if not UTC
toLocal()returns DateTime converted to local
toString()returns String in human-readable format
toUtc()returns DateTime converted to UTC

Duration class

The Duration class is used to create objects that represent a span to time. The constructor takes the following named parameters that are all optional and default to zero: days, hours, minutes, seconds, milliseconds, and microseconds.

The Duration class defines the following constants:

The Duration class defines the following properties:

PropertyDescription
inDaysint number of whole days
inHoursint number of whole hours
inMicrosecondsint number of whole microseconds
inMillisecondsint number of whole milliseconds
inMinutesint number of whole minutes
inSecondsint number of whole seconds
isNegativebool indicating if negative

The Duration class defines the following instance methods:

MethodDescription
abs()returns new Duration that is the absolute value of the receiver
compareTo(Duration other)returns comparator value, often used for sorting
toString()returns String representation

Regular Expressions

A Pattern is a RegExp or String object.

Dart regular expressions use the same syntax as JavaScript regular expressions.

A RegExp object is created with RegExp(r'reg-ex-here');. Recall that placing "r" before a literal string creates a "raw string" that doesn't treat the \ character specially.

The RegExp class defines the following properties:

PropertyDescription
isCaseSensitivebool indicating if matches are case-sensitive (defaults to true)
isDotAllbool indicating if periods should match line terminators (defaults to false)
isMultilinebool indicating if multiline matching will be performed (defaults to false)
isUnicodebool indicating if whether Unicode matching will be performed (defaults to false)
patternregular expression as a String

The RegExp class defines the following instance methods:

MethodDescription
allMatches(String input, [int start = 0])returns Iterable<RegExpMatch> for iterating over matches
firstMatch(String input)returns RegExpMatch? for first match found or null
hasMatch(String input)returns bool indicating if a match was found
matchAsPrefix(String input, [int start = 0])returns a Match? that is null unless a match is found at the start
stringMatch(String input)returns a String? substring of first match in input or null (ignores capture groups)

The following example creates a regular expression that matches strings starting with "The " and ending with " win.". It captures all the characters in between.

var re = RegExp(r'^The (.+) win\.$');
var s = 'The St. Louis Blues win.';
var match = re.firstMatch(s);
if (match != null) {
print(match.group(1)); // St. Louis Blues
}

The optional named constructor parameter multiLine has a bool value that indicates whether it should match at the beginning and end of every line. This defaults to false.

The optional named constructor parameter caseSensitive has a bool value that indicates whether matching should be case-sensitive. This defaults to true.

The RegExpMatch class extends the Match class. Instances of this class describe a regular expression matching result.

The RegExpMatch class defines the following properties:

PropertyDescription
endindex where the match ends plus 1
groupCountnumber of captured groups
inputentire string from which the match was found
patterna Pattern object describing the matching pattern
startindex where the match begins

The RegExpMatch class defines the following instance methods:

MethodDescription
group(int groupIndex)String matched at a given group index
groups(List<int> groupIndices)List of String values matched at specified group indices

The [index] operator can be used in place of the group method retrieve the same value.

Stopwatch Class

The Stopwatch class is used to measure elapsed time in a section of code.

The Stopwatch class defines the following properties.

PropertyDescription
elapsedelapsedTicks converted to a Duration
elapsedMicrosecondsint microseconds (1e-6 second)
elapsedMillisecondsint milliseconds (1e-3 second)
elapsedTicksnumber clock ticks
frequencynumber of ticks in a second
isRunningbool indicating if the stopwatch is running

The number of clock "ticks" in a second is equal to the frequency property. A common value is 1,000,000.

The elapsedTicks property has the same value as elapsedMicroseconds if frequency is 1e6.

The Stopwatch class defines the following instance methods:

MethodDescription
reset()resets all the elapsed* properties to zero
start()starts the stopwatch
stop()stops the stopwatch

The following code demonstrates using the Stopwatch class:

import 'dart:math';

void main() {
var sw = Stopwatch();
sw.start();
var sum = 0;
for (var i = 0; i < 1000000000; i++) {
sum += pow(i, 3) as int;
}
sw.stop();
print(sum);
print('elapsed = ${sw.elapsed.inMilliseconds} ms');
}

StringBuffer Class

The StringBuffer class supports efficient, incremental building of strings. It defines the following properties.

PropertyDescription
isEmptybool indicating whether length == 0
isNotEmptybool indicating whether length >= 0
lengthnumber of code points

The StringBuffer class defines the following instance methods:

MethodDescription
clear()clears all code points
toString()String equivalent
write(Object? object)writes String representation of object
writeAll(Iterable objects, [String separator = ''])writes String representation of all objects in an Iterable with an optional separator
writeCharCode(int charCode)writes a single code point
writeln([Object? obj = ''])same as write(Object? object), but adds newline

Timer class

The Timer class executes a callback function after a given Duration either once or repeatedly. It provides capabilities similar to the JavaScript functions setTimeout and setInterval. The Timer class is defined in the dart:async package which must be imported.

The following example demonstrates creating Timers that fire just once and repeatedly:

import 'dart:async';

void main() {
print('starting timer');
var timer = Timer(Duration(seconds: 2), () {
print('finished timer');
});
//timer.cancel();

var count = 0;
Timer.periodic(Duration(seconds: 1), (timer) {
print('isActive = ${timer.isActive}; tick = ${timer.tick}');
count++;
if (count >= 5) timer.cancel();
});
}

When Timer.periodic is used in a Flutter widget, the timer needs to be stopped when the widget instance is removed from the screen. To do this, capture the return value, override the widget dispose method, and call timer.cancel() and super.dispose() inside it.

Uri Class

The Uri class represents a parsed URI. It provides many constructors for creating a Uri object.

The Uri class defines the following properties.

PropertyDescription
authoritysame String value as host
dataUriData object describing a data URI
fragmentString hash portion without leading #
hasAbsolutePathbool indicating whether the URI has an absolute path
hasAuthoritybool indicating whether the URI has an authority (or domain) part
hasEmptyPathbool indicating whether the URI is missing a path
hasFragmentbool indicating whether the URI has a fragment (or hash) part
hasPortbool indicating whether the URI has a specified port
hasQuerybool indicating whether the URI has query parameters
hasSchemebool indicating whether the URI has a specified scheme such as "https"
hostString domain
isAbsolutebool indicating whether the URI is absolute; false for relative
originString scheme and host combined
pathString path portion that follows host
pathSegmentsList<String> of path parts
portint port that optionally follows host
queryString containing all query parameters
queryParametersMap<String, String> of all query parameters
queryParametersAllMap<String, List<String>> of all query parameters including duplicates
schemeString such as "http", "https", or "data"
userInfoString optional username and password that can precede host

The Uri class defines the following instance methods:

MethodDescription
isScheme(String scheme)returns Bool indicating whether the URI has a given scheme
normalizedPath()returns a new Uri object that is normalized
removeFragment()returns a new Uri without the fragment (or hash) portion
replace(...)returns a new Uri with given parts replaced
resolve(String reference)returns a new Uri resolved relative to a URL String
resolveUri(Uri reference)returns a new Uri resolved relative to another Uri
toFilePath({bool? windows})returns corresponding String file path
toString()returns String representation

The following code demonstrates parsing a URL string using the static parse method and extracting its components from fields of a Uri object. There are many other static methods not described here.

var url = 'https://mvolkmann.github.io/foo/bar?color=yellow&size=10#my-hash';
var uri = Uri.parse(url);
print('scheme = ${uri.scheme}'); // https
print('authority = ${uri.authority}'); // mvolkmann.github.io
print('host = ${uri.host}'); // mvolkmann.github.io
print('port = ${uri.port}'); // 443?
print('origin = ${uri.origin}'); // https://mvolkmann.github.io
print('path = ${uri.path}'); // /foo/bar
print('pathSegments = ${uri.pathSegments}'); // ['foo', 'bar']
print('query = ${uri.query}'); // color=yellow&size=10
print('queryParametersAll = ${uri.queryParametersAll}'); // {color: [yellow], size: [10]}
print('fragment = ${uri.fragment}'); // my-hash

Spread Operators

The spread operator ... spreads a collection inside another.

var colors = ['red', 'green', 'blue'];
var moreColors = ['black', ...colors, 'white'];
print(moreColors);

var fruits = {'b': 'banana', 'c': 'cherry'};
var moreFruits = {'a': 'apple', ...fruits, 'd': 'date'};
print(moreFruits);

The null-aware spread operator ...? only spreads when the value is not null.

Map<String, String>? maybeFruits;
var evenMoreFruits = {'a': 'apple', ...fruits, ...?maybeFruits, 'd': 'date'};
print(evenMoreFruits);

The spread operator cannot be used to expand a List into function arguments. Use that static method Function.apply for this purpose.

Nullable Values

Dart provides special operators for dealing with nullable values. The following example demonstrates these.

import 'dart:math';

var random = Random();
String? randomString() => random.nextBool() ? 'test' : null;

void main() {
String? s;
s = randomString(); // 'test' or null
print('s = $s');

// The ?. operator evaluates to null if the value on the left is null.
// Otherwise it evaluates to the property or method on the right.
print(s?.toUpperCase());

// The ?? operator provides an alternative value to use
// if the value on the left is null.
print(s ?? 'missing value');

// The ??= operator assigns a value to a variable
// only if the variable value is currently null.
s ??= 'supplied value';
print(s);

// The !. operator asserts that the value on the left is not null.
// It essentially casts away nullability.
// s! here is equivalent to (s as String).
// The program will crash with "Script error." if it is null.
print(s!.toUpperCase());
}

Functions

Dart functions are represented by objects from the Function class. They are first class which means they can be assigned variables, passed to other functions, and returned from other functions.

All Dart functions, including anonymous ones, are closures. This means they have access to in-scope variables declared outside the function.

Named function definitions have the following syntax:

return-type fn-name(parameter-list) {
statements
}

Functions with a return type other than void must return a value or throw an exception/error. Functions with a return type of void always return null.

Anonymous functions have similar syntax, but the name is omitted and the return type is inferred.

(parameter-list {
statements
}

Functions that only return the value of a single expression can use a shorthand syntax where => expression; is short for { return expression; }.

Specifying parameter types and the return type are optional. For example, the following is a valid function definition:

add(n1, n2) => n1 + n2;
main() => print(add(2, 3)); // 5

Functions can have both positional and named parameters and each of these can be required or optional. The order of these in both function declarations and calls to functions must be required positional parameters, followed by optional positional parameters, followed by named parameters in any order.

A future version of Dart will allow named parameters to appear anywhere in an argument list. See issue 47451.

Required positional parameters cannot be given default values. Optional positional parameters must appear inside square brackets after the required positional parameters. They must either have a default value or an optional type (ending with ?).

In the following example, req1 and req2 are required parameters and opt1 and opt2 are optional. If only two arguments are passed, opt1 is set to its default value of 0 and opt2 is set to null.

demo(int req1, int req2, [int opt1 = 0, int? opt2]) {
...
}

Named parameters are declared inside curly braces. These must follow all the positional parameters, if any. Named parameters are optional by default and must either have a default value or a nullable type (ending with ?). Default values can be preceded by = or :, but = is preferred. To make a named parameter required, add the required keyword before its type. For example:

// In this version, the named parameter "by" is required.
//int multiply(int n, {required int by}) => n * by;

// In this version, the named parameter "by" is optional.
int multiply(int n, {int by = 0}) => n * by;

main() {
print(multiply(2, by: 3)); // 6
print(multiply(4)); // 4
}

Dart does not support variadic functions which are functions that accept a variable number of arguments.

There are three ways to call a function, using the function invocation operator (), the call instance method, and the static apply method. The following code demonstrates these:

// This function takes two required positional parameters
// and two optional named parameters.
num sum(num n1, num n2, {num? min, num? max}) {
var result = n1 + n2;
if (min != null && result < min) return min;
if (max != null && result > max) return max;
return result;
}

void main() {
print(sum(1, 2)); // 3
print(sum(1, 2, min: 10)); // 10

print(sum.call(1, 2)); // 3
print(sum.call(1, 2, min: 10)); // 10

// First argument to apply is a function to call.
// Second argument to apply is an optional List of positional arguments.
// Third argument to apply is an optional Map<Symbol, dynamic> of named arguments.
print(Function.apply(sum, [1, 2])); // 3
print(Function.apply(sum, [1, 2], {#min: 10})); // 10
print(Function.apply(sum, [15, 10], {#min: 10, #max: 19})); // 19
}

The spread operator cannot be used to expand a List into function arguments. However, the static apply method on the Function class (shown above) can be used for this purpose.

Anonymous function definitions are written like named function definitions, but omit the name. Unlike in JavaScript, parentheses are required around the parameter list even when there is only one parameter. For example:

var numbers = [3, 7, 9];
numbers.forEach((n) => n * 2);

Trailing commas are allowed after the last parameter in function definitions and after the last argument in function calls. This causes dart format to place each parameter or argument on a separate line.

Functions that do not explicitly return a value evaluate to null.

Generator Functions

Generator functions generate values on demand in a lazy fashion. The values it generates are not computed until they are requested.

Generator functions use the yield keyword to return a single value. They can also use the yield* keyword to return all the values from another generator function individually. This serves as a shorter alternative to iterating over the values from another generator and returning each one.

Synchronous generator functions include the sync* keyword after the parameter list and before the body. They always return an Iterator and must generate a value as soon as requested.

The following synchronous generator function generates int values in a given range which can be open-ended.

Iterable<int> range(int start, [int? end]) sync* {
if (end == null) {
var i = start;
while (true) {
yield i++;
}
} else {
for (var i = start; i <= end; i++) {
yield i;
}
}
}

// This demonstrates several ways to use the "range" function.
// All except the last print the numbers 1 through 5.
void main() {
for (var i in range(1, 5)) {
print(i);
}

for (var i in range(1)) {
print(i);
if (i == 5) break;
}

for (var i in range(1).take(5)) {
print(i);
}

// Prints the first five positive integers that are multiples of 3
// which are 3, 6, 9, 12, and 15.
range(1).where((n) => n % 3 == 0).take(5).forEach(print);
}

The Iterable generator method provides an easy way to generate a fixed number of values. It is passed the number of values to generate and a function that takes an index and returns a computed value. For example, the range function above can be replaced by the following if an end value is always supplied.

Iterable<int> range(int start, int end) {
var count = end - start + 1;
return Iterable<int>.generate(count, (index) => index + start);
}

Asynchronous generator functions are sometimes useful, but not frequently used. They include the async* keyword after the parameter list and before the body. They always return a Stream and can return a Future for values that will be computed later.

The following asynchronous generator function reads text files containing player scores on separate lines. Each file contains the scores of a single player.

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

class PlayerAverage {
String player;
double average;

PlayerAverage(this.player, this.average);


String toString() => '$player average score is $average.';
}

Stream<PlayerAverage> computeAverageScores(List<String> players) async* {
for (var player in players) {
var lines = await File('scores-$player.txt').readAsLines();
var total = lines.fold(0, (int acc, String line) => acc + int.parse(line));
var average = total / lines.length;
print('$player average is $average.');
yield PlayerAverage(player, average);
}
}

void main() async {
var players = ['Mark', 'Tami', 'Amanda', 'Jeremy'];

PlayerAverage? winner;
// Note the use of "await for" to iterate over
// values in a Stream inside an async function.
// Only use this when it is certain that the Stream will complete.
await for (var result in computeAverageScores(players)) {
if (winner == null || result.average > winner.average) winner = result;
}

if (winner != null) {
print(
'The winner is ${winner.player} '
'with an average score of ${winner.average}.'
);
}
}

Conditional Logic

Dart supports two statements for implementing conditional logic, if and switch. It also supports the ternary operator ? :.

The condition specified in an if statement must be an expression that evaluates to a bool value.

Here are examples of if statements.

if (temperature < 30) print('stay inside'); // braces are optional

if (temperature > 80) {
print('hot');
} else if (temperature < 40) {
print('cold');
} else {
print('comfortable');
}

The switch statement compares values of type int or String, enumerated types, or compile-time constants. Curly braces around the cases are required. Here is an example of a switch statement.

enum Color { red, green, blue }
var color = Color.green;
switch (color) {
case Color.red:
print('hot');
break;
case Color.blue:
print('cold');
break;
default:
print('comfortable');
}

When an enum value is being evaluated, the default clause is required unless there is a case for each possible value.

The ternary operator selects a value based on boolean expressions. For example:

print(coinSide == 'heads' ? 'You win.' : 'You lose.');

var assessment =
temperature >= 80 ? 'hot' :
temperature <= 32 ? 'cold' :
'comfortable';

The assert statement asserts that a condition is true. If it is not, an optional message is printed and the program exits with an AssertionError. These statements are only executed when enabled. To run a Dart program with asserts enabled, enter dart run --enable-assert. For example:

assert(!worldEnding(), 'So long.');

Iteration

Dart supports three statements for implementing iteration, for, while, and do-while. The condition specified in each of these must be an expression that evaluates to a bool value. The for and while loops are top-tested. They perform a test at the beginning of each iteration and so may not execute their bodies at all. The do-while loop is bottom-tested. It performs a test after each iteration and so will always execute the body at least once

Here are examples of each:

for (var i = 1; i <= 5; i++) print(i); // 1 to 5; braces are optional

for (var i = 1; i <= 5; i++) {
print(i); // 1 to 5
}

var dogs = ['Maisey', 'Ramsay', 'Oscar', 'Comet'];

for (var dog in dogs) print(dog); // each dog name; braces are optional

for (var dog in dogs) {
print(dog); // each dog name
}

var i = 1;
while (i <= 5) {
print(i); // 1 to 5
i++;
}

do {
print(i); // 6 to 1
i--;
} while (i > 0);

All three kinds of loops support the break and continue statements. The break statement exits the loop. The continue statement skips the remainder of the current iteration and proceeds to the beginning of the next iteration, if any.

Exception Handling

To throw an exception, use the throw keyword followed by any object. Typically the object is one that extends from Exception or Error, but this is not required. For example, throwing a String is allowed.

For example:

throw FormatException('invalid phone number');

In general exception can be caught and handled, but errors should result in the program terminating.

Builtin Exception subclasses include FormatException, IOException, TimeoutException, and more.

Builtin Error subclasses include ArgumentError, AssertionError, OutOfMemoryError, TypeError, UnimplementedError, and more. Custom subclasses or Exception and Error can also be defined.

Custom exception classes can be created by defining and instantiating classes that implement Exception or Error. For example:

class RangeException implements Exception {
String message;

RangeException(this.message);


String toString() => message;
}

class Range {
num lower;
num upper;

Range({required this.lower, required this.upper});

bool includes(num value) => lower <= value && value <= upper;

void assertIncludes(num value) {
if (value < lower) {
throw RangeException('value $value is less than lower bound $lower');
}
if (value > upper) {
throw RangeException('value $value is greater than upper bound $upper');
}
}
}

To catch an exception, use the try, catch, and finally keywords. For example:

try {
// code that can throw
} on FormatException {
// catching a specific kind of exception,
// but not using the exception object
} on IOException catch (e) {
// catching a specific kind of exception,
// and using the exception object
} catch (e) {
// catching any kind of exception
// and using the exception object
} finally {
// code to run regardless of whether there was a thrown
}

When a function catches an exception, it can rethrow the exception so the caller can handle it. Dart statement rethrow; is equivalent to throw e; and is preferred.

The Range and RangeException classes defined above can be used as follows:

var range = Range(lower: 1, upper: 10);
try {
range.assertIncludes(3); // doesn't throw
range.assertIncludes(0); // throws
} on RangeException catch (e) {
print(e); // value 0 is less than lower bound 1
} catch (e) {
print("something else went wrong");
}

Access Specifiers

Dart does not support keywords to indicate access levels of things like functions, classes, properties, and methods. Instead add an underscore prefix to a name to indicate that it is private. This is enforced at the library level.

All .dart source files define a "library". Private names in a library are accessible inside the library, but not outside it. A compile-time error is generated if such access is attempted.

Classes

Classes define:

Definitions of immutable classes can be proceeded by the @immutable annotation. This requires all fields to be declared with the final keyword. The compiler will output a warning if the class defines any non-final fields

Constructors

A constructor is defined as a method with no return type and the same name as the class. For example:

import 'dart:math';

class Point {
double x;
double y0;

/* Long way to write a constructor that is not preferred.
Point(double x, double y) {
this.x = x;
this.y = y;
}
*/


// Another way to write the constructor is to use an initializer list.
// Note that having a body is optional.
//Point(double x, double y) : this.x = x, this.y = y;

// Preferred way to write this constructor that
// handles assigning argument values to instance properties.
Point(this.x, this.y);

// To make x and y be optional named parameters with default values,
// change the previous constructor to the following:
//Point({this.x = 0, this.y = 0});

// To make x and y be required named parameters,
// change the previous constructor to the following:
//Point({required this.x, required this.y});

double get distanceFromOrigin => sqrt(pow(x, 2) + pow(y, 2));
}

void main() {
var p = Point(3, 4);
print(p.distanceFromOrigin); // 5
}

When instance properties have default values, during instance creation those are assigned before the constructor is called. Those values can be used in a constructor to compute the values of other properties.

Here's a somewhat advanced rule regarding constructors. Properties marked as final that are not also marked as late and have a non-nullable type must be initialized before the constructor body is reached. Typically this is done in the constructor initializer list.

Static properties which exist outside of any instance cannot be set in a constructor.

The keyword this is only needed to disambiguate property and method references from local variables with the same names. When there is no name conflict, preceding property and method names with this. is not required, and is discouraged.

Getters and Setters

When object properties are accessed, Dart actually calls a "getter" or "setter" method. Non-private properties (including those marked late final but not final) are automatically given getter and setter methods which allow them to be accessed and set from outside their class. Getter and setter methods can be provided for private properties. Getters can also be used to implement computed properties. Setters can validate new values and throw when they are invalid.

The following code demonstrates writing getter and setter methods. Typically the properties on which they operate are made private to prevent direct access.

class Point {
double _x = 0;
double y = 0;

double get x {
print('in getter for x');
return _x;
}

// Setter methods should not have a return type.
set x(double x) {
print('in setter for x');
_x = x;
}

Point(x, this.y) : _x = x;


String toString() {
return '($_x, $y)';
}
}

void main() {
var pt = Point(2, 3);
pt.x = 7; // outputs "in setter for x"
print(pt.x); // outputs "in getter for x" and 7
}

The following code demonstrates implementing a read-only property and a computed property:

class Person {
String _name;
DateTime? birthday;

Person({required String name, this.birthday}) : _name = name;

// This getter computes its value.
int get age {
if (birthday == null) return 0;
var now = DateTime.now();
var years = now.year - birthday!.year;
var month = birthday!.month;
if (now.month < month || (now.month == month && now.day < birthday!.day)) {
years--;
}
return years;
}

// This getter simply provides access to a private property.
// The property is read-only because
// it is private and no setter is defined.
String get name => _name;

// This setter performs validation before setting a private property.
set name(String newName) {
if (newName.isEmpty) throw "Person name cannot be empty";
_name = newName;
}


String toString() => birthday == null ? name : '$name is $age years old.';
}

void main() {
var p1 = Person(name: 'Mark', birthday: DateTime(1961, DateTime.april, 16));
var p2 = Person(name: 'Tami');
print(p1); // Mark is 60 years old.
print(p2); // Tami

try {
p2.name = 'Tamara';
print(p2); // Tamara
p2.name = ''; // throws
print(p2);
} catch (e) {
print(e); // Person name cannot be empty
}
}

If a class doesn't define a constructor, a no-arg constructor that doesn't initialize any properties is provided. If the class has a superclass, the default constructor calls its no-arg constructor.

Initializer Lists

An "initializer list" is a list of property assignments that follow the parameter list and a colon. Both initializer lists and bodies are optional in constructors. When both are present, the assignments in the initializer list occur before the body is executed. The named constructors below demonstrate including an initializer list and omitting a body.

Named Constructors

A class can only have one "regular constructor", but it can have any number of "named constructors". These begin with the the class name followed by a period and a unique name. For example, the Point class above could have a named constructor for creating an origin point and another for initializing x and y to the same value.

  Point.origin() : x = 0, y = 0;

Point.same(double value) : x = value, y = value;

The create an object from a class, call one of its constructors in the same was as calling a function, without a new keyword.

Pulling all of this together we can write the following:

class Point {
double x = 0;
double y = 0;

Point(this.x, this.y);
Point.origin() : x = 0, y = 0;
Point.same(double value): x = value, y = value;


String toString() {
return '($x, $y)';
}
}

void main() {
var pt = Point(2, 3);
print(pt); // (2.0, 3.0)
pt = Point.origin();
print(pt); // (0.0, 0.0)
pt = Point.same(4);
print(pt); // (4.0, 4.0)
}

Here is one more example that demonstrates a constructor that takes named parameters.

class Person {
String name;
int? age;

// This constructor only has named parameters.
// The first parameter is required and the second is optional.
Person({required this.name, this.age});


String toString() => age == null ? name : '$name is $age years old';
}

void main() {
var p1 = Person(name: 'Mark', age: 60);
print(p1); // Mark is 60.

var p2 = Person(name: 'Tami');
print(p2); // Tami
}

Instance Methods

Classes can define instance methods. These look like function definitions, but differ in that they can use the this keyword. For example, the following instance method can be added to the Point class defined above:

class Point {
... same as before ...

void translate(double dx, double dy) {
// We don't need "this." before x and y here because
// there is no name conflict with local variables.
x += dx;
y += dy;
}
}

void main() {
var pt = Point(2, 3);
pt.translate(1, -2);
print(pt); // (3, 1)
}

When a variable holds a nullable object reference, method calls on the variable must check for null. For example:

int listSum(List<int>? list) {
return list == null ? 0 : list.reduce((acc, n) => acc + n);
}

void main() {
List<int>? numbers;
print(listSum(numbers)); // 0
numbers = [1, 2, 3];
print(listSum(numbers)); // 6
}

Class Methods

Classes can define class methods. These look like instance methods, but are preceded by the static keyword. They cannot use the this keyword because they aren't associated with a specific instance, but they can access static properties and the instance properties of objects passed to them. The following example demonstrates a static method in the Point class that takes a List of Point objects and returns a Point in the center of them. It also defines a static property that holds the number of Point objects that have been created.

import 'dart:math';

class Point {
static int instanceCount = 0;
double x = 0;
double y = 0;

Point(this.x, this.y) {
instanceCount++;
}

static Point centerOf(List<Point> points) {
if (points.isEmpty) return Point(0, 0);

final first = points[0];
var minX = first.x;
var minY = first.y;
var maxX = first.x;
var maxY = first.y;
//for (var point in points) {
for (var i = 1; i < points.length; i++) {
var point = points[i];
minX = min(minX, point.x);
minY = min(minY, point.y);
maxX = max(maxX, point.x);
maxY = max(maxY, point.y);
}
return Point((maxX + minX) / 2, (maxY + minY) / 2);
}


String toString() {
return '($x, $y)';
}
}

void main() {
var points = [Point(1, 1), Point(5, 4), Point(7, 2)];
print(Point.instanceCount); // 3
print(Point.centerOf(points)); // (4, 2.5)
}

Operators

Operators are similar to instance methods, but include the keyword "operator". Only operator names supported by Dart can be defined. For example, there is no @ operator in Dart, so custom classes cannot define it. The following code defines the "+" operator for the Point class. This returns a new Point and does not modify the Point on the left.

class Point {
... same as before ...

Point operator +(Point other) {
return Point(x + other.x, y + other.y);
}
}

void main() {
var pt1 = Point(2, 3);
var pt2 = Point(1, 2);
var pt3 = pt1 + pt2;
print(pt3); // (3, 5)
}

Factory Constructors

A normal constructor creates an object, but doesn't explicitly return it. A "factory constructor" can call a normal constructor with computed arguments, modify the object it creates, and return it. One use of a factory constructor is to implement a singleton class where it is not possible create additional instances.

class MySingleton {
// This is a private, named constructor.
MySingleton._private();

// This creates an instance of this class.
static final _instance = MySingleton._private();

// This factory constructor always returns the same instance.
factory MySingleton() => _instance;
}

void main() {
var obj1 = MySingleton();
var obj2 = MySingleton();
print(identical(obj1, obj2)); // true
}

Inheritance

A class can inherit the properties and methods of one other class using the extend keyword. Classes that do not explicitly extend another class implicitly extend the built-in Object class. This means all classes extend Object.

The following code demonstrates creating a subclass of the Person class defined above. Note how the super keyword is used to call a superclass constructor or superclass method whose name matches one in the subclass. The superclass constructor can be called from an initializer list.

class SoftwareEngineer extends Person {
String primaryLanguage;

SoftwareEngineer({
required String name,
int? age,
required this.primaryLanguage
}) : super(name: name, age: age);

// This method calls the superclass method with the same name
// using the super keyword.

String toString() {
var connector = age == null ? '' : ' and';
return '${super.toString()}$connector knows $primaryLanguage.';
}
}

void main() {
var p3 = SoftwareEngineer(name: 'Mark', age: 60, primaryLanguage: 'Dart');
print(p3);

var p4 = SoftwareEngineer(name: 'Tami', primaryLanguage: 'JavaScript');
print(p4);
}

To call a named constructor of a superclass, use super.theName(arguments).

Abstract Classes (Interfaces)

Classes can be marked with the abstract keyword which prevents creating instances of them. Abstract classes are useful for defining functionality to be implemented or inherited by other classes. Including bodies in the methods of an abstract class is optional. When a body is present, subclasses can use the method as is or override it. When a body is not present, a semicolon is placed after the parameter list and subclasses must override the method to provide a body.

A class can implement any number of other classes. For example, class A implements B, C { ... } means that class A will implement every method described in classes B and C, regardless of whether B and C are abstract (because every class defines an interface) and regardless of whether their methods include bodies.

Interface in other programming languages are collections of method signatures that some classes implement. Dart doesn't support defining "interfaces", but abstract classes can be used for this purpose.

A subclass of an abstract class must implement all methods described in the abstract class that do not have bodies. If a subclass implements all the methods, it can use the implements keyword instead of the extends keyword to express its relationship to the abstract class. But if a subclass wants to inherit methods from an abstract class that contain bodies, it must use the extends keyword.

When overriding a superclass method, the parameter types can be made more restrictive using the covariant keyword. The following code demonstrates using this keyword.

import 'dart:math';

abstract class Shape {
double area();
bool same(Shape shape);
}

class Circle implements Shape {
double x; // center
double y; // center
double radius;

Circle({required this.radius, this.x = 0, this.y = 0});

double area() => pi * pow(radius, 2);

bool same(covariant Circle other) =>
x == other.x && y == other.y && radius == other.radius;
}

class Rectangle implements Shape {
double x; // left
double y; // bottom
double height;
double width;

Rectangle({required this.height, required this.width, this.x = 0, this.y = 0});

double area() => width * height;

bool same(covariant Rectangle other) =>
x == other.x && y == other.y && height == other.height && width == other.width;
}

void main() {
var c = Circle(radius: 5);
print(c.area()); // 78.5...

var r = Rectangle(width: 6, height: 3, x: 2, y: 4);
print(r.area()); // 18

print(c.same(Circle(radius: 5))); // true
print(c.same(Circle(radius: 3))); // false

print(r.same(Rectangle(width: 6, height: 3, x: 2, y: 4))); // true
print(r.same(Rectangle(width: 6, height: 3))); // false
}

In addition to defining methods, abstract classes can define instance properties. Classes that implement such an abstract class must also define the same instance properties and annotate them with @override. This is useful when an abstract class defines methods with bodies that require certain instance properties to be present and subclasses will inherit those method implementations.

The following example defines a generic class Pair. This class implements the core abstract class Comparable which describes the compareTo method. Instances of the Pair class hold a pair of values that have the same type. That type must also implement the Comparable interface. Note how this is enforced on the generic type parameter using the extends keyword.

The List sort method can sort any values that implement Comparable. It can also sort values that do not implement Comparable if it is passed a function for performing comparisons. But that is not necessary for Pair objects.

class Pair<T extends Comparable> implements Comparable<Pair<T>> {
T first;
T second;

Pair(this.first, this.second);


int compareTo(Pair<T> other) {
var result = first.compareTo(other.first);
if (result == 0) result = second.compareTo(other.second);
return result;
}


String toString() => '($first, $second)';
}

main() {
var intPairs = [
Pair(1, 2),
Pair(4, 3),
Pair(2, 7),
Pair(0, 4),
Pair(4, 3),
Pair(3, 1)
];
intPairs.sort();
print(intPairs); // [(0, 4), (1, 2), (2, 7), (3, 1), (4, 3), (4, 3)]

var stringPairs = [
Pair('red', 'apple'),
Pair('green', 'grape'),
Pair('orange', 'peach'),
Pair('yellow', 'banana'),
Pair('pink', 'watermelon'),
Pair('orange', 'orange')
];
stringPairs.sort();
print(stringPairs);
// [(green, grape), (orange, orange), (orange, peach),
// (pink, watermelon), (red, apple), (yellow, banana)]
}

Mixins

Mixins provide a way to share property definitions and method implementations between classes.

There are three ways to define a mixin.

  1. Use the mixin keyword (preferred).
  2. Define a class with no constructor.
  3. Define an abstract class.

Recall that classes have a constructor and can be extended. Also recall that abstract classes can be implemented.

Regardless of how a mixin is defined, it cannot do any of the following:

A mixin can be used with the implements keyword to define methods that a class must implement.

To mix a mixin into a class, use the with keyword. A class can use any number mixins. The class can then access any of the mixin properties and methods. It can also override any of the mixin methods.

The order which classes are listed after the with keyword matters. Method lookup occurs from right to left (last one in wins). For example, in class A extends B with C, D { ... }, if B, C, and D all implement the same method, the version in D will take precedence.

When calling a method on a class that extends another class and mixes in multiple mixins, the order of the mixins matters. The extends keyword must appear before the with keyword. Suppose we have class A extends B with C, D and B, C, and D all define a method with the same name. The last one in wins, so the version in D is used.

The following code demonstrates defining two mixins and mixing both into the same class:

import 'dart:math';

// Objects that are "Sized" have a height and weight.
mixin Sized {
double height = 0; // in feet
double weight = 0; // in pounds

void setSize({required double height, required double weight}) {
this.height = height;
this.weight = weight;
}

double getBmi() {
var kg = weight * 0.453592;
var meters = height * 0.3048;
return kg / pow(meters, 2);
}
}

// Objects that are "Located" have x and y coordinates.
mixin Located {
double x = 0;
double y = 0;

void setLocation({required double x, required double y}) {
this.x = x;
this.y = y;
}

double distanceFrom(Located other) {
return sqrt(pow(x - other.x, 2) + pow(y - other.y, 2));
}
}

class Animal {
String species;

Animal({required this.species});
}

// A class can only extend one other class,
// but it can mixin any number of mixins.
class Human extends Animal with Located, Sized {
String name;

Human({required this.name}) : super(species: 'homo sapiens');
}

// We can define additional classes that mixin
// one or both of the Located and Sized mixins.

main() {
var me = Human(name: 'Mark');
me.setSize(height: 6.17, weight: 172);
me.setLocation(x: 0, y: 0);

var wife = Human(name: 'Tami');
// As an alternative to calling setSize,
// we can set the height and weight properties directly.
wife.height = 5.42;
wife.weight = 120;
// As an alternative to calling setLocation,
// we can set the x and y properties directly.
wife.x = 3;
wife.y = 4;

print('my BMI = ${me.getBmi()}'); // 22.06...
print('wife BMI = ${wife.getBmi()}'); // 19.94...
print('distance = ${me.distanceFrom(wife)}'); // 5
}

A mixin can be restricted to only be applicable to classes that extend a given class. For example, the first line of the Sized mixin above could be changed to mixin Sized on Animal to only allow it to be mixed into subclasses of Animal.

Extensions

The extension keyword is used to add properties and methods to an existing class, including core Dart classes.

The following code adds a property and a method to the String class.

extension StringExtension on String {
// Property that holds the first character or null.
String? get first => isEmpty ? null : this[0];

// Method that returns the last character or null.
// This demonstrates adding a method, but it would be
// better to make this a computed property like "first".
String? last() => isEmpty ? null : this[length - 1];

// Method that returns a new String with the characters in reverse order.
String reverse() => String.fromCharCodes(codeUnits.reversed);
}

main() {
var name = 'Mark';
print(name.first); // 'M'
print(name.last()); // 'k'
print(name.reverse()); // 'kraM'
}

Object Class

All Dart classes inherit directly or indirectly from the Object class. This means all objects have the properties and methods defined by that class.

The Object class provides the following properties:

PropertyDescription
hashCodean int value generated from object properties
runtimeTypea Type object describing the object type

The Object class provides the following methods:

MethodDescription
noSuchMethod(Invocation invocation)called when a non-existent method is called on the object
toString()returns the String representation of the object

Invocation Class

The Invocation object passed to the noSuchMethod method of the Object class provides the following properties:

PropertyDescription
isAccessorbool indicating whether a getter or setter was called
isGetterbool indicating whether a getter was called
isMethodbool indicating whether a method was called
isSetterbool indicating whether a setter was called
memberNameSymbol name of the invoked member
namedArgumentsMap of named arguments
positionalArgumentsList of positional arguments
typeArgumentsList of argument types

noSuchMethod Method

The noSuchMethod method defined by the Object class is available in all objects. The default implementation throws NoSuchMethodError. This can be overridden to customize the handling of references to undefined object members.

The following code demonstrates the data available inside a noSuchMethod

class Demo {

noSuchMethod(Invocation invocation) {
var name = invocation.memberName;
if (invocation.isAccessor) {
if (invocation.isGetter) print('$name is a getter');
if (invocation.isSetter) print('$name is a setter');
}
if (invocation.isMethod) {
print('$name is a method.');

// Type arguments are generic parameters between angle brackets.
var typeArgs = invocation.typeArguments;
if (typeArgs.isNotEmpty) print('type arguments are ${typeArgs}');

var posArgs = invocation.positionalArguments;
if (posArgs.isNotEmpty) print('positional arguments are ${posArgs}');

var namedArgs = invocation.namedArguments;
if (namedArgs.isNotEmpty) print('named arguments are ${namedArgs}');
}
}
}

main() {
dynamic demo = Demo(); // type must be dynamic

print(demo.one);
// Output is:
// Symbol("one") is a getter
// null

demo.two = 'test';
// Output is:
// Symbol("two=") is a setter

demo.three('a1', 'a2', a3: true, a4: 4);
// Output is:
// Symbol("three") is a method.
// positional arguments are [a1, a2]
// named arguments are {Symbol("a3"): true, Symbol("a4"): 4}

demo.four<bool, int>(1, 2, 3);
// Output is:
// Symbol("four") is a method.
// type arguments are [bool, int]
// positional arguments are [1, 2, 3]
}

One use of noSuchMethod is implementing domain specific languages (DSL). The following code demonstrates a basic XML builder that can be used to generate HTML.

// Creates an indented String from lines of XML.
String formatLines(List lines) {
var level = 0;
var s = '';
for (var l in lines) {
var line = l as String;
var isEndTag = line.startsWith('</');
if (isEndTag) level--;
s += (' ' * level) + line + '\n';
var isStartTag = !isEndTag &&
line.startsWith('<') &&
line.endsWith('>') &&
!line.endsWith('/>');
if (isStartTag) level++;
}
return s;
}

// Extracts the name of a Symbol.
// symbol.toString() returns 'Symbol("some-name")'.
String symbolName(Symbol symbol) {
var name = symbol.toString().substring(8);
return name.substring(0, name.lastIndexOf('"'));
}

class XMLBuilder {
// Returns a List<String> representing lines of XML.

dynamic noSuchMethod(Invocation invocation) {
// The member name is the name of an element to create.
var name = symbolName(invocation.memberName);

// Only method invocations are supported.
if (!invocation.isMethod) throw 'unsupported accessor $name';

var tag = '<$name';
var lines = [];

// Positional arguments represent child elements and text content.
for (var arg in invocation.positionalArguments) {
if (arg is List) {
lines.addAll(arg);
} else {
lines.add(arg);
}
}

// Named arguments represent attributes of the current element.
invocation.namedArguments.forEach((key, value) {
tag += ' ${symbolName(key)}="$value"';
});

if (invocation.positionalArguments.isEmpty) { // no children
lines.add(tag + ' />'); // closes element in shorthand way
} else {
// Children have already been added to lines.
lines.insert(0, tag + '>'); // adds start tag before children
lines.add('</$name>'); // adds end tag after children
}

return lines;
}
}

main() {
// This must be declared dynamic in order for noSuchMethod to work.
dynamic b = XMLBuilder();

// Dart requires named arguments to follow positional arguments.
// This uses named arguments to describe XML attributes
// and positional arguments to represent child elements and text content.
// So unfortunately XML attributes must be described
// after child elements and text content.
var lines = b.html(
b.head(
b.title("My Title"),
b.link(href: 'my-styles.css'),
),
b.body(
b.h1('My Header'),
b.p('Hello, World!', style: 'color: red'),
),
);
print(formatLines(lines));
}

The code above outputs the following:

<html>
<head>
<title>My Title</title>
<link href="my-styles.css" />
</head>
<body>
<h1>My Header</h1>
<p style="color: red">Hello, World!</p>
</body>
</html>

Cascade Operator

The cascade operator (..) is used to set multiple properties of an object or call multiple methods on an object. In the case of calling methods, it is useful when the methods do not return the receiver, making method chaining impossible.

The following examples demonstrate the cascade operator:

var left = 20;
var top = 10;
var width = 100;
var height = 50;
var r = MutableRectangle(left, top, width, height);
print(r); // Rectangle (20, 10) 100 x 50

r
..left = 30
..top = 0
..width = 200
.. height = 30;
print(r); // Rectangle (30, 0) 200 x 30

var numbers = [7, 3, 9, 2];
numbers
..sort() // [2, 3, 7, 9]
..add(10) // [2, 3, 7, 9, 10]
..insert(0, 1) // [1, 2, 3, 7, 9, 10]
..removeWhere((n) => n % 2 == 0); // [1, 3, 7, 9]
print(numbers); // [1, 3, 7, 9]

The null-aware cascade operator ?.. evaluates to null if the left side is null, avoiding calling a method on null. It should only be used for the first cascade in a series of them. The following example applies the null-aware cascade operator to the result of calling the getSquare function which can return null:

import 'dart:math';

MutableRectangle? getSquare(double size) {
if (size <= 0) return null;
return MutableRectangle(0, 0, size, size);
}

void main() {
var square = getSquare(5)
?..width *= 2 // double the width
..height *= 2; // double the height
print(square); // Rectangle (0, 0) 10 x 10

square = getSquare(0)
?..width *= 2 // won't run when square is null
..height *= 2; // won't run when square is null
print(square); // null
}

Libraries

Every .dart file defines a "library". Typically these are placed in the lib directory of a project. They can be placed at the top of the lib directory or in subdirectories nested at any depth below the lib directory.

All names defined in a library, including private ones, are accessible throughout the library.

import Statement

Libraries are imported into other .dart files with an import statement. For example, suppose a project in a directory named my_project defines a library in the file lib/math/geometry.dart. This library can be imported by the file bin/my_project.dart with the following:

import 'package:my_project/math/geometry.dart';

This makes all the non-private names defined in geometry.dart available in my_project.dart.

To avoid name conflicts with names defined in the importing file with those in the imported file, the import can be changed to the following:

import 'package:my_project/math/geometry.dart' as geometry;

With this change the names defined in geometry.dart must be referred to with a geometry. prefix.

The following code is example content from lib/math/geometry.dart:

import 'dart:math';

class Circle {
double radius;

Circle({required this.radius});

double get area => pi * pow(radius, 2);
}

The following code is example content of bin/my_project.dart:

import 'package:my_project/math/geometry.dart' as geometry;

void main() {
var c = geometry.Circle(radius: 5);
print(c.area); // 78.5...
}

The Dart analyzer will issue a warning if an import statement uses a relative file path (starting with ./ or ../) to navigate from files outside the lib directory to files inside it. Instead import paths for files under the lib directory should begin with package:{project_name}/ as shown above.

Relative file paths in import statements from files inside the lib directory to files outside it are not supported. But relative file paths can be used in import statements between files that are both under the lib directory or both outside it.

Libraries Defined By Multiple Files

Often a library is defined by a single source file, but libraries can be defined by multiple files. There are two ways to do this. One approach involves the export statement. The other involves the part and part of statements.

export Statement

For example, a math library can be divided into the files math.dart, algebra.dart, geometry.dart, and trigonometry.dart. The math.dart file can contain the following export statements:

export 'algebra.dart';
export 'geometry.dart';
export 'trigonometry.dart';

Importing math.dart provides access to the names it defines and also the names defined the files that it exports. The exported files can also be imported individually in source files that do not need access to names defined in all the exported files. But the names in the exported files cannot be used inside the file that exports them.

part and part of Statements

Using the same example, the math.dart file can contain the following part statements instead of export statements:

part 'algebra.dart';
part 'geometry.dart';
part 'trigonometry.dart';

The algebra.dart, geometry.dart, and trigonometry.dart files must contain the following part of statement referring back to math.dart:

part of 'math.dart';

All import statements needed by these four .dart files must appear in file containing the part statements (main.dart in this case) and they must appear before the part statements. Files containing part of statements cannot contain import statements.

As with using export statements, importing math.dart provides access to the names it defines and also the names defined in its part files. Files containing part of statements cannot be imported. Files containing part statements can use the names defined in the files they reference.

File I/O

The dart:io library supports all kinds of input and output, including reading and writing files.

The following code demonstrates reading a file into a List of lines. The entire file is read into memory, so this isn't applicable for large files.

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

void main() async {
var file = File('BeverlyHillbillies.txt');
var lines = await file.readAsLines();
for (var line in lines) {
print(line);
}
}

The openRead method can be used to read a file that is too large to fit in memory. The following code demonstrates this:

import 'dart:convert'; // for LineSplitter
import 'dart:io'; // for File

void main() {
var stream = File('BeverlyHillbillies.txt')
.openRead()
.transform(utf8.decoder)
.transform(LineSplitter());
stream.listen((line) {
print(line);
});
}

The `openWrite method can be used to write to a file.

import 'dart:io'; // for File

void main() {
// By default this overwrites the file if it exists.
var sink = File('my-file.txt').openWrite();
sink.writeln('1st line');
sink.writeln('2nd line');
sink.close();
}

Asynchronous Programming

The dart:async library provides many classes that support asynchronous programming. These include Future, Stream, StreamSubscription, and Timer.

In addition, the dart:isolate library provides classes that support running code in a new thread. These include Isolate, ReceivePort, and SendPort.

Each of these classes are described in the following sections.

Future Class

The dart:async library defines the Future class. A Future represents the result of code that will run in the future inside the current thread.

A Dart Future is similar to a JavaScript Promise. Terminology differs slightly. JavaScript promises are said to either "resolve" or "reject". Dart futures are said to either "complete with a value" or "complete with an error".

Execution of these is managed by the event loop described in the next section. Asynchronous tasks are placed on either the event queue or the microtask queue. Tasks on the microtask queue have a higher priority.

The Future class defines many named constructors.

To execute code after a given Duration, create a Future with the Future.delayed constructor. For example:

main() {
print('first');
Future.delayed(const Duration(seconds: 1), () {
print('third');
});
print('second');
}

Functions can return a Future. Calling code can wait from the Future to succeed or fail. There are two approaches that can be used. One approach is to use the then and catchError methods. The other approach is to use the async and await keywords which requires importing the dart:async library.

The following code demonstrates using then and catchError.

Future<int> getFutureScore(int player) {
return Future<int>.delayed(const Duration(seconds: 1), () {
if (player == 1) return 7;
throw 'unknown player $player';
});
}

void main() {
getFutureScore(1) // throws if argument isn't 1
// The function passed to "then" is
// referred to as a "completion handler".
.then((int score) {
print('score = $score');
})
.catchError((error) {
print('error = $error');
});
}

The following code demonstrates using the async and await keywords. These work similarly to the same keywords in JavaScript. They allow asynchronous code to read more like synchronous code, including the ability to use try/catch to handle errors thrown in a Future. The await keyword can only be used inside functions marked async. Such functions always return a Future even if one isn't explicitly created.

import 'dart:async';

Future<int> getFutureScore(int player) {
return Future<int>.delayed(const Duration(seconds: 1), () {
if (player == 1) return 7;
throw 'unknown player $player';
});
}

// If the delay in the previous function isn't needed, it can be
// rewritten as follows. Note the addition of the async keyword.
/*
Future<int> getFutureScore(int player) async {
if (player == 1) return 7;
throw 'unknown player $player';
}
*/


// The placement of the "async" keyword differs from JavaScript.
// Instead of at the beginning of a function definition,
// it belongs after the parameter list and before the body.
void main() async {
try {
int score = await getFutureScore(1); // throws if argument isn't 1
print('score = $score');
} catch (e) {
print('error = $e');
}
}

The following table summarizes the Future class constructors. Each of these place a Future on either the event or microtask queue that cannot be evaluated until after the current function completes.

ConstructorQueueDescription
Futureeventcreates a Future that can run as soon as the current function completes
Future.delayedeventsimilar to 1st constructor, but must wait for at least the specified Duration
Future.errormicrotaskcreates a Future that fails with a given error
Future.microtaskmicrotasksame as 1st constructor, but uses the microtask queue
Future.syncmicrotaskruns function immediately and places result (success or failure) in a Future
Future.valuemicrotaskcreates a Future that succeeds with a given value

Future.value is similar to the JavaScript Promise.resolve method.

Future.error is similar to the JavaScript Promise.reject method.

The Future class provides the following instance methods:

MethodDescription
asStream()returns Stream<T> emitting the single result
catchError(onErrorFn, [testFn])returns Future<T>; asynchronous equivalent of a catch block; if testFn is supplied, only handles matching errors
then(onValueFn, [onErrorFn])returns Future<T> and registers callbacks for success and optionally error
timeout(Duration timeLimit, {onTimeout()?}returns Future<T> that acts like receiver, but calls onTimeout if timeLimit passes before completing
whenComplete(actionFn)returns Future<T>; asynchronous equivalent of a finally block

The Future class provides the following static methods:

MethodDescription
any(Iterable<Future> futures)returns Future<T> that completes with result of first Future that completes
doWhile(FutureOr<bool> action())calls action repeatedly until it returns false
forEach(Iterable<T> elements, FutureOr action(T element))returns Future that completes with null after action is called on each element; results not saved
wait(Iterable<Future> futures, {bool eagerError=false, cleanup)returns Future<List<T>> that completes with results of multiple Futures after all complete

Future.any is similar to the JavaScript Promise.any and Promise.race methods.

Future.wait is similar to the JavaScript Promise.all and Promise.allSettled methods.

The following code demonstrates the use of each of these constructors and explains the order in which the Future objects will be evaluated. When each Future either succeeds or fails, its value is printed. Each line is followed by a comment that describes the output so far (O) and the contents of the event (E) and microtask (M) queues.

void main() {
// O: empty, E: empty, M: empty

print(1);
// O: 1, E: empty, M: empty

Future(() => 7).then(print);
// O: 1, E: 7, M: empty

Future.delayed(const Duration(seconds: 1), () => 8).then(print);
// O: 1, E: D8 7, M: empty; D8 here means the value 8 is delayed.

Future.error(3).catchError(print); // treating 3 as an error value
// O: 1, E: D8 7, M: 3

Future.microtask(() => 4).then(print);
// O: 1, E: D8 7, M: 4 3
// The previous line could be replaced by
// scheduleMicrotask(() => print(4));
// which requires importing "dart:async".

Future.sync(() => 5).then(print);
// O: 1, E: D8 7, M: 5 4 3

Future.value(6).then(print);
// O: 1, E: D8 7, M: 6 5 4 3

print(2);
// O: 1 2, E: D8 7, M: 6 5 4 3

// Now all the Futures in the microtask queue can be processed.
// O: 1 2 3, E: D8 7, M: 6 5 4
// O: 1 2 3 4, E: D8 7, M: 6 5
// O: 1 2 3 4 5, E: D8 7, M: 6
// O: 1 2 3 4 5 6, E: D8 7, M: empty

// Now all the Futures in the event queue can be processed.
// O: 1 2 3 4 5 6 7, E: D8, M: empty
// O: 1 2 3 4 5 6 7 8, E: empty, M: empty
}

Flutter provides the FutureBuilder class which builds widgets from a single value returned by a Future.

Completer Class

The dart:async library defines the Completer class. It is used to create and manage a Future.

Instances of Completer have the properties future and isCompleted. They also have the methods complete(value) and completeError(error).

The following code demonstates using a Completer to implement a function used in a Flutter app for confirming an action such as deleting data.

Future<bool> confirm(BuildContext context, String question) {
final completer = Completer<bool>();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(question),
content: Column(
children: [
Row(
children: [
ElevatedButton(
child: Text('Cancel'),
onPressed: () {
completer.complete(false);
Navigator.pop(context);
},
),
ElevatedButton(
child: Text('OK'),
onPressed: () {
completer.complete(true);
Navigator.pop(context);
},
),
],
mainAxisAlignment: MainAxisAlignment.center,
)
],
),
),
);
return completer.future;
}

Event Loop

The Dart event loop is responsible for executing function calls placed on the event and microtask queues. Asynchronous functions such as those passed to a constructor of the Future class are placed at the end of one of these queues. When the current function finishes executing, the event loop selects the next function to execute. If the microtask queue is not empty, the function at its beginning is selected (FIFO order). Otherwise the function at the beginning of the event queue is selected (also FIFO order). When a function starts executing, all other functions must wait until it completes. When the function completes, if there are no more functions in the queues then the program exits.

Streams

The dart:async library defines the Stream class. A Dart Stream is like a list of Future objects. It delivers zero or more values and errors over time.

The table below distinguishes four kinds of values in Dart:

SynchronousAsynchronous
single valueTFuture<T>
multiple valuesIterator<T>Stream<T>

A Stream can specify functions to call when:

Some provided library functions, such as the openRead method of the File class, return a Stream object.

There are two kinds of streams, single subscription and broadcast. Single subscription streams can only have one listener and an error occurs if an attempt is made to add more. Broadcast streams can have any number of listeners. A broadcast stream can be created from a single subscription stream by calling its asBroadcastStream method.

If a Stream generates data and there are no listeners, the data is lost. It is not cached for delivering later.

The Stream class support the following constructors for creating an instance:

ConstructorDescription
Stream()probably not used directly
Stream.empty()creates empty broadcast stream
Stream.error(Object error)creates stream that emits a single error
Stream.eventTransformed(Stream source, sinkFn)creates stream of transformed events from another stream
Stream.fromFuture(Future<T> future)creates stream of single value from a Future
Stream.fromFutures(Iterable<Future<T>> futures)creates stream of values from Iterable of Futures
Stream.fromIterable(Iterable<T> elements)creates stream of Iterable elements
Stream.multi(onListenFn)advanced; see docs
Stream.periodic(Duration period, [computationFn])creates stream that emits events at a given time interval
Stream.value()creates stream that emits a single value

The Stream class has the following instance properties, all of which are read-only:

PropertyDescription
firstfirst element
isBroadcastbool indicating whether this is a broadcast stream
isEmptyFuture<bool> indicating whether there are zero elements
lastlast element
lengthFuture<int> number of elements
singleFuture<T> single value; error if zero or more than one element

The Stream class support the following instance methods:

MethodDescription
any(testFn)returns Future<bool> indicating if any element passes a test
asBroadcastStream()returns a broadcast stream that emits the same elements
asyncExpand(convertFn)returns stream whose elements come from streams returned by calling convertFn on each element
asyncMap(convertFn)returns stream whose elements come from calling convertFn on each element
cast<R>()returns stream where each element is cast to type R
contains(Object? value)returns Future<bool> indicating if any element is == to value
distinct([bool equals(T previous, T next)])returns stream of elements not equal to previous using == or provided equals function
drain()discards remaining elements in stream
elementAt(int index)returns Future<T> value at given index
every(testFn)returns Future<bool> indicating if every element passes a test
expand(convertFn)returns stream of values in Iterables returned by calling convertFn on each element
firstWhere(testFn)returns Future<T> first element that passes a test
fold<S>(S initialValue, combineFn)returns Future<S> obtained by combining all elements (like Iterable fold)
forEach(actionFn)returns Future with no value after calling actionFn on each element
handleError(onErrorFn, testFn)returns stream that can ignore or transform errors that pass a test
join([String separator = ""])returns Future<String> formed by concatenating all the lements with an optional separator
lastWhere(testFn)returns Future<T> last element that passes a test
listen(onDataFn, {onError, onDone, cancelOnError})adds a listener and returns StreamSubscription<T>; cancelOnError defaults to true
map(convertFn)returns stream formed by calling convertFn on each element (like Iterable map)
pipe(streamConsumer)pipes all elements into a StreamConsumer and returns Future result value
reduce(combineFn)returns Future obtained by combining all elements (like Iterable reduce)
singleWhere(testFn)returns stream that only contains the first element that passes a test
skip(int count)returns stream that begins after first count elements
skipWhile(testFn)returns stream that begins at first element that does not pass a test
take(int count)returns stream containing the first count elements
takeWhile(testFn)returns stream containing all initial elements that pass a test
timeout(Duration timeLimit, onTimeoutFn)returns stream that emits same elements; if timeLimit passes after last emitted element, onTimeoutFn can generate more
toList()returns Future<List<T>> containing all the elements
toSet()returns Future<Set<T>> containing all the elements
transform(streamTransformer)returns stream created by a StreamTransformer which transforms the entire stream, not necessarily one element at a time
where(testFn)returns stream containing only elements that pass a test

The StreamSubscription object returned by the Stream listen method has the instance property isPaused and the instance methods cancel, onData, onDone, onError, pause, and resume.

The following code demonstrates creating and processing a stream:

import 'dart:async';

void main() {
// "late" is needed here so it can be used inside the callback below.
late StreamSubscription sub;

// The value of "tick" starts at zero and increments by one.
var stream = Stream.periodic(Duration(seconds: 1), (int tick) {
if (tick == 3) throw 'rejecting $tick';
if (tick == 6) sub.cancel();
return tick * 100;
});

// Wrap the periodic stream in one that handles errors.
stream = stream.handleError((error) {
print('got error "$error"');
});

sub = stream.listen(
(element) { print(element); },
onError: (error) { print('got error $error'); }
);
}

The output from the code above is:

0
100
200
got error rejecting 3
400
500

Another way to build a Stream is to create a StreamController which holds a Stream in one of its properties.

The StreamController class support the following constructors:

ConstructorDescription
StreamController()for a single subscription stream
StreamController.broadcast()for a broadcast stream

The StreamController class has the following instance properties:

PropertyDescription
doneread-only bool that indicates no more elements will be sent
hasListenerread-only bool that indicates whether there is a listener
isClosedread-only bool that indicates no more elements can be added
isPausedread-only bool that indicates elements cannot currently be added
onCancelfunction called when the stream is canceled
onListenfunction called when a listener is registered
onPausefunction called when the stream is paused
onResumefunction called when the stream is resumed
sinka StreamSink that has add, addError, and close methods
streamthe Stream being controlled

The StreamController class support the following instance methods:

MethodDescription
add(T element)adds an element to the stream being controlled
addError(Object error)adds an error to the stream being controlled
close()closes the stream being controlled

The following code demonstrates another way to implement the previous example. The only differences in the output are that it doesn't begin with zero and it outputs "done" at the end.

import 'dart:async';

void main() {
var controller = StreamController<int>();

Timer.periodic(Duration(seconds: 1), (Timer t) {
var value = t.tick;
switch (value) {
case 3:
controller.sink.addError('rejecting $value');
break;
case 6:
t.cancel();
controller.close();
break;
default:
controller.sink.add(value * 100);
}
});

controller.stream.listen( // returns a StreamSubscription
(element) {
print(element);
},
onDone: () {
print('done');
},
onError: (error) {
print('got error "$error"');
},
);
}

The call to the listen method above can be replaced by the following for loop which exits when the stream is closed.

  // "handleError" returns a new Stream where
// errors are handled by a supplied function.
var stream = controller.stream.handleError(
(error) { print('got error "$error"'); }
);
await for (final value in stream) {
print(value);
}

The for loop above can be replaced by a call to the Stream forEach method.

  await stream.forEach(print);

Flutter provides the StreamBuilder class which builds widgets from data in a Stream.

For more advanced stream processing, see the pub.dev packages async and rxdart. These provides classes that can cache elements, memoized elements, merge streams, and more.

Isolates

All Dart code runs in an "isolate" which is described by the Isolate class defined by the dart:isolate library. The main function of a Dart program and everything it invokes runs in the main isolate which is provided by Dart.

Additional isolates can be created to run code in new threads. This is useful for computationally intensive tasks.

Each isolate is executed in a single thread and has its own memory and event loop.

The dart:isolate library cannot be used in Dart applications that are compiled to JavaScript. This means it cannot be used inside DartPad.

Isolates can only communicate by sending messages. Each isolate can create multiple ReceivePort objects. Each ReceivePort object has a corresponding SendPort object that can be accessed through the sendPort property of the ReceivePort. To send a message, call the send method on a SendPort object. To receive these messages, call the listen method on the corresponding ReceivePort object.

Each new isolate is given a function to execute. An isolate is terminated and removed when this function exits or when another isolate calls its kill method. An isolate stops running temporarily when another isolate calls its pause method.

The Isolate class support the following class methods:

MethodDescription
exit()terminates the current isolate
spawn()creates a new isolate that runs a given function
spawnUri(Uri uri)creates a new isolate that runs code from a library at the URI

Isolate objects support the following instance methods:

MethodDescription
addErrorListener(SendPort port)requests for uncaught errors to be sent to port
addOnExitListener(SendPort port)requests for a message to be sent to port when the isolate terminates
kill()requests the isolate to terminate
pause()requests the isolate to pause execution until resume is called
ping(SendPort port)requests the isolate to send a message to port to verify it is running
removeErrorListener(SendPort port)stops listening for uncaught error messages
removeOnExitListener(SendPort port)stops listening for an exit message
resume()resumes execution after a call to pause
setErrorsFatal(bool fatal)sets whether uncaught errors should terminate the isolate (defaults to true)

The following code demonstrates creating a new Isolate to call a REST service and compute a value based on what it returns. This avoids blocking the event loop of the main Isolate which allows a Flutter UI to remain responsive while waiting for data to return.

import 'dart:convert'; // for jsonDecode
import 'dart:isolate';
import 'package:http/http.dart' as http;

// This Dart function doesn't know anything about the Isolate
// in which it runs. In order for it to communicate back to
// the Isolate that spawned it, it is passed a SendPort.
void getAverageSalary(SendPort sendPort) async {
// This is a free, public REST service that returns
// an array of objects that describe employees.
// Each contains an "employee_salary" property.
var restUrl = 'http://dummy.restapiexample.com/api/v1/employees';

// http.get returns a Future, but it runs in the current thread.
// Calling this in a new Isolate allows it to run in another thread
// and avoid blocking the event loop of the main Isolate.
var response = await http.get(Uri.parse(restUrl));

var status = response.statusCode;
if (status == 200) {
try {
var employees = jsonDecode(response.body)['data'];
var total = employees.fold(0, (acc, e) {
var salary = e['employee_salary'] as int;
return acc + salary;
});
sendPort.send(total / employees.length);
} catch (e) {
throw e;
}
} else {
// When this REST service returns a 429 status, the body is HTML.
// There's no easy way to extract a message from it,
// but the title element contains "Too Many Requests".
throw status == 429 ? 'too many requests' : 'bad status $status';
}
}

void main() async {
// This receives a successful result from the Isolate spawned below.
var successPort = ReceivePort();

// Isolate.spawn creates and runs a new Isolate
// 1st argument is a function to run in the new Isolate.
// 2nd argument is an argument to pass to the specified function.
var myIsolate = await Isolate.spawn(getAverageSalary, successPort.sendPort);
// There many methods that can be called on myIsolate,
// but only addErrorListener is used here.

// This receives an error from the Isolate spawned above.
var errorPort = ReceivePort();
myIsolate.addErrorListener(errorPort.sendPort);
errorPort.listen((error) {
var message = error[0];
print('got error: $message');

successPort.close();
errorPort.close();
});

// Some Isolate examples show this approach of
// waiting for the first value to arrive on the successPort stream.
// But we can't use this approach because if an error is received above,
// we need to close successPort in order to exit the program.
// Doing that would cause an error in this commented out line.
//var averageSalary = await successPort.first;

successPort.listen((averageSalary) {
print('average salary is ${averageSalary.toStringAsFixed(2)}');
successPort.close();
errorPort.close();
});
}

Flutter simplifies running code in a new thread by providing the compute function. This takes a function and data to be passed to it. It creates a new Isolate, runs the function in it, and returns a Future that provides the result when it succeeds. Use the await keyword inside an async function or the then method to get the result.

Tooling

Code Formatting

Formatting of Dart code is provided by the dart format command.

To get the best formatting, include a comma after every function argument, even the last one. This places each argument on a separate line.

Compiling

The dart compile {format} command compiles Dart code to other formats. The exe format creates an executable program for the current platform which can be Windows, macOS, or Linux. The js format compiles Dart code to JavaScript that includes an implementation of the Dart runtime.

Testing

Dart unit tests can be implementing using the test package. To install this in a Dart project, enter dart pub add test --dev.

Test source files should be placed in the test directory. The directory structure under test should mirror the directory structure under lib. There should be one test source file corresponding to each .dart file under the lib directory.

Each test source file name should match the name of the file for which it provides tests, but must end with _test.dart. And each must import the file that it tests.

Here is an example of code to be tested in the file lib/math.dart that is inside a project named demo.

double add(double n1, double n2) {
return n1 + n2;
}

Here is an example of test code in the file test/math_test.dart.

import 'package:demo/math.dart'; // demo is the project name
import 'package:test/test.dart';

void main() {
test('add works', () { // passing an anonymous function to test
expect(add(0, 0), 0);
expect(add(1, 0), 1);
expect(add(0, 1), 1);
expect(add(2, 3), 5);
});
}

The expect function takes an actual value and an expected value. It also accepts the optional named parameters reason and skip. The reason parameter is a String to be displayed when the actual and expected values do not match.

The expected value can be a matcher. This supports more complex validation such as the following:

expect(getFullName(user), allOf([
startsWith('R'),
contains('Mark'),
endsWith('Volkmann')
]));

If the actual and expected values in a call to expect do not match, it throws a TestFailure exception. This prevents other calls to expect in the same test function from being evaluated.

To test that an expression throws a specific kind of exception, use the throwsA function. For example:

void main() {
int intDivide(int n1, int n2) => n1 ~/ n2;

test('integer division works', () {
expect(intDivide(7, 2), 3);
expect(
() => intDivide(1, 0),
throwsA(isA<IntegerDivisionByZeroException>()),
);
});
}

To temporarily skip evaluating an expect, set the skip parameter to true or any String. When skip is set to true, expect will output "Skip expect: ({reason-or-expected-value})". When skip is set to a String, expect will output "Skip expect: {skip-value}". Note that the arguments to expect are evaluated regardless of the value of skip.

To skip running a test, pass the named parameter skip with a string message to the test function.

To skip running a group of tests, pass the named parameter skip with a string message to the group function.

To skip running all the tests in a source file, add the @Skip(message) annotation to the top of the file before import statements.

Suppose we want to test the function processOrder on orders that contain multiple items, one item, and zero items, and the function returns a bool indicating whether the order was successfully processed. Here are examples of expect calls with a skip value.

expect(processOrder(multipleOrder), true, skip: true);
// outputs "Skip expect: (true)" which isn't descriptive

expect(processOrder(singleOrder), true, reason: 'single order', skip: true);
// outputs "Skip expect: single order"

expect(processOrder(emptyOrder), true, skip: 'not implemented yet');
// outputs "Skip expect: not implemented yet"

The test function and group function (described below) also support a skip named parameter for skipping execution of a test or entire group of tests. It works the same way as does in the expect function. Unfortunately, because named parameters must follow positional ones, the skip parameter must appear after the body of the test or group which makes it difficult to spot when reading code.

To run all the tests in a project, enter dart test. This is a bit slow the first time it is run, but subsequent runs are much faster. If all of the tests pass, this will output "All tests passed!". Otherwise it will output the expected and actual results of the failed tests, followed by "Some tests failed."

To run tests from inside VS Code, click the beaker icon in the left nav to display the test panel. Then click one of the play buttons at the top (non-debug or debug). To run a single test, hover over it in the test panel to reveal play buttons and click one of them.

Tests can be grouped into suites using the group function which takes a name and a function that calls the test and group functions. For example:

import 'package:test/test.dart';
import 'dart:math';

void main() {
group('math', () {
group('trigonometry', () {
test('sin works', () {
expect(sin(pi / 2), 1);
});
});
});
}

Annoyances