Overview

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
- Dart home page
- Dart Language Specification
- Effective Dart - tips to "write consistent, robust, and fast code"
- Dart Complete Course - on Youtube by Flutterly/WCKD
- DartPad - web-based editor for experimenting with Dart
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:
"Provides tools for effectively editing, refactoring, running, and reloading Flutter mobile apps." For example, place the cursor over an undefined name, click the lightbulb icon or open the Command Palette, and select "Import library ...".
This extension provides many command palette entries including "Dart: Add Dependency" and "Dart: Add Dev Dependency". These prompt for a pub.dev library name, update the
pubspec.yamlfile, and download the library."Create dart data classes easily, fast and without writing boilerplate or running code generation."
To generate a constructor for a class definition that has
finalproperties and no constructor, place the cursor over one of the properties, press cmd-period, and select "Create constructor for final fields" from the context menu.To generate many methods for a class definition that has
finalproperties and no constructor, open the Command Palette and select "Dart Data Class Generator: Generate from class properties". This generates a constructor that takes named parameters, and the following methods:copyWith,fromJson,fromMap,hashCode,toMap,toString, andoperator ==."To easily add dependencies to your Dart and Flutter project's pubspec.yaml."
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:
This is a "substitute for Flutter's stock
DataTableandPaginatedDataTablewidgets with fixed header/sticky top row and other useful features missing in the originals.""Drift is a reactive persistence library for Flutter and Dart, built on top of sqlite."
"Functional programming in Dart and Flutter. All the main functional programming types and patterns fully documented, tested, and with examples."
"A composable, multi-platform, Future-based API for HTTP requests."
"Automatically generate code for converting to and from JSON by annotating Dart classes."
"Lightweight and blazing fast key-value database"
"Official Dart lint rules. Defines the 'core' and 'recommended' set of lints suggested by the Dart team."
Use of these lint rules is enabled by the following line found in theanalysis_options.yamlfile:include: package:lints/recommended.yamlEdit the
analysis_options.yamlfile to configure the use of specific rules. An older set of lint rules calledpedanticis deprecated. Another set of lint rules to consider using is Very Good Analysis.Here are some recommended rule configurations that include suppressing warnings about use of the
constkeyword:avoid_print: false
prefer_const_constructors: false
prefer_const_constructors_in_immutables: false
prefer_const_literals_to_create_immutables: false
prefer_single_quotes: trueThis is "a simple widget to add in the widget tree when you want to show nothing. Sometimes, according to a condition, we want to display nothing. Usually when we can't return null, we would return something like const SizedBox()."
"SQLite plugin for Flutter."
This is a Flutter widget that displays progress through a set of wizard steps.
"Supabase is an open source Firebase alternative. We are a service to:"
- listen to database changes
- query your tables, including filtering, pagination, and deeply nested relationships (like GraphQL)
- create, update, and delete rows
- manage your users and their permissions
- interact with your database using a simple UI
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_worldTo 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:
.dart_tool/package_config.jsonThis file is used to resolve Dart package names to
.dartfiles.binThis directory contains
.dartsource files that are meant to be executed.bin/hello_world.dartThis Dart source file is the project starting point.
.gitignoreThis file lists directories and files that should not be added to a Git repository.
libThis directory is not provided, but should be created to hold
.dartfiles that define libraries of functions and classes used by other.dartfiles in the project.Files in the
bindirectory import files from here using the syntaximport 'package:{project-name}/{file-name}.dart';For files in subdirectories of thelibdirectory, add subdirectories after the project name. For example,import 'package:{project-name}/{subdir-name}/{file-name}.dart';.packagesThis file is deprecated.
analysis_options.yamlThis file configures Dart linting rules.
CHANGELOG.mdThis file describes versions of the project.
pubspec.yamlThis file describes project dependencies and more. It is similar to
package.jsonin Node.js projects. The properties that are optional are indicated in the list below.name: package name (can use underscores, but not hyphens)description: package descriptionversion: using semver major.minor.patch conventionhomepage: optional URLrepository: optional URLissue_tracker: optional URLdocumentation: optional URLpublish_to: information for publishing to pub.devenvironment: describes supported Dart versionsdependencies: list of packages needed at runtime; can omit if nonedev_dependencies: list of packages only needed for development; can omit if none
There are three supported syntaxes for specifying acceptable versions of a dependency.
- specific semver; ex.
1.2.3 - semver range; ex.
>=1.2.3 <2.0.0 - caret semver; ex.
^1.2.3(means same as above)
The caret semver syntax is preferred. It has a slightly different meaning when the major version is zero. For example,
^0.1.2is the same as>=0.1.2 <0.2.0.Most dependencies come from pub.dev, but it is also possible to install dependencies from other sources such a Git repositories and packages in the
packagesdirectory of the current project.pubspec.lockThis file records the exact versions of each installed dependency. It is similar to
package-lock.jsonin Node.js projects.When this file doesn't exist, running the
pub getcommand installs the versions of the dependencies specified inpubspec.yamland creates this file to record the installed versions.When this file exists, running the
pub getcommand installs the versions of the dependencies specified here.README.mdThis Markdown file describes the project.
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.
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.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.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.
- Single-line comments begin with
//. - Multi-line comments are delimited by
/*and*/. - Documentation comments begin with
///or are delimited by/**and*/. These are used to generate documentation from source files.
Variables
There are four kinds of variables in Dart.
- top-level, global variables declared outside any function or class
- local variables in a function or method of a class, including parameters
- static properties in a class
- instance properties in a class
There are three ways to declare variables.
With a type
For example,
int score;With the
varkeywordThe type is obtained through type inference. For example, these are equivalent:
int n = 19;
var n = 19;When a
vardeclaration is not initialized, it is treated likedynamicwhich is described next.With the
dynamickeywordThe 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 cTo 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:
void: means a value is never used- Null: represents not having a value
- bool: boolean value with literal values
trueandfalse - int: 64-bit integer
- double: 64-bit floating point number
- String: sequence of UTF-16 characters delimited by single or double quotes
- Symbol: represents an operator or identifier with literal syntax
#name Never: has no values
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:
List: ordered collection of values (see theListsection)Set: unordered collection of unique values (see theSetsection)Map: unordered collection of key/value pairs (see theMapsection)
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:
| Property | Description |
|---|---|
isFinite | bool indicating whether the number is finite |
isInfinite | bool indicating whether the number is infinite |
inNaN | bool indicating whether this is the Not-a-Number value |
isNegative | bool indicating whether the number is negative |
sign | -1, 0, or 1 |
The num class defines the following instance methods:
| Method | Description |
|---|---|
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:
| Method | Description |
|---|---|
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:
| Category | Operators |
|---|---|
| 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:
| Property | Description |
|---|---|
bitLength | minimum number of bits required to store |
isEven | bool indicating if even |
isOdd | bool indicating if odd |
The int class adds the following methods to those defined in the num class (some omitted):
| Method | Description |
|---|---|
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:
| Category | Operators |
|---|---|
| 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.:
| Constant | Description |
|---|---|
infinity | represents an infinite positive number |
maxFinite | largest positive floating point number in 64 bits |
minPositive | smallest positive floating point number in 64 bits |
nan | represents a value that is not a valid number |
negativeInfinity | represents 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:
| Class | Description |
|---|---|
MutableRectangle<T extends num> | represents a mutable rectangle that is axis-aligned (not rotated) |
Point<T extends num> | represents an immutable 2D point |
Random | provides 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:
| Constant | Description |
|---|---|
e | base of natural logarithms; 2.718... |
ln2 | natural log of 2; 0.693... |
ln10 | natural log of 10; 2.30... |
log2e | base 2 log of e; 1.44... |
log10e | base 10 log of e; 0.434... |
pi | PI; 3.14... |
sqrt1_2 | square root of 1/2; 0.707... |
sqrt2 | square root of 2; 1.41... |
The Dart math package defines the following functions:
| Function | Description |
|---|---|
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:
| Property | Description |
|---|---|
codeUnits | a List<int> of the characters (code points) |
isEmpty | bool indicating if empty |
isNotEmpty | bool indicating if not empty |
length | number of characters (code points) |
runes | Iterable over the characters (code points) |
The String class defines the following instance methods:
| Method | Description |
|---|---|
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.
| Conversion | Code |
|---|---|
int to String | i.toString() |
double to String | d.toString() |
double to String | d.toStringAsFixed(decimalPlaces) |
String to int | int.parse(s) |
String to double | double.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 inferredTo 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:
Efor ElementKfor KeyRfor Return typeTfor TypeVfor Value
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:
| Constructor | Description |
|---|---|
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:
| Property | Description |
|---|---|
first | first element |
hashCode | hash code |
isEmpty | bool indicating if there are no elements |
isNotEmpty | bool indicating if there is at least one element |
iterator | Iterator object used to iterate over elements |
last | last element |
length | number of elements |
single | if only one element, that element; otherwise throws |
The Iterable class provides the following methods (some omitted):
| Method | Description |
|---|---|
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:
| Property | Description |
|---|---|
reversed | an 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):
| Method | Description |
|---|---|
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:
| Operator | Description |
|---|---|
+ | 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):
| Method | Description |
|---|---|
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 pairsThe Map class provides the following properties:
| Property | Description |
|---|---|
entries | Iterable over MapEntry objects (have key and value properties) |
isEmpty | bool indicating if there are no key/value pairs |
isNotEmpty | bool indicating if there is at least one key/value pair |
keys | Iterable over keys |
length | number of key/value pairs |
values | Iterable over values |
The Map class provides the following methods (some omitted):
| Method | Description |
|---|---|
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.
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.000ZThere 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:
daysPerWeek(7)monthsPerYear(12)january(1)february(2)march(2)april(4)may(5)june(6)july(7)august(8)september(9)october(10)november(11)december(12)monday(1)tuesday(2)wednesday(3)thursday(4)friday(5)saturday(6)sunday(7)
The DateTime class defines the following properties:
| Property | Description |
|---|---|
day | day of month; 1 to 31 |
hour | 0 to 23 |
isUtc | bool |
millisecond | 0 to 999 |
millisecondsSinceEpoch | milliseconds since 1970-01-01T00:00:00Z |
minute | 0 to 59 |
month | 1 to 12 |
second | 0 to 59 |
timeZoneName | timezone abbreviation such as CST |
timeZoneOffset | difference from UTC (ex. -6:00:00.00-0.0) |
weekday | MONDAY (1) to SUNDAY (7) |
year | includes all digits (4 for current year) |
The DateTime class defines the following instance methods:
| Method | Description |
|---|---|
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:
hoursPerDaymicrosecondsPerDaymicrosecondsPerHourmicrosecondsPerMillisecondmicrosecondsPerMinutemicrosecondsPerSecondmillisecondsPerDaymillisecondsPerHourmillisecondsPerMinutemillisecondsPerSecondminutesPerDayminutesPerHoursecondsPerDaysecondsPerHoursecondsPerMinutezero
The Duration class defines the following properties:
| Property | Description |
|---|---|
inDays | int number of whole days |
inHours | int number of whole hours |
inMicroseconds | int number of whole microseconds |
inMilliseconds | int number of whole milliseconds |
inMinutes | int number of whole minutes |
inSeconds | int number of whole seconds |
isNegative | bool indicating if negative |
The Duration class defines the following instance methods:
| Method | Description |
|---|---|
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:
| Property | Description |
|---|---|
isCaseSensitive | bool indicating if matches are case-sensitive (defaults to true) |
isDotAll | bool indicating if periods should match line terminators (defaults to false) |
isMultiline | bool indicating if multiline matching will be performed (defaults to false) |
isUnicode | bool indicating if whether Unicode matching will be performed (defaults to false) |
pattern | regular expression as a String |
The RegExp class defines the following instance methods:
| Method | Description |
|---|---|
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:
| Property | Description |
|---|---|
end | index where the match ends plus 1 |
groupCount | number of captured groups |
input | entire string from which the match was found |
pattern | a Pattern object describing the matching pattern |
start | index where the match begins |
The RegExpMatch class defines the following instance methods:
| Method | Description |
|---|---|
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.
| Property | Description |
|---|---|
elapsed | elapsedTicks converted to a Duration |
elapsedMicroseconds | int microseconds (1e-6 second) |
elapsedMilliseconds | int milliseconds (1e-3 second) |
elapsedTicks | number clock ticks |
frequency | number of ticks in a second |
isRunning | bool 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:
| Method | Description |
|---|---|
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.
| Property | Description |
|---|---|
isEmpty | bool indicating whether length == 0 |
isNotEmpty | bool indicating whether length >= 0 |
length | number of code points |
The StringBuffer class defines the following instance methods:
| Method | Description |
|---|---|
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.
| Property | Description |
|---|---|
authority | same String value as host |
data | UriData object describing a data URI |
fragment | String hash portion without leading # |
hasAbsolutePath | bool indicating whether the URI has an absolute path |
hasAuthority | bool indicating whether the URI has an authority (or domain) part |
hasEmptyPath | bool indicating whether the URI is missing a path |
hasFragment | bool indicating whether the URI has a fragment (or hash) part |
hasPort | bool indicating whether the URI has a specified port |
hasQuery | bool indicating whether the URI has query parameters |
hasScheme | bool indicating whether the URI has a specified scheme such as "https" |
host | String domain |
isAbsolute | bool indicating whether the URI is absolute; false for relative |
origin | String scheme and host combined |
path | String path portion that follows host |
pathSegments | List<String> of path parts |
port | int port that optionally follows host |
query | String containing all query parameters |
queryParameters | Map<String, String> of all query parameters |
queryParametersAll | Map<String, List<String>> of all query parameters including duplicates |
scheme | String such as "http", "https", or "data" |
userInfo | String optional username and password that can precede host |
The Uri class defines the following instance methods:
| Method | Description |
|---|---|
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-hashSpread 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)); // 5Functions 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:
- class properties (a.k.a. fields) to hold data not associated with a specific instance
- instance properties (a.k.a. fields) to hold data associated with a specific instance
- constructors to create instances
- class methods to operate on class properties and possibly instances passed to them
- instance methods to operate on data in a specific instance
- operators to return data computed from an instance and possibly one other instance (for binary operators)
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.
- Use the
mixinkeyword (preferred). - Define a class with no constructor.
- 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:
- have a constructor that is used to create instances
- be used with the
extendskeyword to act as the superclass of another class
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:
| Property | Description |
|---|---|
hashCode | an int value generated from object properties |
runtimeType | a Type object describing the object type |
The Object class provides the following methods:
| Method | Description |
|---|---|
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:
| Property | Description |
|---|---|
isAccessor | bool indicating whether a getter or setter was called |
isGetter | bool indicating whether a getter was called |
isMethod | bool indicating whether a method was called |
isSetter | bool indicating whether a setter was called |
memberName | Symbol name of the invoked member |
namedArguments | Map of named arguments |
positionalArguments | List of positional arguments |
typeArguments | List 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.
| Constructor | Queue | Description |
|---|---|---|
Future | event | creates a Future that can run as soon as the current function completes |
Future.delayed | event | similar to 1st constructor, but must wait for at least the specified Duration |
Future.error | microtask | creates a Future that fails with a given error |
Future.microtask | microtask | same as 1st constructor, but uses the microtask queue |
Future.sync | microtask | runs function immediately and places result (success or failure) in a Future |
Future.value | microtask | creates 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:
| Method | Description |
|---|---|
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:
| Method | Description |
|---|---|
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:
| Synchronous | Asynchronous | |
|---|---|---|
| single value | T | Future<T> |
| multiple values | Iterator<T> | Stream<T> |
A Stream can specify functions to call when:
- data is ready (first positional argument to
listen) - an error occurs (
onErrornamed argument tolisten) - the
Streamcompletes (onDonenamed argument tolisten)
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:
| Constructor | Description |
|---|---|
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:
| Property | Description |
|---|---|
first | first element |
isBroadcast | bool indicating whether this is a broadcast stream |
isEmpty | Future<bool> indicating whether there are zero elements |
last | last element |
length | Future<int> number of elements |
single | Future<T> single value; error if zero or more than one element |
The Stream class support the following instance methods:
| Method | Description |
|---|---|
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
500Another 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:
| Constructor | Description |
|---|---|
StreamController() | for a single subscription stream |
StreamController.broadcast() | for a broadcast stream |
The StreamController class has the following instance properties:
| Property | Description |
|---|---|
done | read-only bool that indicates no more elements will be sent |
hasListener | read-only bool that indicates whether there is a listener |
isClosed | read-only bool that indicates no more elements can be added |
isPaused | read-only bool that indicates elements cannot currently be added |
onCancel | function called when the stream is canceled |
onListen | function called when a listener is registered |
onPause | function called when the stream is paused |
onResume | function called when the stream is resumed |
sink | a StreamSink that has add, addError, and close methods |
stream | the Stream being controlled |
The StreamController class support the following instance methods:
| Method | Description |
|---|---|
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:
| Method | Description |
|---|---|
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:
| Method | Description |
|---|---|
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
Dart Format should be able to indent lines nicely without requiring a trailing comma after the last argument to every function call.
It would be nice if Dart didn't require semi-colons at the end of statements.
Dart should support a shorthand for named parameters when there is an in-scope variable with the same name. For example, instead of
Person(name: name, birthday: birthday)we should be able to writePerson(name:, birthday:).Dart needs to support type inference of enum values the way Swift does. For example, instead of
color: Colors.red,I want to usecolor: .red,.Dart wants many constructor calls to be preceded by the
constkeyword. This makes the code verbose.