Programming languages cheat sheetDart logo

Dart cheat sheet

Dart syntax on one page, grouped by what you are trying to do: null safety, collections, patterns, classes and mixins, async and streams, checked on Dart 3.13.

Last updated

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.

Running Dart and pub

TaskCommandNotes
Check which version you havedart --version
Run a filedart run main.dartdart main.dart works too
Start a command-line appdart create -t console my_app-t package for a library, -t cli for an app with argument parsing
Run the app in the current packagedart runRuns bin/<package name>.dart
Run it with argumentsdart run bin/my_app.dart --name Ada
Add a dependencydart pub add httpWrites the latest compatible version into pubspec.yaml
Add a dev-only dependencydart pub add dev:test
Remove a dependencydart pub remove http
Download what pubspec.yaml listsdart pub get
See which packages are behinddart pub outdated
Upgrade, including major versionsdart pub upgrade --major-versionsLeave out the flag to stay within your ranges
Run the testsdart testNeeds the test package as a dev dependency
Run the tests matching a namedart test --name "falls back"
Format every filedart format .
Find errors and lint warningsdart analyze
Apply the suggested fixesdart fix --apply--dry-run shows them first
Compile to a native binarydart compile exe bin/my_app.dart -o my_app
Compile for the browserdart compile js web/main.dart -o main.jsdart 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.

TaskCodeNotes
Print a lineprint('Hello');
Variable, type inferredvar name = 'Ada';Still a String. It cannot hold an int later
Set once, at run timefinal city = readCity();Use final unless the value changes
Fixed at compile timeconst maxUsers = 100;Also makes deeply immutable lists and maps
Give the type explicitlyString name = 'Ada';
Declare now, assign before first readlate String label;Reading it early throws at run time
Value that may be nullString? nickname;Starts as null
Default when nullnickname ?? 'Anonymous'
Assign only if nullnickname ??= 'kit';
Reach through nullable valuesuser?.address?.cityThe whole chain is null if any link is
Index a nullable listitems?[0]
Assert it is not nullnickname!Throws if it is. Only where null means a bug
Check, then use as non-nullif (nickname != null) print(nickname.length);Works on locals and, from Dart 3.2, private final fields. Not on public fields
Readable big numbers1_000_000Dart 3.6+
Whole-number division7 ~/ 2Gives 3. 7 / 2 gives 3.5, always a double
Remainder7 % 2-7 % 3 is 2. (-7).remainder(3) is -1
String to numberint.parse('42') double.parse('3.5')Throws FormatException on junk
String to number, or nullint.tryParse(input)
Convert between number typescount.toDouble() 3.9.toInt() 3.5.round()toInt truncates: 3.9 becomes 3
Is it a typevalue is String value is! int
Castvalue as StringThrows if it is not one
What type is itvalue.runtimeType
Opt out of type checksdynamic 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

TaskCodeNotes
Built-in typesint double num String boolnum is the parent of int and double
Record with named fieldsfinal point = (x: 3, y: 4);Then point.x. Records are Dart 3.0+
Record with positional fieldsfinal 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 fieldsfinal (:min, :max) = namedBounds(nums);
Give a type a second nametypedef Json = Map<String, dynamic>;
Function typeint Function(int, int) op = add;
Generic classclass Box<T> { final T value; Box(this.value); }
Define an enumenum Direction { north, south, east, west }
Enum case's nameDirection.north.nameGives 'north'
Every caseDirection.values
Case from its nameDirection.values.byName('south')Throws for an unknown name
Enum with fieldsenum Role { admin('Admin'), member('Member'); const Role(this.label); final String label; }Then Role.admin.label
Leave out the type nameDirection heading = .south;Dot shorthand, Dart 3.10+. Works wherever the type is known
Wrap a type at zero costextension type UserId(int value) {}Dart 3.3+. A UserId cannot be passed where an int is expected
Type that never returnsNever fail(String m) => throw StateError(m);

Strings

TaskCodeNotes
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 typedr'C:\path\new'A raw string. No interpolation either
Join stringsfirst + ' ' + lastOr put literals side by side: 'a' 'b'
Build a long stringfinal sb = StringBuffer()..write('a')..write('b');Then sb.toString()
Lengths.lengthIn UTF-16 units: an emoji counts as 2
Is it emptys.isEmpty s.isNotEmpty
Change cases.toUpperCase() s.toLowerCase()
Trim whitespaces.trim() s.trimLeft() s.trimRight()
Does it contains.contains('cat')
Does it start or end withurl.startsWith('https') file.endsWith('.dart')
Replace every matchs.replaceAll('cat', 'dog')replaceFirst for just the first
Split into partscsv.split(',')Keeps empty parts: 'a,,b' gives [a, , b]
Join a list into a stringnames.join(', ')
Part of a strings.substring(0, 5)End index is excluded
Character at a positions[0]A one-character String
Position of a substrings.indexOf('l')-1 when missing
Pad to a width'7'.padLeft(3, '0')Gives 007
Repeat'-' * 20
Reverses.split('').reversed.join()Fine for plain text. Breaks emoji
Number to string42.toString() '$price'
Fixed decimal placesprice.toStringAsFixed(2)
Does it match a patternRegExp(r'\d+').hasMatch(s)Use raw strings for patterns
Capture part of a matchRegExp(r'(\d{4})-(\d{2})').firstMatch(s)?.group(1)group(0) is the whole match
Every matchRegExp(r'\d+').allMatches(s).map((m) => m[0])
Compare ignoring casea.toLowerCase() == b.toLowerCase()

Collections: lists

TaskCodeNotes
Create a listvar nums = [1, 2, 3];
Empty list of a typevar names = <String>[];
Filled with one valueList.filled(5, 0)
Built from its positionList.generate(5, (i) => i * i)Gives [0, 1, 4, 9, 16]
Read by positionnums[0]Throws RangeError if out of range
First or lastnums.first nums.lastThrow on an empty list
First or last, or nullnums.firstOrNull nums.lastOrNullDart 3.0+
Appendnums.add(4); nums.addAll([5, 6]);
Insert at a positionnums.insert(0, 0);
Remove at a positionnums.removeAt(0)Returns the removed item
Remove the last itemnums.removeLast()
Remove a valuenums.remove(5)First match only. Returns true if found
Remove every matchnums.removeWhere((n) => n < 0);
How many, and is it emptynums.length nums.isEmpty nums.isNotEmpty
Does it containnums.contains(3)
Position of a valuenums.indexOf(3)-1 when missing
A slicenums.sublist(1, 3)A new list. take(2) and skip(1) are lazy
Sort in placenums.sort();Returns void, not the list
Sort by a fieldusers.sort((a, b) => a.age.compareTo(b.age));
Sorted copyfinal sorted = [...nums]..sort();
Transform every itemnums.map((n) => n * 2).toList()map is lazy. toList() runs it
Keep items that pass a testnums.where((n) => n.isEven).toList()
Fold into one valuefinal total = nums.fold(0, (sum, n) => sum + n);Assign it first, or write fold<int>. package:collection adds nums.sum
Smallest and largestnums.reduce(min) nums.reduce(max)import 'dart:math'
First item that passes a testusers.where((u) => u.isAdmin).firstOrNullfirstWhere throws when nothing matches
Do any or all passnums.any((n) => n > 3) nums.every((n) => n > 0)
Loop with the positionfor (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 listsnested.expand((list) => list).toList()
Reversed, shufflednums.reversed.toList() nums.shuffle();
Read-only listList.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.

TaskCodeNotes
Create a mapvar ages = {'Ada': 36, 'Alan': 41};
Empty map of a typevar cache = <String, int>{};
Read a valueages['Ada']int?, null when the key is missing
Read with a defaultages['Bob'] ?? 0
Set a valueages['Bob'] = 30;
Remove a keyages.remove('Bob')Returns the old value, or null
Count how often each value appearscounts.update(word, (n) => n + 1, ifAbsent: () => 1);
Set only if missingcache.putIfAbsent(key, () => load(key));
Does a key existages.containsKey('Ada')
Just the keys or valuesages.keys ages.values
Loop over pairsfor (final MapEntry(:key, :value) in ages.entries) { }A pattern, Dart 3.0+. e.key and e.value also work
Change every valueages.map((k, v) => MapEntry(k, v + 1))
Keep pairs that pass a testMap.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 fieldgroupBy(words, (w) => w.length)From package:collection
Create a setvar tags = {'dart', 'flutter'};
Empty setvar tags = <String>{};A bare {} is an empty map, not a set
Add and checktags.add('server'); tags.contains('dart')
Remove duplicates from a listnums.toSet().toList()Keeps the first of each, in order
Union, intersection, differencea.union(b) a.intersection(b) a.difference(b)
Contains every one ofa.containsAll(b)

Control flow and patterns

TaskCodeNotes
If, else if, elseif (a) { } else if (b) { } else { }Brackets round the condition are required
Pick one of two valuesfinal label = count == 1 ? 'item' : 'items';
Switch statementswitch (code) { case 200 || 201: ok(); case 404: missing(); default: fail(); }No break needed, Dart 3.0+
Switch expressionfinal text = switch (code) { 200 => 'OK', 404 => 'Not found', _ => 'Error' };Dart 3.0+. _ matches anything
Match a rangeswitch (age) { < 13 => 'child', < 18 => 'teen', _ => 'adult' }
Match with a conditioncase int n when n < 0:
Match a recordswitch ((x, y)) { case (0, 0): origin(); case (0, _): onYAxis(); }
Check the shape of JSONif (json case {'name': String name, 'age': int age}) { }Binds name and age when every part matches
Match every subtype of a sealed classswitch (result) { Ok(:final value) => value, Err() => 0 }No default needed, the compiler checks
Counting loopfor (var i = 0; i < 5; i++) { }
Loop over itemsfor (final n in nums) { }
Repeat n timesfor (var i = 0; i < 3; i++) { }There is no range literal
While loopwhile (queue.isNotEmpty) { }
Run the body at least oncedo { } while (tries < 3);
Skip or stopcontinue; break;
Break out of an outer loopouter: for (final row in grid) { for (final cell in row) { break outer; } }
Check an assumptionassert(age >= 0, 'negative age');Only runs with dart run --enable-asserts, and in tests and Flutter debug builds

Functions

TaskCodeNotes
Define a functionint add(int a, int b) { return a + b; }
One-expression bodyint add(int a, int b) => a + b;
Optional positional parameterString greet([String name = 'world'])Call it as greet() or greet('Ada')
Named parametersvoid connect({required String host, int port = 80})Call it as connect(host: 'localhost')
Entry point with argumentsvoid main(List<String> args) { }
Anonymous function(x) => x * xOr (x) { return x * x; } for several lines
Store a functionfinal square = (int x) => x * x;
Pass a function by namenames.forEach(print);A tear-off: no brackets, so it is not called
Name a function typetypedef Compare<T> = int Function(T a, T b);
Closure that keeps statevar total = 0; return () => ++total;It captures total itself, not a copy
Generic functionT firstItem<T>(List<T> items) => items.first;
Generic with a constraintT maxOf<T extends Comparable<T>>(T a, T b) => a.compareTo(b) >= 0 ? a : b;
Ignore parameters(_, _) => 0Several _ allowed from Dart 3.7
Call several methods on one objectfinal sb = StringBuffer()..write('a')..write('b');Cascade: each .. returns the object, not the result
Make an object callableint call(int a, int b) => a + b;Then adder(2, 3)

Classes

TaskCodeNotes
Define a classclass Point { final double x, y; Point(this.x, this.y); }this.x in the parameter list sets the field
Create onefinal p = Point(1, 2);No new keyword needed
Named parameters in a constructorUser({required this.name, this.age = 0});
Second constructorPoint.origin() : x = 0, y = 0;Code after the colon is the initializer list
Check arguments in the constructorUser(this.age) : assert(age >= 0);
Constructor that can return a subtype or cached objectfactory User.fromJson(Map<String, dynamic> json) => User(name: json['name']);
Compile-time constant objectsconst Point(this.x, this.y);Needs final fields. const Point(1, 2) is created once
Getterdouble get area => width * height;
Setterset celsius(double value) => _celsius = value;
Private to the fileint _count = 0;A leading underscore. There is no private keyword
Shared by the class, not each objectstatic const origin = Point(0, 0);
Inheritclass Admin extends User
Pass a parameter up to the parentAdmin({required super.name});
Replace a parent method@override String describe() => 'Admin ${super.describe()}';
Class that cannot be created directlyabstract class Shape { double get area; }
Promise to provide a type's membersclass Square implements ShapeAny class works as an interface
Value equalitybool 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 operatorMoney operator +(Money other) => Money(pence + other.pence);

Mixins, extensions and class modifiers

TaskCodeNotes
Define a mixinmixin Loggable { void log(String m) => print('[$runtimeType] $m'); }
Use one or more mixinsclass Order extends Model with Loggable, Timestamped
Mixin only for certain classesmixin Walker on Animal { }It can call Animal's members
Class that is also a mixinmixin class Musician { }Dart 3.0+
Add a method to an existing typeextension StringX on String { bool get isBlank => trim().isEmpty; }Then ' '.isBlank
Extension only this file seesextension on String { }Leave out the name
Fixed set of subtypessealed class Result { }Subtypes in the same file only. Switches over it must be exhaustive
Cannot be extended or implemented outside the filefinal class Config { }
Can be extended but not implemented outsidebase class Model { }
Can be implemented but not extended outsideinterface 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

TaskCodeNotes
Throwthrow FormatException('bad input');You can throw any non-null object, but stick to Exception and Error
Catch one typetry { } on FormatException catch (e) { print(e.message); }
Catch anything, with the stack tracecatch (e, stackTrace) { }
Always run afterwardsfinally { file.close(); }
Throw the caught error againcatch (e) { log(e); rethrow; }Keeps the original stack trace
Your own exceptionclass LoginException implements Exception { final String message; LoginException(this.message); }
Reject a bad argumentthrow ArgumentError.value(age, 'age', 'must be positive');
Signal a broken statethrow StateError('already closed');
Parse without catchingint.tryParse(input) ?? 0
Code that should be unreachablethrow 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.

TaskCodeNotes
Async functionFuture<User> fetchUser(int id) async { }Returns a Future even though the body returns a User
Wait for itfinal user = await fetchUser(1);Only inside an async function
Async mainFuture<void> main() async { }
Pauseawait Future.delayed(const Duration(seconds: 1));
Run several at oncefinal users = await Future.wait([fetchUser(1), fetchUser(2)]);
Run two of different types at oncefinal (user, posts) = await (fetchUser(1), fetchPosts(1)).wait;Dart 3.0+
Give up after a timeawait fetchUser(1).timeout(const Duration(seconds: 5));Throws TimeoutException from dart:async
Handle an async errortry { await fetchUser(1); } on HttpException catch (e) { }
Start without waitingunawaited(saveLog());From dart:async. Says the missing await is on purpose
Produce values over timeStream<int> countTo(int n) async* { for (var i = 1; i <= n; i++) yield i; }
Consume values over timeawait for (final value in stream) { }
React to each valuestream.listen((v) => print(v), onError: print, onDone: close);Keep the subscription to cancel() it
Transform a streamstream.where((n) => n.isOdd).map((n) => n * 10)
Collect a streamawait stream.toList() await stream.first
Push values in yourselffinal controller = StreamController<int>();controller.add(1), then close(). Read controller.stream
Stream with several listenersStreamController<int>.broadcast()A plain stream allows one listener
Values on a timerStream.periodic(const Duration(seconds: 1), (i) => i).take(3)
Wrap a callback APIfinal completer = Completer<String>();Call completer.complete(value), then await completer.future
Heavy work off the main isolatefinal result = await Isolate.run(() => parse(bigJson));import 'dart:isolate'. Not on the web
Lazy sequenceIterable<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

TaskCodeNotes
Import a core libraryimport 'dart:math';Also dart:async, dart:convert, dart:io, dart:isolate
Import a packageimport 'package:http/http.dart' as http;Then http.get(...). The prefix avoids name clashes
Import your own package's codeimport 'package:my_app/models.dart';Files under lib/ are importable this way
Import a file next to this oneimport 'utils.dart';
Import only some namesimport 'dart:math' show max, min;hide leaves names out instead
Re-export from your libraryexport 'src/greeter.dart';
Pin the SDK versionenvironment: sdk: ^3.13.0In pubspec.yaml. Sets the language version too
Depend on a version rangehttp: ^1.6.0Caret means at least 1.6.0 and below 2.0.0
Depend on a local foldershared: path: ../sharedNested 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 rightWhat actually happensDo this instead
final sorted = nums.sort();sort sorts in place and returns void, so using sorted is a compile errornums.sort(); then use nums, or [...nums]..sort()
var tags = {}; for an empty setIt 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 localsfinal email = user.email; then check email
print('Total: $order.total')Prints the object, then .total as text'${order.total}'
'đŸ‘‹'.length2, 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 listIt is a lazy Iterable, not a ListAdd .toList()
-7 % 3 expecting -12: Dart's % never returns a negative number(-7).remainder(3)
assert(...) catching bad input in productionAsserts are off in dart run and release buildsThrow an ArgumentError instead
users.firstWhere((u) => u.isAdmin)Throws StateError when nothing matchesusers.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.

See all cheat sheets

Want this explained by a cat?

The videos cover the same ground in sixty seconds. If there is a tool you want a cheat sheet for next, ask.