Dart essentials for Flutter
Sound null safety, futures and streams, classes with mixins, collections, extension methods and async/await — the Dart you actually write inside a widget tree.
Null safety and simple models
class Task {
final String id;
final String title;
final DateTime? dueDate;
const Task({required this.id, required this.title, this.dueDate});
bool get isOverdue {
final due = dueDate;
if (due == null) return false; // promotion works inside a local
return due.isBefore(DateTime.now());
}
Task copyWith({String? title, DateTime? dueDate}) =>
Task(id: id, title: title ?? this.title, dueDate: dueDate ?? this.dueDate);
}
void main() {
const task = Task(id: 'a1', title: 'Write release notes');
final label = task.dueDate?.toIso8601String() ?? 'no date';
print('$label ${task.isOverdue}');
}finalmeans assign once,constmeans known at compile time. Aconstwidget subtree is never rebuilt.?makes a type nullable;!asserts it is not null and throws at runtime if you are wrong.- Null promotion only works for local variables, not for fields — assign the field to a local before checking.
- Use
requirednamed parameters instead of optional positionals so call sites read clearly.
Collections and extensions
final scores = <String, int>{'ada': 91, 'grace': 87, 'linus': 78};
final names = scores.keys.toList()..sort();
final top = scores.entries.reduce((a, b) => a.value >= b.value ? a : b);
final passed = scores.entries.where((e) => e.value >= 80).map((e) => e.key).toList();
extension StringX on String {
String get titleCase => isEmpty
? this
: '${this[0].toUpperCase()}${substring(1).toLowerCase()}';
}
final spaced = names.map((n) => n.titleCase).join(', ');
print('${top.key} leads with ${top.value}');
print('passed: ${passed.length}, all: $spaced');| Operation | Result type | Rebuilds lazily? |
|---|---|---|
map | Iterable | Yes |
where | Iterable | Yes |
toList | List | No, materialises |
reduce | Element | No, throws on empty |
fold | Accumulator | No, safe on empty |
Futures, streams and async/await
Future<List<String>> loadTags() async {
await Future<void>.delayed(const Duration(milliseconds: 200));
return ['flutter', 'dart', 'mobile'];
}
Stream<int> countdown(int from) async* {
for (var i = from; i > 0; i--) {
await Future<void>.delayed(const Duration(seconds: 1));
yield i;
}
}
Future<void> main() async {
final tags = await loadTags();
await for (final value in countdown(3)) {
print('tick $value (${tags.length} tags loaded)');
}
// run several futures together and fail fast on the first error
final results = await Future.wait([
loadTags(),
Future.value(['a']),
]);
print(results.expand((e) => e).toSet());
}⚠️
Use
BuildContext across an await only after checking if (!mounted) return;. The widget may have been disposed while the future was pending, and using a stale context throws at runtime.FAQ
When should I use a Stream instead of a Future?
A Future resolves once, so use it for a single request or a one-off read. Use a Stream when values arrive over time — sensor data, websocket messages, or database queries that should update the UI on every change.
Why does my <code>setState</code> Say the widget is unmounted?
The async callback completed after the State was disposed. Guard every post-await
setState with a mounted check, or cancel the subscription in dispose.Related
State management: setState, Provider, Riverpod and BLoC Networking, JSON and serialization
Last refreshed 2026-09-18.