Dart syntax and null safety
var, final and const, sound null safety with ? and !, and the collection syntax you use in every file.
Declarations and inference
Dart is statically typed with inference. var infers the type once and then holds it, dynamic opts out of static checking, and final versus const is the difference between set-once at run time and known at compile time.
void main() {
final name = 'Dart'; // inferred String, assigned once
const pi = 3.14159; // compile-time constant, usable in const contexts
var count = 0; // mutable, type fixed as int
int? maybe; // nullable: starts as null
count += 1;
print('$name $count'); // interpolation, no concatenation needed
print(count.isEven); // numbers are objects, never primitives
print(pi.toStringAsFixed(2));
}
// A const list is deeply immutable; a final list can still be mutated
const frozen = [1, 2, 3];
final flexible = [1, 2, 3];
// frozen[0] = 9; // compile-time error
flexible[0] = 9;
// var infers and locks; dynamic defers every check to run time
var typed = <String, int>{'a': 1};
dynamic loose = 42;
loose = 'now a string'; // accepted, because dynamic is dynamic| Declaration | Meaning | Use for |
|---|---|---|
final x = 1 | One assignment, value may be computed at run time | Objects created in a constructor body |
const x = 1 | Compile-time constant, canonicalised and deeply immutable | Defaults, keys, fixed data |
var x = 1 | Inferred static type, reassignable | Locals that change |
Object? x | Any value or null | Heterogeneous collections |
dynamic x | No static checking at all | Interop and JSON boundaries, sparingly |
late final x | Assigned once, but not at declaration | Expensive fields and lazy initialisation |
Dart uses two-space indentation by convention, semicolons are required, and the official formatter (dart format) is not optional in practice: every tool in the ecosystem assumes it.
Null safety
Sound null safety makes nullability part of the type. A String can never be null, a String? might be, and the compiler forces you to handle the difference before the program runs.
int? parseIntSafe(String raw) => int.tryParse(raw); // null on failure
class Server {
final String host;
int port = 8080; // non-nullable needs an initialiser
Server(this.host);
}
class Cache {
late final List<String> entries = _load(); // computed on first read
static List<String> _load() => <String>[];
}
void main() {
// ?? supplies a fallback, ??= assigns only when null
final parsed = parseIntSafe('nope') ?? 0;
print(parsed + 1); // 1
Server? server;
print(server?.port); // null, no crash
server ??= Server('example.com'); // promoted to non-null after this
print(server.port); // 8080, no ! needed
final cache = Cache();
print(cache.entries.isEmpty); // _load runs on this line
}?.short-circuits the whole chain:a?.b?.cis null if any link is null.!asserts non-null and throws at run time if you are wrong.- Flow analysis promotes locals after a null check, so
if (x != null) x.lengthcompiles without!. - Promotion does not apply to non-final fields or to getters, whose value could change between the check and the use.
latemoves the null check to first use: reading before assignment throwsLateInitializationError.
! you write throws away the guarantee null safety gave you and turns a compile error into a runtime Null check operator used on a null value. Reach for ??, a local variable, or a nullable return type first; keep ! for cases the analyser genuinely cannot see.Collections and control flow
void main() {
final numbers = <int>[1, 2, 3];
final unique = <String>{'a', 'b', 'a'}; // Set: {'a', 'b'}
final ages = <String, int>{'ada': 36};
// Spread and collection-if/for build collections in one expression
final merged = <int>[...numbers, 4, 5];
final evens = <int>[for (final n in numbers) if (n.isEven) n];
final copy = <String, int>{...ages, 'alan': 41};
numbers.add(4);
merged.removeWhere((n) => n > 4);
final doubled = numbers.map((n) => n * 2).toList();
final sum = numbers.fold<int>(0, (acc, n) => acc + n);
print(merged.length);
print(evens); // [2]
print(copy.keys.join(','));
print(doubled.where((n) => n > 4).toList());
print(sum);
// switch is an expression, and it must be exhaustive
final label = switch (numbers.length) {
0 => 'empty',
1 => 'single',
_ => 'many',
};
print(label);
for (var i = 0; i < 3; i++) {
if (i == 1) continue;
print('i=$i');
}
}Listis growable by default;List.filled(n, 0)gives a fixed-length list.Mappreserves insertion order, so iteration order is predictable.- The cascade operator
..calls several methods on one object:list..add(1)..add(2). - Conditions must be
bool: there is no truthiness, soif (list)will not compile.
FAQ
When should I use dynamic?
Why does my field not get promoted?
final port = server.port;) or make the field final.Related
Classes, futures and streams Basic types and inference
Last refreshed 2026-09-18.