DevTools

Cheatsheet Dart

Linguagem da Google, base do Flutter, para apps mobile, web e servidor

Back to languages
Dart
122 cards found
Categories:
Versions:

Basic and Variables


12 cards
main function
void main() {
  print("Hello, Dart!");
}

// With arguments (CLI)
void main(List<String> args) {
  print(args);
}

main() is the entry point of every Dart program. In CLI scripts it can receive args the a List<String>.

String interpolation
String name = "Anna";
int age = 30;

print("Hello, $name");
print("Total: ${price * 2}");
print("Uppercase: ${name.toUpperCase()}");

Use $variable for simple values and ${expression} for expressions or method calls inside the string.

Comparison operators
5 == 5;   // true  equal
5 != 3;   // true  not equal
5 > 3;    // true  greater
5 <= 5;   // true  less than or equal

// identical (same value and type)
1 == 1.0;   // true (== compares value)

== compares values. The operators >, <, >= and <= compare order. != is the negation of equality.

Variables
var name = "Anna";      // inferred type (String)
String city = "Porto";
int age = 30;
double price = 9.99;
bool active = true;

Use var to let Dart infer the type, or declare the type explicitly. A typed variable does not accept values of another type.

Multi-line and raw strings
var text = """
First line
Second line
""";

var raw = r"No \n escape here";
var path = r"C:\folder\file";

Three quotes """ create multi-line strings. The r prefix creates a raw string, ignoring escapes like \n.

Logical operators
true && false;  // false (AND)
true || false;  // true  (OR)
!true;          // false (negation)

// short-circuit
if (a != null && a.length > 0) {
  // only evaluates a.length if a is not null
}

&& (AND) and || (OR) are short-circuit: the second operand is only evaluated if needed. ! negates a bool.

final and const
final name = "Anna";  // one assignment (runtime)
const pi = 3.14;     // compile-time constant

final list = [1, 2];
list.add(3);         // OK: contents change
// list = [];        // ERROR: reassignment

final allows a single assignment at runtime. const is immutable from compilation and requires a statically known value.

Comments
// single-line comment

/* multi-line
   comment */

/// Documentation (generates docs with dartdoc)
/// [name] is the parameter.
void func() {}

Use // for a line, /* */ for a block and /// for documentation. Doc comments generate API docs with the dartdoc tool.

Cascade (..)
var list = []
  ..add(1)
  ..add(2)
  ..add(3)
  ..sort();

var sb = StringBuffer()
  ..write("Hello ")
  ..write("Dart");

The .. operator chains several operations on the same object without repeating its name. Ideal to configure objects compactly.

dynamic and Object
dynamic x = "text";
x = 42;            // OK: any type
x.nonExistentMethod(); // compiles, fails at runtime

Object o = 10;
// o.toUpperCase(); // ERROR: Object does not have it

dynamic disables type checking (a runtime risk). Object is the root of all types, but keeps static safety.

Arithmetic operators
int a = 10, b = 3;
a + b;   // 13  addition
a - b;   // 7   subtraction
a * b;   // 30  multiplication
a / b;   // 3.33 division (double)
a ~/ b;  // 3   integer division
a % b;   // 1   remainder

Division / always returns a double. Use ~/ for the integer quotient and % for the division remainder.

Increment and assignment
int x = 5;
x++;     // 6 (post-increment)
++x;     // 7 (pre-increment)
x += 3;  // x = x + 3
x *= 2;  // x = x * 2
x ??= 0; // assigns only if it is null

The compound operators +=, -=, *= combine operation and assignment. ??= assigns only if the variable is null.

Data Types


12 cards
int and double
int x = 10;        // integer
double y = 3.14;   // decimal
num z = 5;         // int or double
num w = 2.5;       // also valid

int hex = 0xFF;    // hexadecimal
int bin = 0b1010;  // binary (10)

int is an integer and double a decimal. num is the superclass of both. Supports hexadecimal 0x and binary 0b notation.

String: split and replace
String s = "a,b,c,d";
s.split(",");            // ["a","b","c","d"]
s.replaceAll(",", "-");  // "a-b-c-d"
s.replaceFirst(",", ";"); // "a;b,c,d"
s.substring(0, 3);       // "a,b"
s.indexOf(",");          // 1

split() returns a List<String>. replaceAll() replaces all occurrences and substring() extracts part of the string.

List
List<int> nums = [1, 2, 3];
nums.add(4);
nums[0];          // 1
nums.length;      // 4
nums.first;       // 1
nums.last;        // 4
List empty = [];

A List is ordered and dynamic. The generic type List<int> restricts the elements. first and last access the ends.

Numeric methods
double d = 9.876;
d.round();             // 10
d.floor();             // 9
d.ceil();              // 10
d.toStringAsFixed(2);  // "9.88"
d.abs();               // absolute value
d.truncate();          // 9 (drops decimals)

round(), floor() and ceil() round in different ways. toStringAsFixed() formats the decimal places the a string.

Type conversion
int n = int.parse("42");
double d = double.parse("3.14");
String s = n.toString();
String f = 3.14.toStringAsFixed(1); // "3.1"

int? error = int.tryParse("abc"); // null

int.parse() and double.parse() convert strings into numbers and throw an exception if invalid. tryParse() returns null instead of failing.

Map
Map<String, int> ages = {
  "Anna": 30,
  "Ray": 25,
};
ages["Anna"];        // 30
ages["Ze"] = 40;    // adds
ages.keys;          // (Anna, Ray, Ze)
ages.values;        // (30, 25, 40)

A Map stores key-value pairs. Accessing a non-existent key returns null. keys and values return iterables.

Numeric properties
int n = 10;
n.isEven;      // true (even)
n.isOdd;       // false (odd)
n.isNegative;  // false

double d = 3.14;
d.isFinite;    // true
d.isNaN;       // false

The isEven/isOdd properties check parity. isFinite and isNaN are useful to validate calculation results.

Booleans
bool active = true;
bool empty = list.isEmpty;
bool older = age >= 18;

// Dart requires a real bool (no truthy/falsy)
if (active) { }
// if (1) { }  // ERROR: int is not a bool

Dart is strict: conditions require a real bool, unlike JavaScript. There are no "truthy" values — if (1) is a compile error.

Set
Set<int> unique = {1, 2, 2, 3};
// {1, 2, 3} — duplicates removed

unique.add(3);        // ignored
unique.contains(1);   // true
unique.length;        // 3

A Set stores unique values with no guaranteed order. Adding a duplicate has no effect. Useful to remove repetitions from a list.

String methods
String s = "  Hello World  ";
s.trim();             // "Hello World"
s.toLowerCase();      // lowercase
s.toUpperCase();      // uppercase
s.length;             // length
s.contains("World");  // true
s.startsWith("Hello"); // false (spaces)

Strings are immutable: each method returns a new string. trim() removes leading/trailing spaces and contains() checks substrings.

Runes and characters
String s = "Hi";
s.codeUnits;        // UTF-16 codes
s.runes;            // Unicode code points

var emoji = "\u{1F600}"; // 😀
print(emoji);

runes gives access to Unicode code points (important for emojis). \u{...} inserts a Unicode character by its code.

Type check (is / the)
Object x = "text";
if (x is String) {
  print(x.length); // x is promoted to String
}

num n = 5;
int i = n the int;  // explicit cast

is checks the type and promotes the variable inside the block. the performs an explicit cast and throws an exception if the type does not match.

Flow Control


12 cards
if / else
if (age >= 18) {
  print("Adult");
} else if (age > 12) {
  print("Teen");
} else {
  print("Child");
}

The if/else if/else structure evaluates conditions in order and runs the first true block. The parentheses are mandatory.

Classic for
for (int i = 0; i < 10; i++) {
  print(i);
}

for (int i = 10; i > 0; i--) {
  print(i); // countdown
}

The for loop has initialization, condition and increment. It is ideal when you need the index or to control the counter manually.

break and continue
for (int i = 0; i < 10; i++) {
  if (i == 3) continue; // skips 3
  if (i == 7) break;    // stops at 7
  print(i);
}
// prints 0,1,2,4,5,6

continue jumps to the next iteration and break ends the loop immediately. Both work in for, while and switch.

Ternary operator
int age = 20;
String r = age >= 18 ? "Adult" : "Minor";

// Chained (use sparingly)
String level = grade >= 18 ? "A"
             : grade >= 14 ? "B"
             : "C";

The ternary condition ? valueIfTrue : valueIfFalse is a compact if-else in an expression. Avoid chaining many to keep readability.

for-in
var fruits = ["apple", "pear", "grape"];

for (var fruit in fruits) {
  print(fruit);
}

for (var key in map.keys) {
  print("$key: ${map[key]}");
}

for-in iterates over each element of a collection without an index. It is the most readable way to iterate over a List, Set or Map.keys.

Labels in loops
outer:
for (int i = 0; i < 3; i++) {
  for (int j = 0; j < 3; j++) {
    if (i == 1 && j == 1) {
      break outer; // exits the outer loop
    }
  }
}

A label (a name followed by :) lets break or continue act on an outer loop in nested loops.

switch / case
switch (day) {
  case 1:
    print("Monday");
    break;
  case 2:
    print("Tuesday");
    break;
  default:
    print("Other");
}

switch compares a value against several cases. Each case needs a break (or return) so it does not fall through to the next.

while
int x = 5;
while (x > 0) {
  print(x);
  x--;
}
// x ends at 0

while repeats while the condition is true. It checks the condition before each iteration, so it may never run the block.

Pattern matching (Dart 3)
var (year, month, day) = (2024, 7, 24);
print("$day/$month/$year");

// Destructure a list
var [first, second, ...rest] = [1, 2, 3, 4];

// Guard in switch
var r = switch (value) {
  int n when n > 0 => "positive",
  _ => "other",
};

Dart 3 introduces patterns: destructuring of records and lists, and when for conditional guards in switch.

Switch expression (Dart 3)
String name = switch (day) {
  1 => "Monday",
  2 => "Tuesday",
  3 => "Wednesday",
  _ => "Other",
};

In Dart 3 switch can be an expression that returns a value. _ is the default case (wildcard) and replaces default.

do-while
int x = 0;
do {
  print(x);
  x++;
} while (x < 5);
// runs at least once

do-while runs the block before checking the condition, guaranteeing at least one run. Useful for menus and validations.

if-case (Dart 3)
var json = {"type": "circle", "radius": 5};

if (json case {"type": "circle", "radius": var r}) {
  print("Circle with radius $r");
}

// Patterns in declarations
var [a, b] = [1, 2];

if-case combines a condition with pattern matching, extracting values from maps and objects declaratively and safely.

Functions


12 cards
Simple function
int add(int a, int b) {
  return a + b;
}

void greeting(String name) {
  print("Hello, $name");
}

print(add(2, 3)); // 5

Declare the return type before the name. void indicates it returns no value. Typed parameters increase code safety.

Anonymous functions (lambda)
var list = [1, 2, 3];
list.forEach((n) => print(n));

var double = (int x) => x * 2;
print(double(5)); // 10

var sum = (int a, int b) {
  return a + b;
};

Nameless functions can be assigned to variables or passed the arguments. They are the basis of callbacks and of methods like map() and where().

required parameter
class User {
  final String name;
  final String email;

  User({required this.name,
      required this.email});
}

var u = User(name: "Anna", email: "a@b.com");

required forces the named argument to be passed, avoiding unexpected nulls. Essential in constructors and configurations.

Arrow function (=>)
int double(int x) => x * 2;

bool isEven(int n) => n % 2 == 0;

// Equivalent to:
int triple(int x) {
  return x * 3;
}

The => expression syntax is a shortcut for single-expression functions that return that value. It replaces { return ...; }.

Function the parameter
int apply(int x, int Function(int) f) {
  return f(x);
}

print(apply(5, (n) => n * 2)); // 10
print(apply(5, (n) => n + 1)); // 6

Functions are first-class objects in Dart. The type int Function(int) describes a function that receives and returns an int.

Default values
void connect({
  String host = "localhost",
  int port = 3306,
  bool ssl = false,
}) {
  print("$host:$port ssl=$ssl");
}

connect(port: 5432);

Default values apply when the argument is not provided. In named parameters they must be constants (const).

Named parameters
void call({required String number,
    bool speaker = false}) {
  print("$number speaker=$speaker");
}

call(number: "123", speaker: true);
call(number: "456"); // speaker = false

Parameters inside { } are passed by name, in any order. required makes them mandatory and there can be default values.

Function the return value (closure)
Function multiplier(int factor) {
  return (int x) => x * factor;
}

var doubler = multiplier(2);
var tripler = multiplier(3);
print(doubler(5));  // 10
print(tripler(5));  // 15

A function can return another function, creating a closure that captures variables from the outer scope (here the factor).

Variable scope
String global = "outside";

void test() {
  String local = "inside";
  print(global); // accessible
  print(local);
}
// print(local); // ERROR: out of scope

Variables declared inside a function are local and not accessible outside it. Dart has lexical scope: { } blocks create new scopes.

Optional positional parameters
void greet(String name,
    [String title = "Mr."]) {
  print("Hello, $title $name");
}

greet("Anna");         // Mr. Anna
greet("Anna", "Dr.");  // Dr. Anna

Parameters inside [ ] are positional and optional. They must come after the required ones and can have a default value.

Generic functions
T first<T>(List<T> list) {
  return list.first;
}

print(first<int>([1, 2, 3]));     // 1
print(first<String>(["a", "b"])); // "a"

<T> makes the function generic, working with any type. The type can be inferred or specified explicitly in the call.

Tear-off and method reference
var list = [1, 2, 3];

// Tear-off: reference to the method
list.forEach(print);

// Equivalent to:
list.forEach((n) => print(n));

var double = [1, 2].map((n) => n * 2);

A tear-off is passing a method directly (e.g. print) instead of wrapping it in a lambda. Cleaner when the signature matches.

Collections


12 cards
List operations
var list = [1, 2, 3];
list.add(4);         // adds at the end
list.remove(1);      // removes the value 1
list.removeAt(0);    // removes by index
list.insert(0, 9);   // inserts at position
list.contains(2);    // true
list.indexOf(3);     // position

A List is dynamic: add() inserts at the end, remove() deletes by value, insert() places at a specific position.

sort() and reversed
var list = [3, 1, 4, 1, 5];
list.sort();              // [1, 1, 3, 4, 5]
list.sort((a, b) => b.compareTo(a)); // desc

var names = ["ana", "rui"];
names.reversed.toList();   // ["rui", "ana"]

sort() sorts the list in place (mutates the original). Pass a comparator for custom ordering. reversed returns the reverse order.

forEach and indexed
var fruits = ["apple", "pear"];
fruits.forEach((f) => print(f));

// With index (Dart 3)
for (var (i, f) in fruits.indexed) {
  print("$i: $f");
}

forEach() runs a function for each element. .indexed provides (index, value) pairs to iterate with the position.

map() and where()
var list = [1, 2, 3, 4, 5];

var double = list.map((e) => e * 2).toList();
// [2, 4, 6, 8, 10]

var evens = list.where((e) => e % 2 == 0).toList();
// [2, 4]

map() transforms each element and where() filters. Both return an Iterable; use toList() to materialize it.

Spread operator (...)
var a = [1, 2];
var b = [0, ...a, 3]; // [0, 1, 2, 3]

List<int>? nullable;
var c = [...?nullable, 1]; // null-safe spread

var map = {...otherMap, "x": 1};

... expands the elements of a collection inside another. ...? avoids an error if the collection is null.

Map: transformations
var map = {"a": 1, "b": 2};

var doubled = map.map((k, v) => MapEntry(k, v * 2));
// {"a": 2, "b": 4}

map.containsKey("a");  // true
map.remove("b");       // removes the key

A Map's map() returns MapEntry to transform keys and values. containsKey() checks whether a key exists.

any() and every()
var list = [1, 2, 3, 4];

list.any((e) => e > 3);   // true (some)
list.every((e) => e > 0); // true (all)
list.contains(2);         // true
list.isEmpty;             // false

any() checks whether at least one element meets the condition and every() whether all of them do.

Collection-if and for
bool active = true;
var items = [
  "base",
  if (active) "extra",
  for (var i in [1, 2]) "item$i",
];
// ["base", "extra", "item1", "item2"]

Inside collection literals you can use if and for to build lists dynamically, without external logic.

Set: set operations
var a = {1, 2, 3};
var b = {2, 3, 4};

a.union(b);          // {1,2,3,4}
a.intersection(b);   // {2,3}
a.difference(b);     // {1}
a.containsAll([1,2]); // true

A Set offers mathematical operations: union(), intersection() and difference().

fold() and reduce()
var list = [1, 2, 3, 4];

int sum = list.fold(0, (acc, e) => acc + e);
// 10

int product = list.reduce((acc, e) => acc * e);
// 24

fold() accumulates a value from an initial value. reduce() does the same but uses the first element the the starting point.

List.generate()
var squares = List.generate(5, (i) => i * i);
// [0, 1, 4, 9, 16]

var evens = List.generate(10, (i) => i * 2);
// [0, 2, 4, ..., 18]

List.generate() creates a list from a function that receives the index. Ideal to generate sequences or test data.

Immutable collections
var fixed = const [1, 2, 3];
// fixed.add(4); // ERROR: immutable

var immutable = List.unmodifiable([1, 2, 3]);
// immutable[0] = 9; // ERROR at runtime

var readOnly = [1, 2, 3] the List<int>;

A const list is immutable at compilation. List.unmodifiable() creates a read-only view that throws an error when you try to modify it.

Classes e OOP


16 cards
Basic class
class Person {
  String name;
  int age;

  Person(this.name, this.age);
}

var p = Person("Anna", 30);
print(p.name); // Anna

The constructor Person(this.name, this.age) automatically assigns the arguments to the fields. It is the idiomatic and compact form in Dart.

Abstract class
abstract class Shape {
  double area();
}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);

  @override
  double area() => 3.14 * radius * radius;
}

An abstract class cannot be instantiated and defines a contract. Subclasses are required to implement the abstract methods.

Advanced enum
enum Planet {
  earth(9.8),
  mars(3.7),
  moon(1.6);

  final double gravity;
  const Planet(this.gravity);
}

print(Planet.earth.gravity); // 9.8

Enums in Dart can have fields, constructors and methods. Each value passes arguments to the const constructor, allowing associated data.

Static members
class MyMath {
  static const double pi = 3.14159;

  static double circleArea(double r) {
    return pi * r * r;
  }
}

print(MyMath.pi);
print(MyMath.circleArea(2));

static members belong to the class, not the instance. They are accessed by the class name, without creating an object. Useful for utilities and constants.

Named constructor
class Point {
  final int x, y;

  const Point(this.x, this.y);
  Point.origin() : this(0, 0);
  Point.axisX(this.x) : y = 0;
}

var p = Point.origin();

Named constructors like Point.origin() offer alternative ways to create objects. The : this(...) delegates to another constructor.

Interface (implements)
class Point {
  int x = 0, y = 0;
}

class Point3D implements Point {
  int x = 0, y = 0, z = 0;
}

void printPoint(Point p) => print(p.x);

In Dart any class can be an interface with implements. The class must reimplement all members of the interface.

Extension methods
extension StringExtra on String {
  String get reversed =>
      split("").reversed.join();
  bool get isNumber =>
      int.tryParse(this) != null;
}

print("abc".reversed); // "cba"
print("123".isNumber);  // true

An extension adds methods to existing types without modifying them. They are accessed the if they were native methods of the type.

Factory constructor
class Logger {
  static final Logger _inst = Logger._();
  Logger._();

  factory Logger() => _inst;

  void log(String msg) => print(msg);
}

var a = Logger();
var b = Logger(); // same instance

A factory constructor controls object creation, being able to return existing instances (singleton) or subclasses.

Getters and setters
class Rectangle {
  double width = 0, height = 0;

  double get area => width * height;

  set area(double v) {
    width = height = v;
  }
}

var r = Rectangle();
r.area = 5;    // setter
print(r.area); // getter

get creates a computed read-only property and set allows assignment with logic. They are accessed like normal fields, without parentheses.

Mixin (with)
mixin Swimmer {
  void swim() => print("swimming");
}

mixin Flyer {
  void fly() => print("flying");
}

class Duck extends Animal with Swimmer, Flyer {}

Duck().swim();
Duck().fly();

A mixin adds behavior to several classes without classic inheritance. It is applied with with and can combine multiple mixins.

Generic classes
class Box<T> {
  T value;
  Box(this.value);
}

var c = Box<int>(42);
var s = Box<String>("hi");
print(c.value); // 42

<T> makes the class reusable for any type. The type is fixed at instantiation, ensuring type safety.

Immutable classes
class Point {
  final int x;
  final int y;
  const Point(this.x, this.y);
}

const a = Point(1, 2);
const b = Point(1, 2);
print(identical(a, b)); // true (canonical)

final fields + a const constructor create immutable objects. Identical const instances are canonical (shared in memory).

Inheritance (extends)
class Animal {
  void speak() => print("...");
}

class Dog extends Animal {
  @override
  void speak() => print("Woof woof");
}

Dog().speak(); // Woof woof

extends inherits fields and methods from the superclass. @override indicates that you are rewriting an inherited method.

Simple enum
enum Status { active, inactive, pending }

Status s = Status.active;
print(s.name);    // "active"
print(s.index);   // 0

if (s == Status.active) {
  print("active!");
}

The enum defines a fixed set of values. .name returns the name the a string and .index the position (starts at 0).

super parameters (Dart 2.17+)
class Animal {
  final String name;
  Animal(this.name);
}

class Dog extends Animal {
  final String breed;
  Dog(super.name, this.breed);
}

var c = Dog("Rex", "Labrador");

super.name in the constructor automatically forwards the argument to the superclass, avoiding writing : super(name).

== operator and hashCode
class Point {
  final int x, y;
  const Point(this.x, this.y);

  @override
  bool operator ==(Object o) =>
      o is Point && o.x == x && o.y == y;

  @override
  int get hashCode => Object.hash(x, y);
}

To compare objects by value, override operator == and hashCode. Object.hash() generates a consistent hash of the fields.

Asynchronous


12 cards
async / await
Future<String> fetch() async {
  var r = await http.get(url);
  return r.body;
}

void main() async {
  var data = await fetch();
  print(data);
}

async marks an asynchronous function and await pauses execution until the Future completes, keeping the code readable.

Future.wait (parallel)
var results = await Future.wait([
  fetchA(),
  fetchB(),
  fetchC(),
]);
print(results); // [a, b, c]

Future.wait() runs several Futures in parallel and awaits all of them. Faster than sequential await when they are independent.

async* and yield*
Stream<int> upTo(int n) async* {
  for (var i = 0; i <= n; i++) {
    yield i;
  }
}

Stream<int> doubled() async* {
  yield* upTo(3).map((e) => e * 2);
}

async* returns a Stream and yield emits values. yield* re-emits all values from another stream.

Future
Future<String> task() {
  return Future.delayed(
    Duration(seconds: 2),
    () => "ready",
  );
}

task().then((v) => print(v));

A Future<T> represents a value available in the future. .then() registers a callback run when the result is ready.

StreamController
final controller = StreamController<int>();

controller.stream.listen((n) => print(n));

controller.add(1);
controller.add(2);
await controller.close();

StreamController lets you emit values manually to a stream via .add(). .listen() subscribes to the events.

sync* (generator)
Iterable<int> fib(int n) sync* {
  int a = 0, b = 1;
  for (var i = 0; i < n; i++) {
    yield a;
    [a, b] = [b, a + b];
  }
}

print(fib(6).toList()); // [0,1,1,2,3,5]

sync* creates a synchronous generator that returns an Iterable. The values are produced lazily the you iterate.

try / catch async
try {
  var data = await fetch();
  print(data);
} catch (e) {
  print("Error: $e");
} finally {
  print("finished");
}

Errors in await code are caught with a normal try/catch. finally always runs, with or without error.

listen and cancellation
var sub = stream.listen(
  (data) => print(data),
  onError: (e) => print("Error: $e"),
  onDone: () => print("end"),
);

// Later:
await sub.cancel();

listen() subscribes to a stream with data, error and done callbacks. Keep the StreamSubscription to cancel with .cancel().

Timeout
try {
  var r = await fetch()
      .timeout(Duration(seconds: 5));
} on TimeoutException {
  print("Took too long");
}

timeout() throws TimeoutException if the Future does not complete in time. Essential for robust network requests.

Stream
Stream<int> count() async* {
  for (var i = 0; i < 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

await for (var n in count()) {
  print(n);
}

A Stream emits multiple values over time. async* with yield produces the values and await for consumes them.

then / catchError
fetch()
  .then((v) => print(v))
  .catchError((e) {
    print("Failed: $e");
    return "fallback";
  })
  .whenComplete(() => print("end"));

then() chains the success, catchError() handles errors (able to return a fallback) and whenComplete() always runs at the end.

Isolates (parallelism)
import 'dart:isolate';

int heavyCalc(int n) {
  var sum = 0;
  for (var i = 0; i < n; i++) sum += i;
  return sum;
}

// Runs in another isolate (thread)
var result = await Isolate.run(
  () => heavyCalc(1000000),
);

Dart is single-threaded per isolate. Isolate.run() runs heavy work in another isolate, avoiding blocking the UI. Each isolate has its own memory.

Null Safety


10 cards
Nullable types (?)
String? name;    // accepts null
name = null;     // OK

String s = "x";
// s = null;     // ERROR: non-nullable

int? number;     // nullable
int age = 0;     // never null

By default types do not accept null. The ? suffix makes the type nullable, allowing you to assign null safely.

required
class User {
  final String email;
  final String? phone;

  User({required this.email, this.phone});
}

var u = User(email: "a@b.com");
// User(); // ERROR: email required

required makes a named parameter mandatory, avoiding unexpected nulls. Parameters without it must be nullable or have a default.

Null-aware in cascade (..?)
StringBuffer? sb = getBuffer();

sb?..write("Hi")
   ..write(" Dart");
// only runs if sb != null

list?..add(1)..sort();

?.. combines null-aware with cascade: the chain only runs if the object is not null, avoiding exceptions in optional configurations.

?? operator (fallback)
String? input;
var name = input ?? "Anonymous";
// "Anonymous" if input is null

int? attempts;
attempts ??= 0; // assigns only if null

?? returns the left operand if it is not null, otherwise the right one. ??= assigns a value only if the variable is null.

late (lazy initialization)
class Service {
  late final ApiClient api;

  Service() {
    api = ApiClient(); // after the constructor
  }
}

late String config = loadConfig();

late lets you declare a non-null variable initialized later. Dart only checks the assignment on the first access to the variable.

Nullable return
int? findIndex(List l, Object v) {
  int i = l.indexOf(v);
  return i >= 0 ? i : null;
}

var idx = findIndex([1, 2], 2);
if (idx != null) print("position $idx");

Functions that can fail return a nullable type (int?). The caller checks with != null before using the result.

?. operator (safe access)
String? name;
int? len = name?.length; // null, no error

// Chained
var city = user?.address?.city;

// With method
list?.add(1); // only calls if list != null

?. stops the chain and returns null instead of throwing an exception if the object is null. Ideal for chained accesses.

Type promotion (flow analysis)
String? name = getName();

if (name != null) {
  // here name is promoted to String
  print(name.length); // no !
}

// Also with is
Object x = "text";
if (x is String) print(x.length);

After checking != null or is, Dart promotes the variable to the non-null type inside the block, no need for the ! operator.

! operator (force non-null)
String? name = "Anna";
int len = name!.length; // asserts non-null

// CAUTION: throws error if null
String? nullValue;
// nullValue!.length; // Null check operator error

! asserts to the compiler that the value is not null. If wrong, it throws an error at runtime. Use only when you are sure.

Nullable collections
List<int>? list;
list?.add(1);            // safe
var l = list ?? [];      // fallback

Map<String, int?> map = {
  "a": 1,
  "b": null, // nullable value
};

A collection can be nullable (List?) or have nullable elements (List<int?>). Use ?. and ?? the needed.

Generics, Records and Patterns


14 cards
Generics in classes
class Stack<T> {
  final List<T> _items = [];

  void push(T item) => _items.add(item);
  T pop() => _items.removeLast();
  bool get empty => _items.isEmpty;
}

var p = Stack<int>();
p.push(1);

Generics (<T>) create reusable classes with type safety. The type is fixed at instantiation, avoiding runtime casts.

Destructuring
var list = [1, 2, 3, 4];
var [a, b, ...rest] = list;
// a=1, b=2, rest=[3,4]

var map = {"x": 10, "y": 20};
var {"x": x, "y": y} = map;

Dart 3 lets you destructure lists and maps into individual variables. ...rest captures the remaining elements in a new list.

Multiple type parameters
Map<K, V> invert<K, V>(Map<V, K> map) {
  return map.map((k, v) => MapEntry(v, k));
}

var original = {1: "one", 2: "two"};
var inverted = invert(original);
// {"one": 1, "two": 2}

Functions can have several type parameters (<K, V>). The compiler infers them from the passed arguments.

Libraries and imports
import 'dart:math';
import 'package:http/http.dart' the http;
import 'utils.dart' show format;
import 'other.dart' hide internal;

export 'model.dart';

import brings in libraries; the creates a prefix, show/hide filter symbols. export re-exports from another library.

Generic constraints (bounds)
T greater<T extends Comparable<T>>(T a, T b) {
  return a.compareTo(b) > 0 ? a : b;
}

print(greater(5, 10));       // 10
print(greater("a", "z"));    // "z"

T extends Comparable<T> restricts the generic to comparable types. The compiler guarantees that compareTo() exists.

Switch with patterns
String describe(Object value) => switch (value) {
  int n => "integer $n",
  String s => "text $s",
  [1, 2] => "list [1,2]",
  { "name": var n } => "map name=$n",
  _ => "other",
};

switch with patterns matches by type, value and structure. It extracts values directly into variables (e.g. int n, var n).

Object vs dynamic vs void
Object a = 1;     // any type, safe
dynamic b = 1;    // no checking (risky)
void c = func();  // do not use the value

// a.methodX(); // compile-time ERROR
b.methodX();     // compiles, fails at runtime

Object is the typed root (safe), dynamic disables checks and void indicates that the value should not be used.

Parts and library extension
// lib.dart
library my_lib;
part 'part_a.dart';
part 'part_b.dart';

// part_a.dart
part of 'lib.dart';

void funcA() {}

part and part of split a library into several files that share the same scope. Useful for organizing large codebases.

Records (Dart 3)
(String, int) person = ("Anna", 30);
print(person.$1); // Anna
print(person.$2); // 30

// With names
({String name, int age}) p =
    (name: "Anna", age: 30);
print(p.name);

records group several values without creating a class. They can be positional ($1, $2) or named, and are immutable.

Guards (when)
String classify(int n) => switch (n) {
  < 0 => "negative",
  0 => "zero",
  > 0 && < 10 => "small",
  _ => "large",
};

// Guard with when
var r = switch (value) {
  int x when x.isEven => "even",
  _ => "odd",
};

relational patterns (<, >) and the when guard allow refined conditions within the switch cases.

Compile-time constants
const list = [1, 2, 3];
const map = {"a": 1};
const point = Point(1, 2); // const constructor

// Deep const: everything is immutable
const nested = [1, [2, 3]];
// nested[1].add(4); // ERROR

const creates immutable values at compile time, including nested collections (deep const). It reduces allocations and improves performance.

Records the return
(double, double) divide(int a, int b) {
  return (a / b, a % b.toDouble());
}

var (quotient, remainder) = divide(10, 3);
print("$quotient remainder $remainder");

Functions can return several values via records, without creating a class. The destructuring var (a, b) = ... extracts the values.

typedef (type aliases)
typedef Callback = void Function(String);
typedef Comparator<T> = int Function(T, T);

void register(Callback cb) {
  cb("ok");
}

register((msg) => print(msg));

typedef creates an alternative name for a type, useful for complex function signatures and reusable generics.

assert
void setAge(int age) {
  assert(age >= 0, "Invalid age");
  // only active in debug mode
}

assert(list.isNotEmpty);
assert(x > 0, "x must be positive");

assert checks a condition in debug mode and throws AssertionError if false. It is ignored in production (does not affect performance).

Exceptions and Errors


10 cards
try / catch
try {
  int r = 10 ~/ 0;
} catch (e) {
  print("Error: $e");
}

// Catch a specific type
try {
  risky();
} on FormatException {
  print("invalid format");
}

try/catch catches exceptions at runtime. on filters by exception type, allowing specific handling for each case.

Custom exception
class InsufficientBalance implements Exception {
  final double amount;
  InsufficientBalance(this.amount);

  @override
  String toString() =>
      "Insufficient balance: $amount";
}

throw InsufficientBalance(100.0);

Create classes that implement Exception for domain-specific errors. Override toString() for clear messages.

tryParse (avoid exceptions)
// Instead of try/catch:
int? n = int.tryParse("abc"); // null
if (n != null) {
  print("number: $n");
}

double? d = double.tryParse("3.14");

tryParse() methods return null instead of throwing an exception. Preferable for input validation, cleaner and more efficient.

finally
try {
  var data = await readFile();
  process(data);
} catch (e) {
  print("Failed: $e");
} finally {
  closeResources(); // always runs
}

The finally block always runs, with or without an exception. Ideal for releasing resources like files, connections or subscriptions.

Capturing the stack trace
try {
  operation();
} catch (e, stack) {
  print("Error: $e");
  print("Stack: $stack");
}

The second catch parameter (stack) is the StackTrace, useful for debugging where the exception occurred.

Errors vs Exceptions
// Error: programming bug (do not recover)
// StateError, ArgumentError, RangeError

// Exception: expected condition (recoverable)
// FormatException, IOException, HttpException

throw StateError("invalid state");
throw FormatException("bad input");

Error indicates bugs (should not normally be caught). Exception represents expected and recoverable program conditions.

throw (throw exception)
void setAge(int age) {
  if (age < 0) {
    throw ArgumentError("Invalid age: $age");
  }
}

void save(Object? v) {
  if (v == null) throw Exception("empty");
}

throw throws an exception. You can use native types like ArgumentError or Exception, or create your own.

Common exceptions
int.parse("abc");   // FormatException
[1,2][5];           // RangeError
null!.length;       // TypeError (null check)
10 ~/ 0;            // UnsupportedError
{}["x"]!;           // null check operator

Frequent exceptions: FormatException (invalid parse), RangeError (index out of range), TypeError (wrong type/null).

rethrow
try {
  operation();
} catch (e) {
  print("log: $e");
  rethrow; // re-throws the same exception
}

rethrow re-throws the current exception preserving the original stack trace. Useful for logging the error before propagating it.

runZonedGuarded
import 'dart:async';

runZonedGuarded(() {
  // code that may throw errors
  asyncOperation();
}, (error, stack) {
  print("Global error: $error");
});

runZonedGuarded() catches unhandled errors within a zone, including asynchronous ones. Useful the a global error handler.