Dartâ„¢ is the language behind Flutter apps, and it runs just as well on the command
line, on servers and in the browser. It is null-safe by default, so a value cannot
be null unless its type says so, and since Dart 3 it has records, pattern matching
and sealed classes. The reference below is grouped by what you are trying to do, and
the filter box searches all of it at once. Type null and every null-aware operator
comes to you, or type 3.0 to see what arrived with Dart 3.
Every snippet is checked against Dart 3.13.4, the current stable release.
Anything that needs a version newer than 3.0 says so in the notes column. Names like
user and nums are placeholders for your own. The official Docker image runs a
file in one line, docker run --rm -v "$PWD":/app -w /app dart:stable dart run main.dart,
and the Docker cheat sheet has the rest. Coming
from another language? The Swift,
Ruby and PHP cheat
sheets are grouped the same way, so they read side by side.
Dart and the related logo are trademarks of Google LLC. We are not endorsed by or affiliated with Google LLC.
Searches the task, the command and the third column. Press / from anywhere on the page.
241 commands
Running Dart and pub
| Task | Command | Notes |
|---|---|---|
| Check which version you have | dart --version | |
| Run a file | dart run main.dart | dart main.dart works too |
| Start a command-line app | dart create -t console my_app | -t package for a library, -t cli for an app with argument parsing |
| Run the app in the current package | dart run | Runs bin/<package name>.dart |
| Run it with arguments | dart run bin/my_app.dart --name Ada | |
| Add a dependency | dart pub add http | Writes the latest compatible version into pubspec.yaml |
| Add a dev-only dependency | dart pub add dev:test | |
| Remove a dependency | dart pub remove http | |
| Download what pubspec.yaml lists | dart pub get | |
| See which packages are behind | dart pub outdated | |
| Upgrade, including major versions | dart pub upgrade --major-versions | Leave out the flag to stay within your ranges |
| Run the tests | dart test | Needs the test package as a dev dependency |
| Run the tests matching a name | dart test --name "falls back" | |
| Format every file | dart format . | |
| Find errors and lint warnings | dart analyze | |
| Apply the suggested fixes | dart fix --apply | --dry-run shows them first |
| Compile to a native binary | dart compile exe bin/my_app.dart -o my_app | |
| Compile for the browser | dart compile js web/main.dart -o main.js | dart compile wasm for WebAssembly |
There is no interactive shell in the SDK, so try things in a file with dart run, or in the browser at dartpad.dev. The Flutter SDK includes Dart, so if you have Flutter you already have the dart command.
Variables and null safety
Every type is non-nullable unless you add a question mark. A String can never be null, a String? can, and Dart will not let you use a String? as a String until you have checked it.
| Task | Code | Notes |
|---|---|---|
| Print a line | print('Hello'); | |
| Variable, type inferred | var name = 'Ada'; | Still a String. It cannot hold an int later |
| Set once, at run time | final city = readCity(); | Use final unless the value changes |
| Fixed at compile time | const maxUsers = 100; | Also makes deeply immutable lists and maps |
| Give the type explicitly | String name = 'Ada'; | |
| Declare now, assign before first read | late String label; | Reading it early throws at run time |
| Value that may be null | String? nickname; | Starts as null |
| Default when null | nickname ?? 'Anonymous' | |
| Assign only if null | nickname ??= 'kit'; | |
| Reach through nullable values | user?.address?.city | The whole chain is null if any link is |
| Index a nullable list | items?[0] | |
| Assert it is not null | nickname! | Throws if it is. Only where null means a bug |
| Check, then use as non-null | if (nickname != null) print(nickname.length); | Works on locals and, from Dart 3.2, private final fields. Not on public fields |
| Readable big numbers | 1_000_000 | Dart 3.6+ |
| Whole-number division | 7 ~/ 2 | Gives 3. 7 / 2 gives 3.5, always a double |
| Remainder | 7 % 2 | -7 % 3 is 2. (-7).remainder(3) is -1 |
| String to number | int.parse('42') double.parse('3.5') | Throws FormatException on junk |
| String to number, or null | int.tryParse(input) | |
| Convert between number types | count.toDouble() 3.9.toInt() 3.5.round() | toInt truncates: 3.9 becomes 3 |
| Is it a type | value is String value is! int | |
| Cast | value as String | Throws if it is not one |
| What type is it | value.runtimeType | |
| Opt out of type checks | dynamic data = jsonDecode(body); | Object? is safer: you must check before use |
| Swap two variables | (a, b) = (b, a); | A record pattern, Dart 3.0+ |
| Comment | // one line /* several */ /// doc comment |
Types, records and enums
| Task | Code | Notes |
|---|---|---|
| Built-in types | int double num String bool | num is the parent of int and double |
| Record with named fields | final point = (x: 3, y: 4); | Then point.x. Records are Dart 3.0+ |
| Record with positional fields | final pair = (1, 'a'); | Then pair.$1 and pair.$2 |
| Return two values | (int, int) bounds(List<int> values) | Unpack with final (lo, hi) = bounds(nums); |
| Unpack named fields | final (:min, :max) = namedBounds(nums); | |
| Give a type a second name | typedef Json = Map<String, dynamic>; | |
| Function type | int Function(int, int) op = add; | |
| Generic class | class Box<T> { final T value; Box(this.value); } | |
| Define an enum | enum Direction { north, south, east, west } | |
| Enum case's name | Direction.north.name | Gives 'north' |
| Every case | Direction.values | |
| Case from its name | Direction.values.byName('south') | Throws for an unknown name |
| Enum with fields | enum Role { admin('Admin'), member('Member'); const Role(this.label); final String label; } | Then Role.admin.label |
| Leave out the type name | Direction heading = .south; | Dot shorthand, Dart 3.10+. Works wherever the type is known |
| Wrap a type at zero cost | extension type UserId(int value) {} | Dart 3.3+. A UserId cannot be passed where an int is expected |
| Type that never returns | Never fail(String m) => throw StateError(m); |
Strings
| Task | Code | Notes |
|---|---|---|
| Interpolate a variable | 'Hello, $name' | |
| Interpolate an expression | 'Hello, ${user.name}' | Braces for anything more than a name |
| Several lines | ''' ... ''' | Triple double quotes work too |
| Backslashes kept as typed | r'C:\path\new' | A raw string. No interpolation either |
| Join strings | first + ' ' + last | Or put literals side by side: 'a' 'b' |
| Build a long string | final sb = StringBuffer()..write('a')..write('b'); | Then sb.toString() |
| Length | s.length | In UTF-16 units: an emoji counts as 2 |
| Is it empty | s.isEmpty s.isNotEmpty | |
| Change case | s.toUpperCase() s.toLowerCase() | |
| Trim whitespace | s.trim() s.trimLeft() s.trimRight() | |
| Does it contain | s.contains('cat') | |
| Does it start or end with | url.startsWith('https') file.endsWith('.dart') | |
| Replace every match | s.replaceAll('cat', 'dog') | replaceFirst for just the first |
| Split into parts | csv.split(',') | Keeps empty parts: 'a,,b' gives [a, , b] |
| Join a list into a string | names.join(', ') | |
| Part of a string | s.substring(0, 5) | End index is excluded |
| Character at a position | s[0] | A one-character String |
| Position of a substring | s.indexOf('l') | -1 when missing |
| Pad to a width | '7'.padLeft(3, '0') | Gives 007 |
| Repeat | '-' * 20 | |
| Reverse | s.split('').reversed.join() | Fine for plain text. Breaks emoji |
| Number to string | 42.toString() '$price' | |
| Fixed decimal places | price.toStringAsFixed(2) | |
| Does it match a pattern | RegExp(r'\d+').hasMatch(s) | Use raw strings for patterns |
| Capture part of a match | RegExp(r'(\d{4})-(\d{2})').firstMatch(s)?.group(1) | group(0) is the whole match |
| Every match | RegExp(r'\d+').allMatches(s).map((m) => m[0]) | |
| Compare ignoring case | a.toLowerCase() == b.toLowerCase() |
Collections: lists
| Task | Code | Notes |
|---|---|---|
| Create a list | var nums = [1, 2, 3]; | |
| Empty list of a type | var names = <String>[]; | |
| Filled with one value | List.filled(5, 0) | |
| Built from its position | List.generate(5, (i) => i * i) | Gives [0, 1, 4, 9, 16] |
| Read by position | nums[0] | Throws RangeError if out of range |
| First or last | nums.first nums.last | Throw on an empty list |
| First or last, or null | nums.firstOrNull nums.lastOrNull | Dart 3.0+ |
| Append | nums.add(4); nums.addAll([5, 6]); | |
| Insert at a position | nums.insert(0, 0); | |
| Remove at a position | nums.removeAt(0) | Returns the removed item |
| Remove the last item | nums.removeLast() | |
| Remove a value | nums.remove(5) | First match only. Returns true if found |
| Remove every match | nums.removeWhere((n) => n < 0); | |
| How many, and is it empty | nums.length nums.isEmpty nums.isNotEmpty | |
| Does it contain | nums.contains(3) | |
| Position of a value | nums.indexOf(3) | -1 when missing |
| A slice | nums.sublist(1, 3) | A new list. take(2) and skip(1) are lazy |
| Sort in place | nums.sort(); | Returns void, not the list |
| Sort by a field | users.sort((a, b) => a.age.compareTo(b.age)); | |
| Sorted copy | final sorted = [...nums]..sort(); | |
| Transform every item | nums.map((n) => n * 2).toList() | map is lazy. toList() runs it |
| Keep items that pass a test | nums.where((n) => n.isEven).toList() | |
| Fold into one value | final total = nums.fold(0, (sum, n) => sum + n); | Assign it first, or write fold<int>. package:collection adds nums.sum |
| Smallest and largest | nums.reduce(min) nums.reduce(max) | import 'dart:math' |
| First item that passes a test | users.where((u) => u.isAdmin).firstOrNull | firstWhere throws when nothing matches |
| Do any or all pass | nums.any((n) => n > 3) nums.every((n) => n > 0) | |
| Loop with the position | for (final (i, name) in names.indexed) { } | Dart 3.0+ |
| Combine lists | [...a, ...b] | ...? skips a list that is null |
| Build with if and for | [if (isAdmin) 'Admin', for (final n in nums) 'n$n'] | |
| Add an item only if not null | ['Ada', ?nickname] | Null-aware element, Dart 3.8+ |
| Flatten nested lists | nested.expand((list) => list).toList() | |
| Reversed, shuffled | nums.reversed.toList() nums.shuffle(); | |
| Read-only list | List.unmodifiable(nums) const [1, 2] | Changing either throws UnsupportedError |
Collections: maps and sets
A Dart map keeps its keys in the order you added them, and so does a set. That makes looping predictable, unlike maps in Swift or Go.
| Task | Code | Notes |
|---|---|---|
| Create a map | var ages = {'Ada': 36, 'Alan': 41}; | |
| Empty map of a type | var cache = <String, int>{}; | |
| Read a value | ages['Ada'] | int?, null when the key is missing |
| Read with a default | ages['Bob'] ?? 0 | |
| Set a value | ages['Bob'] = 30; | |
| Remove a key | ages.remove('Bob') | Returns the old value, or null |
| Count how often each value appears | counts.update(word, (n) => n + 1, ifAbsent: () => 1); | |
| Set only if missing | cache.putIfAbsent(key, () => load(key)); | |
| Does a key exist | ages.containsKey('Ada') | |
| Just the keys or values | ages.keys ages.values | |
| Loop over pairs | for (final MapEntry(:key, :value) in ages.entries) { } | A pattern, Dart 3.0+. e.key and e.value also work |
| Change every value | ages.map((k, v) => MapEntry(k, v + 1)) | |
| Keep pairs that pass a test | Map.fromEntries(ages.entries.where((e) => e.value > 30)) | |
| Merge, right side wins | {...defaults, ...overrides} | |
| Index a list by a field | {for (final u in users) u.id: u} | |
| Group a list by a field | groupBy(words, (w) => w.length) | From package:collection |
| Create a set | var tags = {'dart', 'flutter'}; | |
| Empty set | var tags = <String>{}; | A bare {} is an empty map, not a set |
| Add and check | tags.add('server'); tags.contains('dart') | |
| Remove duplicates from a list | nums.toSet().toList() | Keeps the first of each, in order |
| Union, intersection, difference | a.union(b) a.intersection(b) a.difference(b) | |
| Contains every one of | a.containsAll(b) |
Control flow and patterns
| Task | Code | Notes |
|---|---|---|
| If, else if, else | if (a) { } else if (b) { } else { } | Brackets round the condition are required |
| Pick one of two values | final label = count == 1 ? 'item' : 'items'; | |
| Switch statement | switch (code) { case 200 || 201: ok(); case 404: missing(); default: fail(); } | No break needed, Dart 3.0+ |
| Switch expression | final text = switch (code) { 200 => 'OK', 404 => 'Not found', _ => 'Error' }; | Dart 3.0+. _ matches anything |
| Match a range | switch (age) { < 13 => 'child', < 18 => 'teen', _ => 'adult' } | |
| Match with a condition | case int n when n < 0: | |
| Match a record | switch ((x, y)) { case (0, 0): origin(); case (0, _): onYAxis(); } | |
| Check the shape of JSON | if (json case {'name': String name, 'age': int age}) { } | Binds name and age when every part matches |
| Match every subtype of a sealed class | switch (result) { Ok(:final value) => value, Err() => 0 } | No default needed, the compiler checks |
| Counting loop | for (var i = 0; i < 5; i++) { } | |
| Loop over items | for (final n in nums) { } | |
| Repeat n times | for (var i = 0; i < 3; i++) { } | There is no range literal |
| While loop | while (queue.isNotEmpty) { } | |
| Run the body at least once | do { } while (tries < 3); | |
| Skip or stop | continue; break; | |
| Break out of an outer loop | outer: for (final row in grid) { for (final cell in row) { break outer; } } | |
| Check an assumption | assert(age >= 0, 'negative age'); | Only runs with dart run --enable-asserts, and in tests and Flutter debug builds |
Functions
| Task | Code | Notes |
|---|---|---|
| Define a function | int add(int a, int b) { return a + b; } | |
| One-expression body | int add(int a, int b) => a + b; | |
| Optional positional parameter | String greet([String name = 'world']) | Call it as greet() or greet('Ada') |
| Named parameters | void connect({required String host, int port = 80}) | Call it as connect(host: 'localhost') |
| Entry point with arguments | void main(List<String> args) { } | |
| Anonymous function | (x) => x * x | Or (x) { return x * x; } for several lines |
| Store a function | final square = (int x) => x * x; | |
| Pass a function by name | names.forEach(print); | A tear-off: no brackets, so it is not called |
| Name a function type | typedef Compare<T> = int Function(T a, T b); | |
| Closure that keeps state | var total = 0; return () => ++total; | It captures total itself, not a copy |
| Generic function | T firstItem<T>(List<T> items) => items.first; | |
| Generic with a constraint | T maxOf<T extends Comparable<T>>(T a, T b) => a.compareTo(b) >= 0 ? a : b; | |
| Ignore parameters | (_, _) => 0 | Several _ allowed from Dart 3.7 |
| Call several methods on one object | final sb = StringBuffer()..write('a')..write('b'); | Cascade: each .. returns the object, not the result |
| Make an object callable | int call(int a, int b) => a + b; | Then adder(2, 3) |
Classes
| Task | Code | Notes |
|---|---|---|
| Define a class | class Point { final double x, y; Point(this.x, this.y); } | this.x in the parameter list sets the field |
| Create one | final p = Point(1, 2); | No new keyword needed |
| Named parameters in a constructor | User({required this.name, this.age = 0}); | |
| Second constructor | Point.origin() : x = 0, y = 0; | Code after the colon is the initializer list |
| Check arguments in the constructor | User(this.age) : assert(age >= 0); | |
| Constructor that can return a subtype or cached object | factory User.fromJson(Map<String, dynamic> json) => User(name: json['name']); | |
| Compile-time constant objects | const Point(this.x, this.y); | Needs final fields. const Point(1, 2) is created once |
| Getter | double get area => width * height; | |
| Setter | set celsius(double value) => _celsius = value; | |
| Private to the file | int _count = 0; | A leading underscore. There is no private keyword |
| Shared by the class, not each object | static const origin = Point(0, 0); | |
| Inherit | class Admin extends User | |
| Pass a parameter up to the parent | Admin({required super.name}); | |
| Replace a parent method | @override String describe() => 'Admin ${super.describe()}'; | |
| Class that cannot be created directly | abstract class Shape { double get area; } | |
| Promise to provide a type's members | class Square implements Shape | Any class works as an interface |
| Value equality | bool operator ==(Object other) => other is Point && other.x == x && other.y == y; | Override hashCode too: Object.hash(x, y) |
| Control what print shows | @override String toString() => 'Point($x, $y)'; | |
| Define an operator | Money operator +(Money other) => Money(pence + other.pence); |
Mixins, extensions and class modifiers
| Task | Code | Notes |
|---|---|---|
| Define a mixin | mixin Loggable { void log(String m) => print('[$runtimeType] $m'); } | |
| Use one or more mixins | class Order extends Model with Loggable, Timestamped | |
| Mixin only for certain classes | mixin Walker on Animal { } | It can call Animal's members |
| Class that is also a mixin | mixin class Musician { } | Dart 3.0+ |
| Add a method to an existing type | extension StringX on String { bool get isBlank => trim().isEmpty; } | Then ' '.isBlank |
| Extension only this file sees | extension on String { } | Leave out the name |
| Fixed set of subtypes | sealed class Result { } | Subtypes in the same file only. Switches over it must be exhaustive |
| Cannot be extended or implemented outside the file | final class Config { } | |
| Can be extended but not implemented outside | base class Model { } | |
| Can be implemented but not extended outside | interface class Repository { } |
The class modifiers arrived in Dart 3.0 and only restrict code in other libraries, which usually means other files. Inside the file that declares the class, you can still do anything.
Error handling
| Task | Code | Notes |
|---|---|---|
| Throw | throw FormatException('bad input'); | You can throw any non-null object, but stick to Exception and Error |
| Catch one type | try { } on FormatException catch (e) { print(e.message); } | |
| Catch anything, with the stack trace | catch (e, stackTrace) { } | |
| Always run afterwards | finally { file.close(); } | |
| Throw the caught error again | catch (e) { log(e); rethrow; } | Keeps the original stack trace |
| Your own exception | class LoginException implements Exception { final String message; LoginException(this.message); } | |
| Reject a bad argument | throw ArgumentError.value(age, 'age', 'must be positive'); | |
| Signal a broken state | throw StateError('already closed'); | |
| Parse without catching | int.tryParse(input) ?? 0 | |
| Code that should be unreachable | throw UnimplementedError(); |
An Exception is a failure the caller is expected to handle, such as bad input or a network error. An Error, such as RangeError or a failed null check, means a bug in the program: fix the code rather than catching it.
Async, await and streams
A Future is one value that arrives later. A Stream is a series of values that arrive over time, such as lines from a file, user events or messages from a socket.
| Task | Code | Notes |
|---|---|---|
| Async function | Future<User> fetchUser(int id) async { } | Returns a Future even though the body returns a User |
| Wait for it | final user = await fetchUser(1); | Only inside an async function |
| Async main | Future<void> main() async { } | |
| Pause | await Future.delayed(const Duration(seconds: 1)); | |
| Run several at once | final users = await Future.wait([fetchUser(1), fetchUser(2)]); | |
| Run two of different types at once | final (user, posts) = await (fetchUser(1), fetchPosts(1)).wait; | Dart 3.0+ |
| Give up after a time | await fetchUser(1).timeout(const Duration(seconds: 5)); | Throws TimeoutException from dart:async |
| Handle an async error | try { await fetchUser(1); } on HttpException catch (e) { } | |
| Start without waiting | unawaited(saveLog()); | From dart:async. Says the missing await is on purpose |
| Produce values over time | Stream<int> countTo(int n) async* { for (var i = 1; i <= n; i++) yield i; } | |
| Consume values over time | await for (final value in stream) { } | |
| React to each value | stream.listen((v) => print(v), onError: print, onDone: close); | Keep the subscription to cancel() it |
| Transform a stream | stream.where((n) => n.isOdd).map((n) => n * 10) | |
| Collect a stream | await stream.toList() await stream.first | |
| Push values in yourself | final controller = StreamController<int>(); | controller.add(1), then close(). Read controller.stream |
| Stream with several listeners | StreamController<int>.broadcast() | A plain stream allows one listener |
| Values on a timer | Stream.periodic(const Duration(seconds: 1), (i) => i).take(3) | |
| Wrap a callback API | final completer = Completer<String>(); | Call completer.complete(value), then await completer.future |
| Heavy work off the main isolate | final result = await Isolate.run(() => parse(bigJson)); | import 'dart:isolate'. Not on the web |
| Lazy sequence | Iterable<int> naturals() sync* { var n = 0; while (true) yield n++; } | Then naturals().take(3) |
Dart runs your code on one thread per isolate, with an event loop. await pauses the function, not the program, so other work carries on while a Future is pending. For CPU-heavy work that would freeze the loop, move it to another isolate.
Packages and imports
| Task | Code | Notes |
|---|---|---|
| Import a core library | import 'dart:math'; | Also dart:async, dart:convert, dart:io, dart:isolate |
| Import a package | import 'package:http/http.dart' as http; | Then http.get(...). The prefix avoids name clashes |
| Import your own package's code | import 'package:my_app/models.dart'; | Files under lib/ are importable this way |
| Import a file next to this one | import 'utils.dart'; | |
| Import only some names | import 'dart:math' show max, min; | hide leaves names out instead |
| Re-export from your library | export 'src/greeter.dart'; | |
| Pin the SDK version | environment: sdk: ^3.13.0 | In pubspec.yaml. Sets the language version too |
| Depend on a version range | http: ^1.6.0 | Caret means at least 1.6.0 and below 2.0.0 |
| Depend on a local folder | shared: path: ../shared | Nested YAML under dependencies |
Packages come from pub.dev. Commit pubspec.lock for an app so every machine gets the same versions, and leave it out for a package other people depend on.
A model, start to finish
Most of the class reference in one place: an enum with a field, a class with a
const constructor and a named one, an operator, Comparable, value equality and
toString.
enum Currency {
gbp('£'),
usd(r'$');
const Currency(this.symbol);
final String symbol;
}
class Money implements Comparable<Money> {
final int pence;
final Currency currency;
const Money(this.pence, [this.currency = Currency.gbp]);
Money.pounds(double pounds, [Currency currency = Currency.gbp])
: this((pounds * 100).round(), currency);
Money operator +(Money other) {
if (other.currency != currency) {
throw ArgumentError('Cannot add ${other.currency.name} to ${currency.name}');
}
return Money(pence + other.pence, currency);
}
@override
int compareTo(Money other) => pence.compareTo(other.pence);
@override
bool operator ==(Object other) =>
other is Money && other.pence == pence && other.currency == currency;
@override
int get hashCode => Object.hash(pence, currency);
@override
String toString() {
final sign = pence < 0 ? '-' : '';
final whole = pence.abs() ~/ 100;
final fraction = (pence.abs() % 100).toString().padLeft(2, '0');
return '$sign${currency.symbol}$whole.$fraction';
}
}
void main() {
const price = Money(1999);
final total = price + Money.pounds(5);
print(total); // £24.99
print([price, const Money(500)]..sort()); // [£5.00, £19.99]
print(total == Money(2499)); // true
try {
price + const Money(100, Currency.usd);
} on ArgumentError catch (e) {
print(e.message); // Cannot add usd to gbp
}
}Without the == and hashCode overrides, two Money objects with the same pence
would not be equal, because Dart compares objects by identity unless told otherwise.
The usd symbol is a raw string, r'$', because a bare $ in a string starts
interpolation. Money is held in pence as an int on purpose: a double cannot hold
most decimal fractions exactly, so 0.1 + 0.2 == 0.3 is false in Dart, as it is in
almost every language.
Sealed classes and exhaustive switches
A sealed class tells the compiler every subtype there will ever be, so a switch
over it needs no default and fails to compile when you add a new shape and forget
to handle it. Each case pulls the fields it needs straight out of the object.
sealed class Shape {}
class Circle extends Shape {
Circle(this.radius);
final double radius;
}
class Rectangle extends Shape {
Rectangle(this.width, this.height);
final double width, height;
}
class Triangle extends Shape {
Triangle(this.base, this.height);
final double base, height;
}
double area(Shape shape) => switch (shape) {
Circle(:final radius) => 3.14159 * radius * radius,
Rectangle(:final width, :final height) when width == height => width * width,
Rectangle(:final width, :final height) => width * height,
Triangle(:final base, :final height) => base * height / 2,
};
(double total, Shape largest) summarise(List<Shape> shapes) {
var total = 0.0;
var largest = shapes.first;
for (final shape in shapes) {
total += area(shape);
if (area(shape) > area(largest)) largest = shape;
}
return (total, largest);
}
void main() {
final shapes = [Circle(1), Rectangle(2, 3), Triangle(4, 5)];
final (total, largest) = summarise(shapes);
print(total.toStringAsFixed(2)); // 19.14
print(largest.runtimeType); // Triangle
}Circle(:final radius) is an object pattern: it matches a Circle and binds its
radius field to a local of the same name. summarise returns a record, so the
caller gets two values back without a wrapper class.
Mixins and extensions
A mixin is a bundle of fields and methods that any class can take with with,
without becoming its subclass. An extension adds methods to a type from the outside.
mixin Loggable {
final List<String> history = [];
void log(String event) => history.add('$runtimeType: $event');
}
mixin Discountable {
int get pence;
int discounted(int percent) => pence * (100 - percent) ~/ 100;
}
class Order with Loggable, Discountable {
Order(this.id, this.pence) {
log('created');
}
final int id;
@override
final int pence;
void ship() => log('shipped');
}
extension on Order {
bool get isNew => history.length == 1;
}
void main() {
final order = Order(42, 2000);
print(order.isNew); // true
order.ship();
print(order.history); // [Order: created, Order: shipped]
print(order.discounted(15)); // 1700
}Discountable declares an abstract pence getter, so any class that mixes it in
has to provide one. That is how a mixin asks for what it needs without extending
anything. The unnamed extension is visible only in this file, which suits a helper
that nobody else should rely on.
Decoding JSON
dart:convert turns a JSON string into maps and lists. A map pattern then checks the
shape and pulls out typed values in one step, instead of a cast on every field.
import 'dart:convert';
class Repo {
const Repo({
required this.id,
required this.fullName,
required this.stars,
this.description,
});
final int id;
final String fullName;
final int stars;
final String? description;
factory Repo.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'id': int id,
'full_name': String fullName,
'stargazers_count': int stars,
} =>
Repo(
id: id,
fullName: fullName,
stars: stars,
description: json['description'] as String?,
),
_ => throw FormatException('Not a repo: $json'),
};
}
Map<String, dynamic> toJson() => {
'id': id,
'full_name': fullName,
'stargazers_count': stars,
'description': description,
};
}
void main() {
const body = '''
{ "id": 1, "full_name": "dart-lang/sdk", "stargazers_count": 10500, "description": null }
''';
final repo = Repo.fromJson(jsonDecode(body) as Map<String, dynamic>);
print('${repo.fullName} ${repo.stars}'); // dart-lang/sdk 10500
print(const JsonEncoder.withIndent(' ').convert(repo));
try {
Repo.fromJson({'id': '1'});
} on FormatException catch (e) {
print(e.message); // Not a repo: {id: 1}
}
}jsonEncode and JsonEncoder call toJson() on any object they do not know how to
encode, which is why convert(repo) works. For bigger models, the
json_serializable package generates fromJson and toJson for you.
Futures and streams together
Future.wait runs several requests at once. An async* function turns a list of
requests into a stream, and await for reads it one value at a time.
import 'dart:async';
class Profile {
const Profile(this.id, this.name);
final int id;
final String name;
}
class NotFound implements Exception {
const NotFound(this.id);
final int id;
}
Future<Profile> fetchProfile(int id) async {
await Future.delayed(const Duration(milliseconds: 100)); // stands in for a network call
if (id <= 0) throw NotFound(id);
return Profile(id, 'User $id');
}
Stream<Profile> watchProfiles(List<int> ids) async* {
for (final id in ids) {
try {
yield await fetchProfile(id);
} on NotFound catch (e) {
print('Skipping ${e.id}');
}
}
}
Future<void> main() async {
final watch = Stopwatch()..start();
final profiles = await Future.wait([1, 2, 3].map(fetchProfile));
print(profiles.map((p) => p.name).toList()); // [User 1, User 2, User 3]
print(watch.elapsedMilliseconds < 250); // true: they ran at the same time
try {
await Future.wait([fetchProfile(1), fetchProfile(-1)]);
} on NotFound catch (e) {
print('No profile ${e.id}'); // No profile -1
}
await for (final profile in watchProfiles([1, -1, 2])) {
print(profile.name); // User 1, Skipping -1, User 2
}
final names = await watchProfiles([3, 4]).map((p) => p.name).toList();
print(names); // [User 3, User 4]
}Future.wait waits for every future to finish, then throws the first error, so one
failure does not cancel the others. The stream version handles each failure where it happens and carries on,
which suits a feed where one bad item should not stop the rest.
A package, start to finish
dart create -t console greeter writes this layout. Code in lib/ is the library
other files import, bin/ holds the entry point, and test/ holds the tests.
# pubspec.yaml
name: greeter
description: Says hello from the command line.
version: 1.0.0
publish_to: none
environment:
sdk: ^3.13.0
dependencies:
args: ^2.7.0
dev_dependencies:
lints: ^6.0.0
test: ^1.25.0// lib/greeter.dart
/// Builds the greeting for [name], falling back to "world".
String greeting(String name) => 'Hello, ${name.isEmpty ? 'world' : name}!';// bin/greeter.dart
import 'package:args/args.dart';
import 'package:greeter/greeter.dart';
void main(List<String> arguments) {
final parser = ArgParser()..addOption('name', abbr: 'n', defaultsTo: '');
final results = parser.parse(arguments);
print(greeting(results.option('name')!));
}// test/greeter_test.dart
import 'package:greeter/greeter.dart';
import 'package:test/test.dart';
void main() {
test('greets by name', () {
expect(greeting('Ada'), 'Hello, Ada!');
});
group('empty name', () {
test('falls back to world', () {
expect(greeting(''), 'Hello, world!');
});
});
}dart run prints Hello, world!, dart run bin/greeter.dart -n Ada prints
Hello, Ada!, and dart test runs both tests. The sdk constraint is not just a
minimum: it sets the language version, so raising it is what turns on newer syntax
such as dot shorthands. Commit pubspec.lock for an app, and the
Git cheat sheet covers the rest of that workflow.
A note on Flutter
Flutter is Google's UI toolkit, and it is written in Dart: every widget is a Dart
class, and a screen is a tree of them built in a build method. Everything on this
page applies unchanged. The parts you will lean on most are named parameters with
required (every widget constructor uses them), const constructors (Flutter skips
rebuilding widgets it knows cannot change), collection if and for inside lists of
child widgets, and async/await for loading data.
Flutter brings its own tools: flutter create, flutter run and flutter pub add
stand in for the dart commands above, and hot reload swaps your code into the
running app without losing its state. Learn the language first with plain dart run
and the step to Flutter is mostly learning the widget library.
Gotchas
The mistakes almost everyone makes in their first month of Dart.
| Looks right | What actually happens | Do this instead |
|---|---|---|
final sorted = nums.sort(); | sort sorts in place and returns void, so using sorted is a compile error | nums.sort(); then use nums, or [...nums]..sort() |
var tags = {}; for an empty set | It is an empty Map | <String>{} |
print(nums.fold(0, (sum, n) => sum + n)) | Compile error: sum is inferred as Object? | Assign the result to a variable first, or write fold<int> |
if (user.email != null) send(user.email) | Compile error: public fields do not promote, only locals | final email = user.email; then check email |
print('Total: $order.total') | Prints the object, then .total as text | '${order.total}' |
'đŸ‘‹'.length | 2, not 1: length counts UTF-16 units | .runes.length, or the characters package for what people see |
nums.map((n) => n * 2) passed on as a list | It is a lazy Iterable, not a List | Add .toList() |
-7 % 3 expecting -1 | 2: Dart's % never returns a negative number | (-7).remainder(3) |
assert(...) catching bad input in production | Asserts are off in dart run and release builds | Throw an ArgumentError instead |
users.firstWhere((u) => u.isAdmin) | Throws StateError when nothing matches | users.where((u) => u.isAdmin).firstOrNull |
Dart's List.sort is not guaranteed to be stable: items that compare equal can end
up in either order, so sorting by surname after sorting by first name may not keep
the first names in order. Sort on both fields in one comparison instead. The SDK
uses a mix of insertion sort for short lists and a
quick sort variant for long ones,
unlike merge sort, which keeps equal
items in place. Both are on the site as step-through visualisations.
Common questions
Which version of Dart does this cheat sheet cover?
Dart 3.13.4, the current stable release. Most of the page works on any Dart 3 release, and anything newer than 3.0 says so in the notes column, for example extension types need 3.3, digit separators 3.6, null-aware elements 3.8 and dot shorthands 3.10. Run dart --version to see which version you have. Flutter ships its own copy of Dart, so flutter --version shows that one.
What is the difference between var, final and const in Dart?
var declares a variable you can reassign. final declares one you can set only once, and the value can be worked out while the program runs, such as the current time or a function's result. const is stricter: the value must be known when the code is compiled, and a const list or map cannot be changed at all, while a final list can still have items added. Use final by default, const for fixed values, and var only when the value really changes.
What does the question mark and exclamation mark mean in Dart?
A question mark after a type, as in String?, means the value may be null. After a value, as in user?.name, it gives null instead of throwing when user is null, and ?? supplies a default. An exclamation mark after a value, as in nickname!, tells Dart you are sure it is not null and throws at run time if you are wrong, so keep it for values that can only be null because of a bug.
What is the difference between a Future and a Stream?
A Future delivers one value, or one error, some time later, like the result of an HTTP request. A Stream delivers any number of values over time and then finishes, like keystrokes, file chunks or messages on a socket. You await a Future, and you consume a Stream with await for or listen. A function marked async returns a Future, and one marked async* returns a Stream and sends values with yield.
When should I use a mixin instead of extends?
Use extends for an is-a relationship where the subclass really is a kind of the parent, and a class can only extend one parent. Use a mixin to share a piece of behaviour, such as logging or validation, across classes that are otherwise unrelated. A class can take several mixins with the with keyword. Use an extension instead when you want to add methods to a type you do not own, such as String.
Is Dart only for Flutter?
No. Flutter is the main reason most people learn Dart, but the language also runs command-line tools, servers and web apps. dart compile exe produces a native binary with no runtime to install, and dart compile js and dart compile wasm target the browser. Everything on this page runs without Flutter.
What is the difference between dynamic and Object?
Both can hold any value. With Object? the analyzer still checks your code, so you must test the type with is or cast it with as before calling a method, which catches mistakes early. With dynamic the checks are switched off and any method call compiles, then fails at run time if the method does not exist. Prefer Object? and keep dynamic for things like freshly decoded JSON.
Is Dart single-threaded?
Each isolate runs your code on one thread with an event loop, so there are no data races on shared variables. await does not block that thread: it pauses the function and lets other work run until the Future completes. For CPU-heavy work, Isolate.run starts another isolate with its own memory and returns the result, which keeps an app responsive while it works.
