Getting Started
Hello World & Comments
main() is the entry point of every Dart program. Use // for single-line, /* */ for block, and /// for documentation comments. Doc comments support Markdown and are consumed by dartdoc to generate API documentation.
// Hello World in Dart
void main() {
print('Hello, World!');
}
/* Multi-line
block comment */
/// Documentation comment
/// Supports Markdown
void greet(String name) {
print('Hi, $name');
}Variables: var, final, const
Use var for inferred types, final for runtime constants (assigned once), const for compile-time constants. const is stricter—DateTime.now() cannot be const because its value isn't known at compile time. Prefer final/const over var when the value won't change.
var name = 'Alice'; // type inferred
String city = 'NYC'; // explicit type
final age = 30; // runtime constant
const PI = 3.14; // compile-time constant
final now = DateTime.now(); // OK: runtime value
// const time = DateTime.now(); // ERROR: not compile-time
const list = [1, 2, 3]; // const list (immutable)
final list2 = [4, 5, 6]; // final ref, mutable contentNull Safety Basics
Dart has sound null safety. Append ? to make a type nullable (int? can hold null; int cannot). Use ?? for default values, ??= to assign-if-null, ?. for safe access, ! to assert non-null. Non-nullable types are guaranteed non-null, eliminating NullPointerException at compile time.
int value = 42; // non-nullable
int? nullableValue; // nullable (default null)
print(nullableValue); // null
print(nullableValue ?? 0); // 0 (null coalescing)
nullableValue ??= 10; // assign if null
print(nullableValue); // 10
// int x = null; // ERROR: non-nullable
String name = 'Alice';
print(name.length); // safe, no null checkOutput & Input
print() outputs to stdout with a newline and takes a single argument. Use stdout.write() to suppress the newline. stdin.readLineSync() reads a line from stdin and returns a nullable String. dart:io is not available on the web platform.
import 'dart:io';
void main() {
print('Hello'); // stdout with newline
stdout.write('no newline'); // no trailing newline
stderr.writeln('an error'); // stderr with newline
String? input = stdin.readLineSync(); // read a line
int? n = int.tryParse(input ?? ''); // safe parse
print('You entered: $n');
}Type Inference & dynamic
var lets Dart infer the type at compile time (the type cannot change afterward). dynamic disables static type checking—use sparingly. Object is the supertype of all non-null types. Use 'as' to cast, but prefer type-safe checks. dynamic is the only type that permits unknown method calls at compile time.
var x = 10; // inferred as int
var y = 3.14; // inferred as double
var s = 'hi'; // inferred as String
dynamic d = 10; // type can change
d = 'now string'; // OK
// d.foo(); // compiles, may fail at runtime
Object o = 'hello'; // supertype of all non-null types
// o.length; // ERROR: Object has no length
print((o as String).length); // 5 (cast)Data Types
Numbers (int, double, num)
num is the supertype of int and double. ~/ is integer division, % or remainder() for modulo. / always returns a double even for int operands. int has bitLength, toRadixString(); double has toStringAsFixed(). On the web target all numbers compile to JS numbers.
int a = 42;
double b = 3.14;
num c = 10; // num is supertype of int & double
num d = 2.71;
print(a.bitLength); // 6
print(b.toStringAsFixed(2)); // '3.14'
print(10 ~/ 3); // 3 (integer division)
print(10.remainder(3)); // 1
print(0xFF); // 255 (hex)
print(1.5e3); // 1500.0 (scientific)Strings
Strings are immutable sequences of UTF-16 code units. Use single or double quotes interchangeably. Triple quotes allow multiline. $var interpolates a variable, ${expr} an expression. The r prefix creates raw strings (no escape processing). Interpolation is preferred over concatenation.
var s1 = 'single';
var s2 = "double";
var s3 = '''multi
line string''';
var name = 'Alice';
print('Hi, $name'); // interpolation
print('Length: ${name.length}'); // expression interpolation
var raw = r'No escape: \n'; // raw string (literal)
var escaped = 'It\'s ok'; // escaped quote
print(s1[0]); // 's' (index access)Booleans
Dart has only two bool values: true and false. Unlike JavaScript there is no truthy/falsy coercion—conditions require an actual bool. Use .isNotEmpty / .isEmpty for collections and strings instead of relying on truthiness.
bool isTrue = true;
bool isFalse = false;
print(!isTrue); // false
print(isTrue && isFalse); // false
print(isTrue || isFalse); // true
// Only bool is allowed in conditions; no truthy/falsy
// if (1) {} // ERROR: must be bool
if ('text'.isNotEmpty) {
print('non-empty');
}Lists
Lists are ordered, growable collections (like arrays). Specify the element type with <Type>[]. Use const for immutable lists. The spread operator ... expands a list into another. Lists use zero-based indexing and are the most common Dart collection.
var list = [1, 2, 3];
var typed = <String>['a', 'b'];
var constList = const [1, 2, 3];
list.add(4);
list.addAll([5, 6]);
print(list.length); // 6
print(list[0]); // 1
print(list.sublist(1, 3)); // [2, 3]
var spread = [...list, 7]; // spread
// constList.add(0); // ERROR: immutableMaps
Maps are key-value pairs. Keys and values can be any type; specify with <KeyType, ValueType>{}. Access via map[key] returns null if the key is missing—use ?? for a default. Maps preserve insertion order. entries, keys, and values expose iterables.
var map = {
'name': 'Alice',
'age': 30,
};
var typed = <String, int>{'a': 1, 'b': 2};
map['city'] = 'NYC'; // add entry
print(map['name']); // Alice
print(map.length); // 3
print(map.containsKey('age')); // true
map.forEach((k, v) => print('$k: $v'));
var keys = map.keys.toList(); // [name, age, city]Sets & Runes
Sets are unordered collections of unique elements—useful for deduplication and set operations (union, intersection, difference). Runes expose the Unicode code points of a string, which is needed for emoji and non-BMP characters stored as surrogate pairs in UTF-16.
var set = {1, 2, 3};
set.add(2); // no duplicate added
set.add(4);
print(set); // {1, 2, 3, 4}
print(set.contains(2)); // true
print(set.intersection({2, 3, 5})); // {2, 3}
// Runes (Unicode code points)
var heart = '♥'; // ♥
print(heart); // ♥
print('A'.codeUnitAt(0)); // 65Operators
Arithmetic Operators
~/ is Dart's integer division operator (truncates to int). % is modulo. / always returns a double even for int operands. Arithmetic operators work on num, int, and double. Use ~/ when you need an integer result from division.
print(5 + 3); // 8
print(5 - 3); // 2
print(5 * 3); // 15
print(5 / 3); // 1.6666... (double)
print(5 ~/ 3); // 1 (integer division)
print(5 % 3); // 2 (modulo)
print(-(5)); // -5 (unary minus)
print(2.toString()); // '2'Increment & Decrement
++ and -- have prefix and postfix forms. Postfix (i++) returns the original value then increments; prefix (++i) increments then returns the new value. Behavior matches C/Java. Avoid mixing these into complex expressions for readability.
var i = 5;
print(i++); // 5 (postfix: use, then add)
print(i); // 6
print(++i); // 7 (prefix: add, then use)
print(i--); // 7
print(--i); // 5Relational & Equality
== compares values for built-in types (strings, numbers). For lists and most objects, == compares references by default unless overridden. Use listEquals() from package:collection or flutter/foundation.dart for deep equality of collections.
print(3 == 3); // true
print(3 != 4); // true
print(3 < 4); // true
print(3 > 4); // false
print(3 <= 3); // true
print(3 >= 4); // false
print('a' == 'a'); // true (content equality)
var l1 = [1, 2];
var l2 = [1, 2];
print(l1 == l2); // false (reference equality)Logical Operators
&& (AND), || (OR), and ! (NOT) operate only on bool. Both && and || short-circuit: && stops if the left side is false, || stops if the left side is true. Unlike some languages, operands must be bool—no truthy/falsy coercion.
bool a = true, b = false;
print(a && b); // false
print(a || b); // true
print(!a); // false
// short-circuit evaluation
bool check() { print('called'); return true; }
false && check(); // check() NOT called
true || check(); // check() NOT calledType Test: is, as
is checks the type at runtime and enables smart-casting inside the branch (no explicit cast needed). is! is the negation. as performs an unsafe cast that throws TypeError if the type doesn't match. Prefer is checks over as casts for safety.
Object x = 'hello';
print(x is String); // true
print(x is! int); // true
if (x is String) {
print(x.length); // smart-cast to String
}
Object y = 42;
print((y as int) + 1); // 43 (cast)
// (y as String); // runtime TypeErrorCascade (..) & Null-aware
.. is the cascade operator—it invokes a method or sets a field and returns the original object, enabling fluent chaining without repeating the variable. ?. is the null-aware access operator (returns null if the target is null). Combine with ?? for defaults.
class Builder {
String? name;
int? size;
Builder setName(String n) { name = n; return this; }
Builder setSize(int s) { size = s; return this; }
}
var b = Builder()
..setName('widget')
..setSize(10);
print(b.name); // widget
String? path;
print(path?.length); // null (safe access)
print(path?.length ?? 0); // 0Control Flow
if-else
if-else conditions must evaluate to bool—no truthy/falsy coercion. The ternary operator ?: works like other C-family languages. else-if chains are common. Dart 3 also supports if as an expression inside collection literals (collection-if).
int score = 85;
if (score >= 90) {
print('A');
} else if (score >= 80) {
print('B');
} else {
print('C');
}
// ternary expression
var grade = score >= 60 ? 'pass' : 'fail';
print(grade); // pass
// if (score) {} // ERROR: condition must be boolfor & for-in
Classic C-style for loops use init; condition; update. for-in iterates any Iterable. For Maps, iterate .entries, .keys, or .values. for-in is preferred when you don't need the index. ${entry.key} uses expression interpolation.
for (var i = 0; i < 3; i++) {
print(i);
}
var list = ['a', 'b', 'c'];
for (var item in list) {
print(item);
}
// for-in with Map entries
var map = {'x': 1, 'y': 2};
for (var entry in map.entries) {
print('${entry.key}: ${entry.value}');
}while & do-while
while checks the condition before each iteration; do-while checks after, so the body always executes at least once. Both require bool conditions. Use while when the iteration count is unknown ahead of time.
var i = 0;
while (i < 3) {
print('while $i');
i++;
}
var j = 0;
do {
print('do $j');
j++;
} while (j < 3);switch (Dart 3)
Dart 3 introduced switch expressions which return a value and use => per case. Patterns support OR (||), AND (&&), relational (>=), and wildcard (_). Classic switch statements require break (no fall-through). Exhaustive matching is required for sealed types and enums.
// Classic switch statement
var color = 'red';
switch (color) {
case 'red':
print('stop');
break;
case 'green':
print('go');
break;
default:
print('unknown');
}
// Dart 3 switch expression
String describe(int n) => switch (n) {
0 => 'zero',
1 || 2 => 'small',
>= 3 && <= 10 => 'medium',
_ => 'large',
};
print(describe(5)); // mediumbreak & continue
break exits the nearest loop; continue skips to the next iteration. Labels (outer:) allow break/continue to target an outer loop—use sparingly because it reduces readability. break is also used to end switch cases.
for (var i = 0; i < 5; i++) {
if (i == 2) continue; // skip 2
if (i == 4) break; // stop at 4
print(i); // 0, 1, 3
}
// labels for nested loops
outer:
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
if (i + j > 2) break outer;
print('$i,$j');
}
}assert
assert(condition, message) checks a condition during development. Assertions are enabled in debug mode and removed in production (release) builds. Use them for internal invariants and debugging—not for input validation that must run in production.
void setAge(int age) {
assert(age >= 0, 'age must be non-negative');
// ...
}
assert(1 == 1); // passes in debug
// assert(1 == 2, 'math is broken'); // fails in debug
// Assertions are stripped in release/production buildsFunctions
Function Declaration
In Dart, functions are first-class objects—they can be assigned to variables, passed as arguments, and returned. Type aliases like int Function(int) describe function types. Function declarations include optional return type and parameter types.
// named function with return type
int add(int a, int b) {
return a + b;
}
// functions are first-class objects
int Function(int) makeAdder(int n) {
return (int x) => x + n;
}
var add5 = makeAdder(5);
print(add5(3)); // 8
print(add(2, 3)); // 5Arrow Functions
The => (arrow) syntax is shorthand for { return expr; } and is used for single-expression functions. It's common for short methods, getters, and callbacks. Arrow functions make code concise without sacrificing type safety.
// single-expression function with =>
int square(int x) => x * x;
String greet(String name) => 'Hi, $name';
// arrow with nullable
String? firstChar(String? s) => s?.isEmpty ?? true ? null : s[0];
print(square(4)); // 16
print(greet('Al')); // Hi, Al
print(firstChar('hi')); // hOptional Positional Parameters
Optional positional parameters are wrapped in square brackets [] and must come after required parameters. They default to null (or a provided default value). Use them when parameter order is intuitive and obvious.
// optional positional params wrapped in []
String greet(String name, [String? title]) {
if (title != null) {
return 'Hello, $title $name';
}
return 'Hello, $name';
}
print(greet('Alice')); // Hello, Alice
print(greet('Bob', 'Dr.')); // Hello, Dr. BobNamed Parameters
Named parameters are wrapped in {} and passed by name at the call site (order-independent). Use required to mark a named parameter as mandatory. Named parameters default to null unless a default value is given. Preferred for APIs with many optional parameters.
// named params wrapped in {}, required marks mandatory
void createUser({
required String name,
int age = 0,
String? email,
}) {
print('$name, $age, $email');
}
createUser(name: 'Alice', age: 30);
createUser(name: 'Bob', email: '[email protected]');
// named params are passed by name, order-independentDefault Values
Both optional positional and named parameters can have default values, specified with =. Default values must be compile-time constants. Default values make optional parameters safe by avoiding null when a sensible default exists.
// default values for optional positional
double calc(double a, [double b = 1.0, double c = 0.0]) {
return a * b + c;
}
// default values for named
void config({String host = 'localhost', int port = 8080}) {
print('$host:$port');
}
print(calc(5)); // 5.0
print(calc(5, 2)); // 10.0
config(port: 3000); // localhost:3000Anonymous Functions & Closures
Anonymous functions (lambdas) have no name and are often assigned to variables or passed as callbacks. Closures capture variables from their enclosing scope and keep them alive. The parameter list can be typed (int a, int b) or untyped (a, b).
// anonymous function assigned to a variable
var multiply = (int a, int b) => a * b;
print(multiply(3, 4)); // 12
// closure capturing a variable
Function counter() {
int count = 0;
return () => ++count;
}
var c = counter();
print(c()); // 1
print(c()); // 2
// used as callbacks
[1, 2, 3].forEach((n) => print(n));Classes & Objects
Class Definition
Classes group fields (state) and methods (behavior). The constructor Person(this.name, this.age) uses parameter initializers shorthand to assign fields directly. Unlike Java, Dart classes have a single constructor body; use named constructors for variants.
class Person {
String name;
int age;
// constructor
Person(this.name, this.age);
void greet() {
print('Hi, I am $name');
}
}
var p = Person('Alice', 30);
p.greet(); // Hi, I am Alice
print(p.name); // AliceInstance Variables & this
Instance variables (fields) hold per-object state. Use this only when disambiguating from parameters or for clarity. All non-nullable fields must be initialized—via constructor initializers, this.x shorthand, or default values. Fields generate implicit getters/setters.
class Point {
double x;
double y;
Point(this.x, this.y);
double distanceTo(Point other) {
return ((x - other.x) * (x - other.x) +
(y - other.y) * (y - other.y));
}
void moveBy(double dx, double dy) {
this.x += dx; // 'this' is optional when unambiguous
y += dy;
}
}Getters & Setters
Getters and setters, defined with get/set keywords, let you expose computed properties that look like fields to callers. Use them to validate input or compute derived values without changing the public API. Final fields cannot have setters.
class Rectangle {
double width, height;
Rectangle(this.width, this.height);
// computed getter
double get area => width * height;
set size(double v) {
width = v;
height = v;
}
}
var r = Rectangle(3, 4);
print(r.area); // 12 (accessed like a field)
r.size = 10;
print(r.area); // 100Static Members
Static members (fields and methods) belong to the class itself, not instances. They're accessed via ClassName.member. Use static for utility functions and constants shared across all instances. Static members cannot reference non-static members without an instance.
class MathUtils {
static const double PI = 3.14159;
static double circleArea(double r) => PI * r * r;
}
// accessed via the class, not an instance
print(MathUtils.PI); // 3.14159
print(MathUtils.circleArea(2)); // 12.566
// static members belong to the class, not instancesFactory Constructor
A factory constructor (factory keyword) doesn't always create a new instance—it can return a cached one, a subtype, or a pre-built object. Useful for singletons, caches, and returning subclasses. Factory constructors use the class name like normal constructors.
class Logger {
final String name;
static final Map<String, Logger> _cache = {};
// factory may return a cached instance
factory Logger(String name) {
return _cache.putIfAbsent(name, () => Logger._internal(name));
}
Logger._internal(this.name);
}
var a = Logger('app');
var b = Logger('app');
print(identical(a, b)); // true (same cached instance)Object & toString
Every class inherits from Object, which provides toString(), ==, hashCode, and runtimeType. Override toString() for readable debugging. If you override ==, you must also override hashCode to maintain the contract for use in Maps and Sets.
class Book {
String title;
Book(this.title);
@override
String toString() => 'Book($title)';
@override
bool operator ==(Object other) =>
other is Book && other.title == title;
@override
int get hashCode => title.hashCode;
}
print(Book('Dart')); // Book(Dart)
print(Book('A') == Book('A')); // trueConstructors
Default & Generative Constructor
If you don't define any constructor, Dart provides a default no-argument constructor. A generative constructor creates a new instance. The this.field shorthand in parameters assigns fields directly. Once you define a constructor, the default is no longer generated.
class Animal {
String species;
// generative constructor
Animal(this.species);
// if no constructor is written, Dart provides:
// Animal() : species = 'unknown';
}
var a = Animal('cat');
print(a.species); // catNamed Constructors
Named constructors (ClassName.name) let a class have multiple constructors for different creation patterns. The initializer list (: field = value) runs before the constructor body and can initialize final fields. Useful for factory-style creation and conversion constructors.
class Point {
double x, y;
Point(this.x, this.y);
// named constructor
Point.origin() : x = 0, y = 0;
Point.fromList(List<double> l) : x = l[0], y = l[1];
}
var p1 = Point.origin();
var p2 = Point.fromList([3, 4]);
print('${p1.x},${p1.y}'); // 0.0,0.0
print('${p2.x},${p2.y}'); // 3.0,4.0Initializer List
The initializer list (: expr, expr) runs before the constructor body and can set final fields and run asserts. It's the only place to initialize final fields (other than this.field shorthand). Use it for validation and computed field values.
class Temperature {
final double celsius;
// initializer list runs before body
Temperature(double c) : celsius = c;
Temperature.fromFahrenheit(double f)
: celsius = (f - 32) * 5 / 9;
// assert in initializer list
Temperature.clamped(double c)
: assert(c >= -273.15),
celsius = c < -273.15 ? -273.15 : c;
}
print(Temperature.fromFahrenheit(32).celsius); // 0.0Redirecting Constructors
A redirecting constructor forwards to another constructor in the same class using 'this(...)'. It has no body and no initializer list of its own. Useful for providing convenience constructors that delegate to a primary constructor.
class Point {
double x, y;
Point(this.x, this.y);
// redirect to another constructor with 'this'
Point.alongX(double x) : this(x, 0);
Point.origin() : this(0, 0);
Point.fromDouble(double n) : this.alongX(n);
}
print(Point.alongX(5).y); // 0.0
print(Point.origin().x); // 0.0Constant Constructor
A const constructor (const ClassName) creates compile-time constant instances. All fields must be final. Identical const instances are canonicalized (shared). const constructors enable deeply immutable objects and compile-time constant collections.
class ImmutablePoint {
final double x;
final double y;
// const constructor: creates compile-time constant instances
const ImmutablePoint(this.x, this.y);
static const origin = ImmutablePoint(0, 0);
}
const p = ImmutablePoint(1, 2);
const o = ImmutablePoint.origin;
print(identical(o, ImmutablePoint(0, 0))); // trueFactory & Caching
Factory constructors can return const or cached instances and may use Dart 3 switch expressions. They differ from generative constructors which always create a new instance. Use factories when construction logic must choose what to return.
class Shape {
final String type;
const Shape._(this.type);
factory Shape(String kind) {
return switch (kind) {
'circle' => const Shape._('circle'),
'square' => const Shape._('square'),
_ => const Shape._('unknown'),
};
}
}
print(Shape('circle').type); // circle
print(identical(Shape('circle'), Shape('circle'))); // trueInheritance
extends & super
Use extends to inherit from a single superclass (Dart is single-inheritance). The subclass constructor must call a super constructor (often via super(...) in the initializer list). Use @override to mark methods that replace the superclass implementation.
class Animal {
String name;
Animal(this.name);
void speak() => print('$name makes a sound');
}
class Dog extends Animal {
Dog(String name) : super(name);
@override
void speak() => print('$name barks');
}
var d = Dog('Rex');
d.speak(); // Rex barks@override & super calls
@override is a metadata annotation marking that a method overrides a superclass member. It triggers a compile-time check that the parent method exists. super.method() calls the parent implementation from within an overridden method, allowing extension of behavior.
class Vehicle {
int speed = 0;
void accelerate(int by) => speed += by;
void describe() => print('Vehicle at $speed');
}
class Car extends Vehicle {
@override
void accelerate(int by) {
super.accelerate(by); // call parent method
print('Car now at $speed');
}
}
Car().accelerate(10); // Car now at 10Constructor Chaining
In inheritance, the superclass constructor runs before the subclass body. The super(...) call in the initializer list passes arguments up. Execution order: initializer list -> super constructor -> this constructor body. This guarantees the parent is fully initialized first.
class Base {
String tag;
Base(this.tag) {
print('Base created: $tag');
}
}
class Derived extends Base {
Derived(String tag) : super(tag) {
print('Derived created');
}
}
// order: super initializer -> super body -> derived body
Derived('x');
// Base created: x
// Derived creatednoSuchMethod
noSuchMethod is invoked when code attempts to call a method or access a field that doesn't exist on an object. Override it to implement dynamic proxies or mock objects. Returning a value lets the call continue; the default throws a NoSuchMethodError.
class Proxy implements Object {
@override
dynamic noSuchMethod(Invocation inv) {
print('Called: ${inv.memberName}');
return null;
}
}
var p = Proxy();
p.someMissingMethod(); // Called: Symbol("someMissingMethod")Sealed Classes (Dart 3)
Sealed classes (Dart 3) are closed hierarchies: all direct subtypes must be in the same library. The compiler enforces exhaustive switch matching over a sealed type's subtypes, making pattern matching safe. Perfect for modeling fixed sets of variants (like a Result/Either type).
sealed class Shape {
const Shape();
}
class Circle extends Shape {
final double r;
const Circle(this.r);
}
class Square extends Shape {
final double s;
const Square(this.s);
}
// switch is exhaustive over sealed subtypes
double area(Shape s) => switch (s) {
Circle(:var r) => 3.14 * r * r,
Square(:var s) => s * s,
};
print(area(const Circle(2))); // 12.56Abstract Classes & Interfaces
Abstract Class
Abstract classes (abstract class) cannot be instantiated directly and may contain abstract methods (no body) that subclasses must implement. They can also have concrete methods. Use abstract classes to define a partial implementation shared by subclasses.
abstract class Animal {
// abstract method: no body, must be overridden
void makeSound();
// concrete method: inherited as-is
void breathe() => print('breathing');
}
class Cat extends Animal {
@override
void makeSound() => print('meow');
}
var c = Cat();
c.makeSound(); // meow
// Animal(); // ERROR: cannot instantiate abstract classImplicit Interface
Every Dart class implicitly defines an interface containing all its members. Use implements to implement that interface—you must provide bodies for every member (no code is inherited). A class can implement multiple interfaces, separated by commas.
class Television {
void turnOn() => print('on');
void turnOff() => print('off');
}
// every class implicitly defines an interface
class SmartTV implements Television {
@override
void turnOn() => print('smart on');
@override
void turnOff() => print('smart off');
}
SmartTV().turnOn(); // smart onimplements Multiple
A class can implement multiple interfaces, unlike extends which allows only one superclass. With implements you inherit the type contract but no implementation—every member must be overridden. This enables a form of multiple inheritance of type.
abstract class Flyer {
void fly();
}
abstract class Swimmer {
void swim();
}
// implement multiple interfaces
class Duck implements Flyer, Swimmer {
@override
void fly() => print('flying');
@override
void swim() => print('swimming');
}
Duck().fly(); // flying
Duck().swim(); // swimmingextends vs implements
extends reuses the parent's implementation (single inheritance). implements inherits only the type contract—every member must be re-implemented, but you can implement multiple interfaces. Use extends for 'is-a' with code reuse; implements for 'can-do' contracts.
class Base {
void greet() => print('hello');
void wave() => print('waving');
}
// extends: reuse implementation, single parent
class A extends Base {
@override
void greet() => print('A says hi');
}
// implements: contract only, must override all
class B implements Base {
@override
void greet() => print('B says hi');
@override
void wave() => print('B waving');
}Abstract vs Interface Guidelines
Use abstract classes to share implementation among closely related types (inheritance of code). Use interfaces (abstract classes used via implements) to define capabilities or contracts that unrelated types can fulfill. Dart merges these concepts: an abstract class can serve as both.
// Abstract class: share code among related types
abstract class Repository<T> {
T? find(int id); // abstract
void save(T item) => print('saved'); // shared
}
// Interface: define a capability
abstract class Comparable<T> {
int compareTo(T other);
}
class Product extends Repository<Product>
implements Comparable<Product> {
@override
Product? find(int id) => null;
@override
int compareTo(Product other) => 0;
}Mixins
Define Mixin
A mixin (mixin keyword) is a reusable unit of behavior that can be mixed into classes using 'with'. Mixins cannot have constructors and are not instantiated directly. They're ideal for sharing horizontal functionality across unrelated classes.
mixin Greeter {
String get name;
void greet() => print('Hello, $name!');
}
class User with Greeter {
@override
String name;
User(this.name);
}
User('Alice').greet(); // Hello, Alice!Using Mixins (with)
Apply mixins with the 'with' keyword followed by one or more mixins (comma-separated). A class can mix in multiple mixins, and the order matters when mixins override each other. Mixins are applied linearly to the class hierarchy.
mixin Walker {
void walk() => print('walking');
}
mixin Talker {
void talk() => print('talking');
}
// mix in multiple mixins
class Person extends Object with Walker, Talker {
String name;
Person(this.name);
}
var p = Person('Bob');
p.walk(); // walking
p.talk(); // talkingMixin Constraints (on)
The 'on' clause constrains a mixin to only be used with classes that extend (or implement) the specified supertype. This lets the mixin call methods from that supertype. The constraint type acts like a required base for the mixin's behavior.
mixin Musician on Performer {
void playNote() => print('playing note');
}
abstract class Performer {
void perform();
}
class Singer extends Performer with Musician {
@override
void perform() => print('singing');
}
Singer().playNote(); // playing note
// Musician can only be mixed into Performer subtypesmixin class (Dart 3)
Dart 3 introduced 'mixin class' which can be used both as a mixin (with) and as a regular class (extends/implements). A plain 'mixin' cannot be extended, and a plain 'class' cannot be mixed in—mixin class bridges both. Useful for sharing behavior that also needs normal class semantics.
// 'mixin class' can be both extended and mixed in
mixin class Counter {
int _count = 0;
int get count => _count;
void increment() => _count++;
}
class App extends Counter {}
class Tool with Counter {}
print(App().count); // 0
App().increment();
Tool().increment();
print(App().count); // 0 (separate instance)Mixin Method Resolution
When multiple mixins define the same method, the rightmost (last) mixin in the 'with' clause wins, because mixins are applied left-to-right with later ones overriding earlier ones. The class's own methods override all mixins. This linearization determines method resolution.
mixin A {
void hello() => print('A');
}
mixin B {
void hello() => print('B');
}
class X with A, B {}
class Y with B, A {}
X().hello(); // B (later mixin wins)
Y().hello(); // A (later mixin wins)
// resolution order: class -> last mixin -> ... -> first mixinGenerics
Generic Class
Generics allow classes and methods to operate on any type while preserving type safety. <T> declares a type parameter. The same Stack class works for int, String, or any type without casts. Dart generics are reified (type info is available at runtime), unlike Java's erasure.
class Stack<T> {
final List<T> _items = [];
void push(T item) => _items.add(item);
T pop() => _items.removeLast();
bool get isEmpty => _items.isEmpty;
}
var s = Stack<int>();
s.push(1);
s.push(2);
print(s.pop()); // 2
var names = Stack<String>();
names.push('Al');Generic Method
Methods can have their own type parameters independent of the class. Type arguments are usually inferred from the parameters. Generic methods provide type-safe utilities like firstOrDefault, where the return type depends on the input types.
// generic method with its own type parameter
T firstOrDefault<T>(List<T> list, T defaultValue) {
return list.isEmpty ? defaultValue : list.first;
}
print(firstOrDefault<int>([1, 2, 3], 0)); // 1
print(firstOrDefault([], 'none')); // none (type inferred)
// type often inferred from argumentsGeneric Constraints (extends)
Use 'extends' to constrain a type parameter to a supertype (bound). The constraint lets you call methods of the bound type inside the generic. 'T extends num' ensures T supports arithmetic. Without a constraint, T is treated as Object? and very few methods are available.
class Comparable<T> {
int compareTo(T other);
}
// constrain T to subtypes of Comparable<T>
T max<T extends Comparable<T>>(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
// number sum constraint
num sum<T extends num>(List<T> nums) =>
nums.fold(0, (a, b) => a + b);
print(sum([1, 2.5, 3])); // 6.5Generic Collections
Dart collections are generic and type information is preserved at runtime (reified). 'is List<int>' works because generics are not erased. Dart collections are covariant: List<int> can be passed where List<num> is expected, which is convenient but can throw at runtime on writes.
List<int> nums = [1, 2, 3];
Map<String, int> scores = {'a': 1, 'b': 2};
Set<double> uniq = {1.1, 2.2, 1.1};
// runtime type checks work (reified generics)
print(nums is List<int>); // true
print(nums is List<String>); // false
print(scores is Map); // true
// generic variance
void process(List<num> list) => print(list);
process(nums); // OK: int is a numGeneric Typedef
Typedefs can be generic, creating reusable type aliases for generic function types or class types. Transformer<T> aliases a function T->T. This is invaluable for declaring callback contracts in libraries. Generic typedefs keep complex type signatures readable.
// generic function type alias
typedef Transformer<T> = T Function(T input);
int doubler(int x) => x * 2;
String upper(String s) => s.toUpperCase();
Transformer<int> dt = doubler;
Transformer<String> ut = upper;
print(dt(5)); // 10
print(ut('hi')); // HI
// generic class alias
typedef IntList = List<int>;
IntList xs = [1, 2, 3];Collections (List/Set/Map)
List Operations
Lists support rich functional operations: where (filter), map (transform), fold/reduce (accumulate), sort, reversed. These return Iterables—call .toList() to materialize. Operations are lazy when chained on Iterables. List is Dart's workhorse ordered collection.
var nums = [3, 1, 2];
nums.sort();
print(nums); // [1, 2, 3]
print(nums.reversed.toList()); // [3, 2, 1]
print(nums.indexOf(2)); // 1
print(nums.contains(3));// true
print(nums.where((n) => n > 1).toList()); // [2, 3]
print(nums.map((n) => n * 2).toList()); // [2, 4, 6]
print(nums.fold(0, (a, b) => a + b)); // 6Set Operations
Sets provide mathematical set operations: union, intersection, difference. Convert a List to a Set to remove duplicates, then back to a List with toList(). Sets are unordered collections of unique elements—ideal for membership testing and deduplication.
var a = {1, 2, 3};
var b = {2, 3, 4};
print(a.union(b)); // {1, 2, 3, 4}
print(a.intersection(b)); // {2, 3}
print(a.difference(b)); // {1}
var dedup = [1, 1, 2, 3, 3].toSet();
print(dedup.toList()); // [1, 2, 3]
print(dedup.contains(2)); // trueMap Operations
Maps expose keys and values as Iterables. update() modifies a value in place; map() transforms entries into a new Map. forEach iterates key-value pairs. Maps preserve insertion order. Use putIfAbsent for set-if-missing semantics.
var ages = {'Alice': 30, 'Bob': 25};
ages['Carol'] = 28;
print(ages.keys); // (Alice, Bob, Carol)
print(ages.values); // (30, 25, 28)
print(ages.length); // 3
ages.update('Bob', (v) => v + 1);
print(ages['Bob']); // 26
ages.remove('Alice');
ages.forEach((k, v) => print('$k=$v'));
var mapped = ages.map((k, v) => MapEntry(k, v + 100));Spread & Null-spread
The spread operator ... expands a collection's elements into another collection. ...? is the null-aware spread—it handles null gracefully (expands to nothing). Spread works on Lists, Sets, and Maps, making collection composition concise.
var a = [1, 2];
var b = [0, ...a, 3]; // [0, 1, 2, 3]
print(b);
List<int>? maybe;
var c = [0, ...?maybe, 4]; // [0, 4] (null-spread is safe)
print(c);
var m1 = {'a': 1};
var m2 = {'b': 2, ...m1}; // {b: 2, a: 1}
print(m2);Collection-if & Collection-for
Collection-if and collection-for are Dart-specific syntax that build collections conditionally or via iteration inline. They avoid separate add() calls and intermediate variables. They can be combined and nested, producing clean declarative collection construction.
var promo = true;
var menu = [
'home',
'products',
if (promo) 'sale',
'about',
];
print(menu); // [home, products, sale, about]
var nums = [1, 2, 3];
var doubled = [
for (var n in nums) n * 2,
];
print(doubled); // [2, 4, 6]
// combine: [for (var x in xs) if (x > 0) x]Higher-order Methods
Lists/Iterables support any, every, firstWhere, reduce, fold, skip, take, expand, and more. fold is powerful—it carries an accumulator of any type. These higher-order methods enable declarative, functional-style data processing without explicit loops.
var nums = [1, 2, 3, 4, 5];
print(nums.any((n) => n > 4)); // true
print(nums.every((n) => n > 0)); // true
print(nums.firstWhere((n) => n > 2)); // 3
print(nums.reduce((a, b) => a + b)); // 15
var byParity = nums.fold(<bool, List<int>>{}, (m, n) {
m[n.isOdd] = [...?m[n.isOdd], n]; return m;
});
print(byParity); // {true: [1,3,5], false: [2,4]}Async (Future / async / await)
Future Basics
A Future represents a value (or error) that will be available at some point in the future. Future.delayed completes after a duration. Use .then() to register a callback for when the Future completes. Futures are the foundation of Dart's async model.
Future<String> fetchUser() {
return Future.delayed(Duration(seconds: 1), () => 'Alice');
}
void main() {
fetchUser().then((name) {
print('Got: $name'); // Got: Alice (after 1s)
});
print('waiting...');
}async & await
Mark a function async to use await inside it. async functions always return a Future. await pauses execution until the awaited Future completes, then yields its value—without blocking the event loop. This makes asynchronous code read like synchronous code.
Future<String> fetchUser() async {
await Future.delayed(Duration(seconds: 1));
return 'Alice';
}
Future<void> main() async {
print('start');
String name = await fetchUser();
print('Got: $name');
print('done');
}Error Handling in Async
Errors thrown in async functions propagate as failed Futures. Use try/catch/finally around await to handle them—identical to synchronous error handling. catch catches the error object; use 'on ExceptionType' to catch specific types.
Future<int> divide(int a, int b) async {
if (b == 0) throw Exception('divide by zero');
return a ~/ b;
}
Future<void> main() async {
try {
var result = await divide(10, 0);
print(result);
} catch (e) {
print('Error: $e'); // Error: Exception: divide by zero
} finally {
print('done');
}
}Future.then, catchError
Instead of await, you can chain Futures with .then(), .catchError(), and .whenComplete(). Each .then() returns a new Future, enabling pipelines. catchError handles any failure in the chain. Prefer async/await for readability; use chaining for simple cases.
Future<int> compute() async => 42;
compute()
.then((v) => v * 2)
.then((v) => print(v)) // 84
.catchError((e) => print('err: $e'))
.whenComplete(() => print('cleanup'));
// chaining transforms the result type
Future<String> fetch() async => 'data';
fetch().then((s) => s.length).then(print); // 4Future.wait & Future.any
Future.wait runs multiple Futures in parallel and completes when all of them complete (returns a List of results). Future.any completes with the result of the first Future to finish. Use wait for parallelism and any for racing (e.g., request + timeout).
Future<int> task(int n) async {
await Future.delayed(Duration(milliseconds: n));
return n;
}
// run in parallel, wait for all
var all = await Future.wait([task(100), task(50), task(200)]);
print(all); // [100, 50, 200]
// resolve with the first to complete
var first = await Future.any([task(100), task(50), task(200)]);
print(first); // 50 (fastest)Completer
A Completer lets you manually create and complete a Future. Call complete(value) or completeError(error) to finish it. Completers are useful when wrapping callback-based APIs into Futures, or when a Future's completion is triggered by an external event you control.
import 'dart:async';
Completer<String> completer = Completer<String>();
// complete the future from elsewhere
Future<String> get value => completer.future;
completer.complete('resolved!');
void main() async {
print(await value); // resolved!
}
// useful when bridging callback-based APIs to FuturesStream
Create Stream (async*)
An async* generator function returns a Stream and yields values with yield. Each yield emits a value to the stream. The generator is lazy—values are produced as the stream is consumed. async* is the asynchronous counterpart to sync* (Iterable).
Stream<int> countDown(int from) async* {
while (from > 0) {
await Future.delayed(Duration(seconds: 1));
yield from;
from--;
}
}
void main() async {
await for (var n in countDown(3)) {
print(n); // 3, 2, 1 (one per second)
}
}Listen to a Stream
listen() subscribes to a Stream and registers callbacks for data, errors, and completion. It returns a StreamSubscription which you can pause, resume, or cancel. Single-subscription streams allow only one listener; broadcast streams allow many.
var sub = countDown(3).listen(
(n) => print('got $n'),
onDone: () => print('done'),
onError: (e) => print('err: $e'),
);
// pause/resume/cancel
sub.pause();
sub.resume();
// sub.cancel(); // stop listening
Stream<int> countDown(int from) async* {
while (from > 0) yield from--;
}Stream Methods
Streams support functional transformations like where, map, take, skip, and expand, returning new Streams. You can also use first, last, length, isEmpty as Futures. These methods make Stream processing declarative—similar to List but asynchronous.
var stream = Stream.fromIterable([1, 2, 3, 4]);
// transform like an Iterable
var evens = stream.where((n) => n.isEven);
var doubled = stream.map((n) => n * 2);
await for (var n in Stream.fromIterable([1,2,3]).map((n) => n * 10)) {
print(n); // 10, 20, 30
}
print(await stream.first); // 1
print(await stream.last); // 4
print(await stream.length); // 4StreamController
A StreamController gives you a sink to manually push data, errors, and completion events into a Stream. Use it to create custom streams from non-stream sources. The default is single-subscription; pass broadcast: true for a multi-listener broadcast stream.
import 'dart:async';
var controller = StreamController<int>();
// add events manually
controller.add(1);
controller.add(2);
controller.addError('oops');
controller.close();
controller.stream.listen(
(n) => print(n), // 1, 2
onError: (e) => print(e), // oops
onDone: () => print('done'),
);
// use controller.addError/sink.add for errorsawait for & Broadcast
await for iterates a Stream asynchronously, processing each event as it arrives until the stream closes. Broadcast streams (created with .broadcast() or StreamController.broadcast) allow multiple simultaneous listeners, while single-subscription streams allow only one.
// await for consumes a stream like a loop
Future<int> sumStream(Stream<int> s) async {
var total = 0;
await for (var n in s) {
total += n;
}
return total;
}
print(await sumStream(Stream.fromIterable([1, 2, 3]))); // 6
// broadcast stream: multiple listeners
var bc = StreamController<int>.broadcast();
bc.stream.listen(print);
bc.stream.listen((n) => print('got $n'));
bc.add(5); // both listeners receive 5Exception Handling
throw
throw raises an exception, interrupting normal execution. Dart can throw any non-null object, but it's idiomatic to throw Exception or Error subtypes. Common built-in types: ArgumentError, StateError, FormatException, RangeError. Throwing strings is discouraged.
void checkAge(int age) {
if (age < 0) {
throw ArgumentError('age cannot be negative');
}
if (age > 150) {
throw StateError('unrealistic age: $age');
}
}
// you can throw any non-null object
void fail() => throw 'something went wrong';try-catch-finally
Use on Type catch (e) to catch specific exception types, or catch (e, stackTrace) to catch anything and access the stack trace. finally always runs whether or not an exception occurred. Order matters: put specific on-clauses before the general catch.
try {
checkAge(-5);
} on ArgumentError catch (e) {
print('argument error: $e');
} on StateError catch (e) {
print('state error: $e');
} catch (e, stackTrace) {
print('unknown: $e');
print(stackTrace);
} finally {
print('always runs');
}Custom Exception
Create custom exceptions by implementing the Exception interface. Provide a descriptive message and override toString() for readable output. Custom exceptions make error handling more precise—callers can catch specific failure modes. Convention: name them *Exception.
class InvalidCredentialsException implements Exception {
final String message;
InvalidCredentialsException(this.message);
@override
String toString() => 'InvalidCredentialsException: $message';
}
void login(String user, String pass) {
if (user.isEmpty) {
throw InvalidCredentialsException('username required');
}
}
try {
login('', 'x');
} on InvalidCredentialsException catch (e) {
print(e); // InvalidCredentialsException: username required
}rethrow
rethrow re-raises the currently caught exception while preserving its original stack trace. Use it in middleware/wrapper functions that log or clean up but don't fully handle the error. Unlike throw e, rethrow keeps the original stack trace intact for debugging.
Future<void> logErrors(Future<void> Function() action) async {
try {
await action();
} catch (e) {
print('logging error: $e');
rethrow; // re-throw the caught exception
}
}
void main() async {
try {
await logErrors(() async => throw Exception('fail'));
} catch (e) {
print('handled upstream: $e');
}
}Error vs Exception
Exception is for runtime conditions a program can reasonably catch and recover from (e.g., network failure, bad input). Error represents programming bugs (type errors, assertion failures, index out of range) that should be fixed in code, not caught at runtime. Catching Errors is discouraged.
// Exception: recoverable, expected to be caught
class MyException implements Exception {}
// Error: programming bugs, not meant to be caught
class MyError extends Error {
@override
String toString() => 'MyError: invalid state';
}
void risky() {
throw MyError(); // bug: should fix the code
throw MyException(); // runtime condition: catch it
}
// assert failures, type errors, range errors are ErrorsTypedef (Type Aliases)
Function typedef
typedef creates an alias for a function type, making signatures readable and reusable. IntOperator now names the (int,int)->int function type. Without typedef, you'd repeat 'int Function(int, int)' everywhere. Assign compatible functions to variables of the typedef type.
// alias for a function type
typedef IntOperator = int Function(int, int);
int add(int a, int b) => a + b;
int mul(int a, int b) => a * b;
IntOperator op = add;
print(op(2, 3)); // 5
op = mul;
print(op(2, 3)); // 6
// pass as a parameter
void apply(IntOperator f, int a, int b) => print(f(a, b));Generic typedef
typedefs can be generic, taking their own type parameters. Mapper<T, R> aliases a function T->R. This lets you express rich function contracts once and reuse them. Generic typedefs are essential for typed functional programming patterns in Dart.
// generic function type alias
typedef Mapper<T, R> = R Function(T input);
String stringify(int n) => n.toString();
int lenOf(String s) => s.length;
Mapper<int, String> intToStr = stringify;
Mapper<String, int> strToLen = lenOf;
print(intToStr(42)); // '42'
print(strToLen('hi')); // 2New-style typedef (non-function)
Since Dart 2.13, typedef can alias any type—including classes, not just functions. This improves readability for complex generic types. Dart 3 records can also be aliased. The alias is fully interchangeable with the original type—no runtime difference.
// Dart 2.13+: alias for any type, not just functions
typedef IntList = List<int>;
typedef StringMap<V> = Map<String, V>;
IntList nums = [1, 2, 3];
StringMap<int> scores = {'a': 1};
// alias for a record type (Dart 3)
typedef Point = ({double x, double y});
Point p = (x: 1.0, y: 2.0);
print(p.x); // 1.0Using typedef in APIs
typedefs shine in API signatures: they name callback contracts clearly. Predicate<T> reads better than 'bool Function(T)'. Library authors use them to expose clean, documented function-type contracts. Callers can pass any compatible function or lambda.
typedef Predicate<T> = bool Function(T);
bool isEven(int n) => n.isEven;
List<T> filter<T>(List<T> list, Predicate<T> test) {
return list.where(test).toList();
}
print(filter([1, 2, 3, 4], isEven)); // [2, 4]
print(filter(['', 'a', ''], (s) => s.isNotEmpty)); // [a]
// typedef makes callback contracts explicittypedef vs inline Function types
A typedef is just an alias—it's identical to the inline function type at runtime and for type checking. typedef improves readability and centralizes the contract so changes happen in one place. Prefer typedef for any function type used in more than one location.
// these two are equivalent
typedef Handler = void Function(String event);
class EventBus {
// using typedef
void on(Handler handler) {}
// using inline type
void on2(void Function(String) handler) {}
}
// both accept the same functions
void myHandler(String e) => print(e);
EventBus().on(myHandler);
EventBus().on2(myHandler);Enums
Basic Enum
Enums declare a fixed set of named constants. Each value has a name (String) and index (int). values returns a list of all enum values in declaration order. Enums are implicitly static and const. Like switch on enums benefits from exhaustiveness checks.
enum Color { red, green, blue }
var c = Color.red;
print(c); // Color.red
print(c.name); // 'red'
print(c.index); // 0
print(Color.values); // [Color.red, Color.green, Color.blue]
print(Color.green.index); // 1Enhanced Enum (Dart 3)
Dart 3 enhanced enums can have fields, constructors, methods, and getters—like classes. Constructors must be const. This lets enums carry data and behavior, replacing many uses of class hierarchies. The first values still get automatic name and index properties.
enum Vehicle {
car('Car', 4),
bike('Bike', 2),
truck('Truck', 6);
final String label;
final int wheels;
const Vehicle(this.label, this.wheels);
int get axles => wheels ~/ 2;
}
print(Vehicle.car.label); // Car
print(Vehicle.bike.wheels); // 2
print(Vehicle.truck.axles); // 3Iterate & Switch
Enum values are iterable via .values. Switching over an enum is exhaustive—if you cover all cases, no default is needed (Dart 3 enforces this for sealed types and enums). This makes adding a new enum value a compile-time signal to update every switch.
enum Status { pending, active, done }
// iterate all values
for (var s in Status.values) {
print(s.name);
}
// exhaustive switch (no default needed)
String label(Status s) => switch (s) {
Status.pending => 'Waiting',
Status.active => 'Running',
Status.done => 'Finished',
};
print(label(Status.active)); // RunningEnum with Methods
Enhanced enums can define methods and getters that use 'this' to refer to the current value. A switch on 'this' inside an enum method is exhaustive without a default. This pattern encapsulates behavior per enum value cleanly, often replacing utility classes.
enum HttpStatus {
ok(200),
notFound(404),
serverError(500);
final int code;
const HttpStatus(this.code);
bool get isSuccess => code >= 200 && code < 300;
String get reason => switch (this) {
ok => 'OK',
notFound => 'Not Found',
serverError => 'Internal Server Error',
};
}
print(HttpStatus.ok.isSuccess); // true
print(HttpStatus.notFound.reason); // Not FoundEnum Comparison
Each enum value is a singleton—there's exactly one instance per value per program. == compares by identity effectively. index allows ordering by declaration position. Use == for equality; identical() also works since values are canonicalized. Enums make great Map keys and Set elements.
enum Priority { low, medium, high }
var a = Priority.low;
var b = Priority.high;
print(a == b); // false
print(a == Priority.low);// true
print(a.index < b.index);// true
print(identical(a, Priority.low)); // true
// enums are singletons: only one instance per value
// use == for equality, not identical (though both work)Annotations (Metadata)
Built-in Annotations
Annotations start with @ and attach metadata to libraries, classes, fields, parameters, and methods. @override marks method overrides (compile-time check). @deprecated/@Deprecated signal obsolete APIs. @protected, @visibleForTesting, @visibleForOverriding control visibility hints.
class Animal {
@override
String toString() => 'Animal';
@Deprecated('use newName instead')
String oldName = 'x';
String newName = 'x';
@protected
void internalMethod() {}
@visibleForTesting
String testHook() => 'test';
}@override & @Deprecated
@override tells the compiler to verify a method actually overrides a superclass member—catching typos at compile time. @Deprecated('message') marks an API as obsolete; the analyzer warns at every call site with the provided message, guiding migration to the replacement.
class Base {
void greet() {}
String name = 'base';
}
class Derived extends Base {
@override
void greet() => print('hi');
// @override verifies the parent method exists
// typo here would be a compile error:
// @override void greeet() {}
}
// @Deprecated emits a warning at the call site
@Deprecated('use bar()')
void foo() {}
void bar() {}Custom Annotation
Any class with a const constructor can be used as an annotation. Define custom annotations to mark code for tooling, documentation, or code generation. Reading annotations requires dart:mirrors (VM-only, disabled in Flutter) or build_runner code generation. Common in JSON serialization packages.
// a custom annotation is just a const constructor class
class Todo {
final String msg;
const Todo(this.msg);
}
class Service {
@Todo('refactor to use cache')
void fetchData() {}
@Todo('add tests')
void process() {}
}
// annotations are accessed via dart:mirrors (VM) or
// code generation (build_runner) in practice@immutable & @JsonSerializable
@immutable (from package:meta) marks a class whose instances should not change after construction; subclasses and fields should be final. @JsonSerializable (from json_serializable) triggers code generation for JSON conversion via build_runner. Annotations drive many Dart/Flutter ecosystems.
import 'package:meta/meta.dart';
// requires the 'meta' package
@immutable
class User {
final String name;
final int age;
const User(this.name, this.age);
}
// with package:json_annotation / json_serializable
// @JsonSerializable()
// class Product {
// final String id;
// Product(this.id);
// factory Product.fromJson(Map<String, dynamic> j) => ...;
// }Annotation on Parameters
Annotations can be placed on parameters, library declarations, and typedefs too. Before null safety, @required marked mandatory named params; the modern equivalent is the 'required' keyword. Annotations on parameters are widely used by serialization and DI frameworks.
class Required {
const Required([this.reason]);
final String? reason;
}
class Service {
// annotation on a parameter
void create({
@Required('name is mandatory') String? name,
@protected int? internal,
}) {
print(name);
}
}
// @required is built into Dart (the 'required' keyword
// is preferred in Dart 2.12+ for null safety)Library & Visibility
import & show/hide
import brings a library's public API into scope. Use show to import only specific names, hide to exclude names, and as to give a prefix (avoiding collisions).dart: is for SDK libraries; package: is for pub packages; relative paths work for local files.
// import an entire library
import 'dart:io';
import 'package:http/http.dart';
// import only specific names
import 'dart:math' show Random, pi;
// hide specific names
import 'dart:async' hide Timer;
// prefix to avoid name clashes
import 'package:http/http.dart' as http;
http.get(Uri.parse('https://example.com'));export & part
part splits a single library across multiple files (they share the library's private namespace). export re-exports another library's symbols so importers of your library also get them—useful for creating a single public entry point. Prefer export over part for most modular designs.
// library.dart
library my_lib;
// split implementation across files
part 'src/widget_a.dart';
part 'src/widget_b.dart';
// re-export another library's API
export 'src/utils.dart' show formatDate, parseDate;
// users import library.dart and get everythingVisibility (_underscore)
Dart's privacy is library-based, not class-based: identifiers starting with _ are private to the library (the file, or the part-of group) that declares them. The same library can access private members of classes defined in it. There are no protected or package-private keywords.
// names starting with _ are library-private
class _Internal {
void _helper() {}
}
class Public {
String _secret = 'hidden'; // private field
String name = 'visible'; // public field
String _process() => 'internal';
String reveal() => _process();
}
// _secret is accessible anywhere in the SAME library/file
// but not from other libraries that import this filedeferred (lazy) loading
Deferred loading (lazy import) downloads a library only when loadLibrary() is first called—useful on the web to reduce initial bundle size. The deferred library's symbols are accessed via the prefix. Supported on web; native platforms load eagerly. Ideal for rarely-used features.
// load a library on demand (web only)
import 'package:heavy_lib/heavy.dart' deferred as heavy;
Future<void> main() async {
// library is NOT loaded until this call
await heavy.loadLibrary();
heavy.SomeClass().doWork();
}
// useful for splitting large web bundles and
// loading rarely-used features only when neededLibrary Directive
The library directive names a library and is optional in modern Dart. It's mainly relevant when using part/part-of to split a library across files. Most Dart files omit the directive and are treated as anonymous libraries. The name helps tooling and documentation.
// explicit library declaration
library my_package.utils;
import 'dart:math';
part 'src/helper.dart';
const double version = 1.0;
// a 'library' name is optional in modern Dart;
// it's mainly used with part/part-of and tooling
// most files omit it and are treated as anonymous librariespart & part of
part/part-of splits one library across files: the main file declares part 'file.dart'; the part file declares 'part of library;'. Parts share the library's scope including private (_name) members. Prefer separate libraries with export for new code—parts are for tightly-coupled implementations.
// shapes.dart
library shapes;
part 'circle.dart';
part 'square.dart';
class Shape {}
// circle.dart
part of shapes;
class Circle extends Shape {}
// files in the same library share private members
// (_name visible across all parts)Null Safety
Nullable & Non-nullable Types
Sound null safety: int can never be null; int? can. The compiler enforces this so NullPointerExceptions are caught at compile time. You must initialize non-nullable variables before use. Append ? to any type to make it nullable. This is Dart's core safety feature since 2.12.
int a = 42; // non-nullable: cannot be null
int? b; // nullable: can be null
print(b); // null
b = 10;
print(b); // 10
String name = 'Al'; // non-nullable
String? middle; // nullable
// non-nullable types are guaranteed non-null
// int x = null; // ERROR
// print(a.length); // safe: a is non-nullNull Assertion (!)
The ! (null assertion) operator tells the compiler 'I know this is non-null; trust me.' It throws at runtime if the value is actually null. Use it sparingly—prefer ?. or null checks. Overusing ! defeats null safety. It's reasonable when a framework guarantees a value is set.
String? maybeName;
// int len = maybeName.length; // ERROR: maybeName is nullable
maybeName = 'Alice';
int len = maybeName!.length; // ! asserts non-null
print(len); // 5
// throws if null at runtime:
// String? n; print(n!.length); // NoSuchMethodError/nullNull-aware Operators
?. is null-aware access (returns null if the target is null). ?? is the null-coalescing operator (provides a default). ??= assigns only if the current value is null. Together these let you handle nullable values gracefully without explicit if-null checks.
String? name;
print(name?.length); // null (safe access)
print(name?.length ?? 0);// 0 (default if null)
name = 'Alice';
print(name?.length); // 5
name ??= 'Bob'; // assign only if null
print(name); // Alice (already set)
List<int>? list;
print(list?.first); // null
print(list?.first ?? -1);// -1Type Promotion
Type promotion: after a null check (x != null) or type check (x is String), the compiler narrows the type within that branch—no explicit cast needed. Promotion also happens after a non-null assignment. Local variables promote well; fields may need explicit local copies.
String? name;
if (name != null) {
// name is promoted to non-nullable String here
print(name.length); // safe, no ! needed
}
// promoted via is check
Object obj = 'hello';
if (obj is String) {
print(obj.length); // smart-cast to String
}
// promoted via assignment
int? x;
x = 5;
print(x.abs()); // x is non-null after assignmentlate & required
late marks a non-nullable variable that will be initialized after declaration but before first use—deferring initialization. late final initializes once (lazily if given an initializer). required marks a named parameter as mandatory. Together they integrate cleanly with null safety.
class Config {
// late: non-nullable, initialized later
late final String value = _load();
// late without initializer: assign before first use
late final int computed;
Config() {
computed = expensive();
}
String _load() => 'loaded';
int expensive() => 42;
}
// required: mandatory named parameter
void build({required String name}) {}
build(name: 'Al'); // OK
// build(); // ERROR: missing requiredlate Lazy Initialization
A late field with an initializer is lazy—the initializer runs on first access, not at construction. The result is cached for subsequent accesses. This is great for expensive initialization, circular references, and fields that depend on 'this' being fully constructed. late final makes it a one-time computation.
class Service {
// lazy: _expensive runs only on first access
late final int cache = _expensive();
int _expensive() {
print('computing...');
return 42;
}
}
var s = Service();
print('created');
print(s.cache); // computing... 42
print(s.cache); // 42 (cached, no recompute)
// late fields with initializers are evaluated lazilyRelated Dart snippets
Copy-paste ready code for common tasks.
Classes and Constructors
Define classes with named and factory constructors.
Async/Await and Futures
Asynchronous programming with Future and async/await.
Collections (List, Map, Set)
Work with collections and functional operations.
Null Safety
Sound null safety with ? and ! operators.
Generics
Type-safe reusable classes and methods.
Mixins and Extensions
Compose behaviors without inheritance.
Futures and Streams
Work with single and multiple async values.
Isolates (True Parallelism)
Run code in separate isolates for CPU-bound work.
Was this helpful?