Chapter 01
Dart essentials
The parts of Dart 3 that Flutter code leans on every day: null safety, immutable classes, pattern matching, collections, Futures, Streams and isolates.
Sound null safety
Also called null safety
- What
- A type like
Stringcan never hold null; onlyString?can. The compiler forces you to handle the null case with?.,??, a null check orlatebefore you use the value. - Why
- Null crashes are the most common runtime error in apps. Sound null safety moves them to compile time, so a missing value is handled on purpose instead of crashing a screen.
String? nickname;
final shown = nickname ?? 'Guest';
final length = nickname?.length;
if (nickname != null) {
print(nickname.toUpperCase());
}
late final String token;Watch out
Sprinkling ! to silence the compiler just moves the crash to runtime; handle the null case instead.
var, final and const
- What
varcan be reassigned,finalis set once at runtime, andconstis a compile-time constant that is created once and shared.- Why
- Prefer
finalfor most variables.constwidgets are built once and Flutter can skip rebuilding them, which is a cheap performance win.
var count = 0;
count++;
final startedAt = DateTime.now();
const padding = EdgeInsets.all(16);
const title = Text('Settings');Watch out
A final list can still be mutated with add; only const or an unmodifiable list is truly frozen.
Classes and immutable models
Also called immutable model, mixin
- What
- Dart classes have fields, constructors with named and
requiredparameters, and factory constructors. App models are usually immutable, with acopyWithmethod to create changed copies. - Why
- Immutable models make state changes explicit, which is what Flutter state management libraries expect and what makes bugs easy to trace.
class User {
const User({required this.id, required this.name});
final String id;
final String name;
User copyWith({String? name}) =>
User(id: id, name: name ?? this.name);
factory User.fromJson(Map<String, dynamic> j) =>
User(id: j['id'] as String, name: j['name'] as String);
}Watch out
Two instances with the same fields are not == unless you override equality or use a package such as freezed or equatable.
Sealed classes, records and patterns
Also called switch expression, pattern matching, sealed class
- What
- Dart 3 adds
sealedclass families, records such as(int, String)and patterns that destructure values. Aswitchover a sealed type must cover every subtype. - Why
- Modelling states like loading, success and error as a sealed family means the compiler tells you when a screen forgets to handle one.
sealed class Shape {}
class Circle extends Shape { Circle(this.r); final double r; }
class Square extends Shape { Square(this.s); final double s; }
double area(Shape shape) => switch (shape) {
Circle(:final r) => pi * r * r,
Square(:final s) => s * s,
};
final (name, age) = ('Ana', 21);List, Map and Set
- What
- The core collections: ordered
List, key-valueMapand uniqueSet. Collectionif, collectionforand the spread operator...build them inline. - Why
- Widget trees are built from lists, so collection
ifandforlet you show or repeat children without temporary variables.
final scores = <String, int>{'ana': 90, 'li': 72};
final passed = [
for (final e in scores.entries)
if (e.value >= 75) e.key,
];
final everyone = {...passed, 'guest'};
final top = scores.values.reduce(max);Futures, async and await
Also called event loop
- What
- A
Futureis a value that arrives later, such as an HTTP response.asyncmarks a function that returns a Future, andawaitpauses it until the value is ready without blocking the UI. - Why
- Every network call, file read and database query in Flutter is a Future, so you need to await them and handle their errors with
tryandcatch.
Future<String> fetchName() async {
await Future.delayed(const Duration(seconds: 1));
return 'Ana';
}
Future<void> main() async {
try {
final (a, b) = await (fetchName(), fetchName()).wait;
print('$a and $b');
} catch (e) { print('Failed: $e'); }
}Watch out
Calling an async function without await silently drops its errors; the unawaited_futures lint catches this.
Streams
- What
- A
Streamdelivers many values over time, like location updates or database changes. You read it withlisten,await foror aStreamBuilderwidget, and create one with anasync*function. - Why
- Anything that changes repeatedly, from a chat feed to a Firestore query, is modelled as a Stream so the UI can update as values arrive.
Stream<int> countdown(int from) async* {
for (var i = from; i >= 0; i--) {
await Future.delayed(const Duration(seconds: 1));
yield i;
}
}
final sub = countdown(3).listen(print);
await sub.cancel();Watch out
Forgetting to cancel a subscription in dispose keeps the listener alive and calls setState on a widget that is gone.
Isolates
- What
- Dart runs your code on one thread per isolate, and isolates share no memory.
Isolate.runexecutes a function on a separate isolate and returns its result. - Why
- Heavy work on the main isolate, like parsing a large JSON file or resizing images, blocks frames and makes the app stutter. Moving it to another isolate keeps scrolling smooth.
final items = await Isolate.run(() {
final data = jsonDecode(hugeJson) as List<dynamic>;
return data.map((e) => Item.fromJson(e)).toList();
});Watch out
Async code is not parallel: await frees the isolate while waiting for I/O, but CPU-heavy work still blocks it.
Packages and pubspec.yaml
Also called pub.dev, pubspec
- What
pubspec.yamllists your app's dependencies, assets and SDK constraints. Packages come from pub.dev and are added withflutter pub add.- Why
- Most features, such as HTTP, storage and notifications, come from packages, so you need to pick well-maintained ones and keep them up to date.
flutter pub add http
flutter pub add dev:build_runner
flutter pub get
flutter pub outdatedWatch out
Choosing a package by popularity alone; check its pub points, platform support and last release before depending on it.