March 10, 2025 · 3 min read
Modern Dart in Practice: The Features That Matter
Modern Dart — records, patterns, sealed classes, sound null safety — is far more than 'the language you learn for Flutter.' The features I use every day, with examples.
Dart is often described as "the language you learn on the way to Flutter." That undersells it. Modern Dart — especially since Dart 3 — is an expressive, sound, null-safe language with pattern matching and records that make everyday code shorter and safer. Here are the features I lean on daily.
Sound null safety
Every type is non-nullable unless you opt in with ?. The compiler forces you to handle absence, which kills a whole class of runtime crashes.
String greet(String? name) {
// name could be null, so we must handle it before use.
return 'Hello, ${name ?? 'stranger'}';
}
// The `!` operator asserts non-null — use it only when you're certain.
final user = findUser(id)!; // throws if null, by design
Prefer ??, ?., and pattern matching over !. A bang operator is a promise to the compiler; if you're wrong, it crashes.
Records: lightweight, structural tuples
Records let you return multiple values without inventing a class.
(int, int) minMax(List<int> xs) {
xs.sort();
return (xs.first, xs.last);
}
final (lo, hi) = minMax([3, 1, 9, 4]); // destructured
print('$lo..$hi'); // 1..9
// Named fields read even better:
({double lat, double lng}) location() => (lat: 36.19, lng: 44.01);
Pattern matching and switch expressions
Dart 3's patterns turn branching logic into something declarative.
sealed class Shape {}
class Circle extends Shape { Circle(this.r); final double r; }
class Square extends Shape { Square(this.side); final double side; }
double area(Shape shape) => switch (shape) {
Circle(:final r) => 3.14159 * r * r,
Square(:final side) => side * side,
};
Because Shape is sealed, the compiler knows every subtype and will error if you forget a case — exhaustiveness checking you get for free.
Async: Futures and Streams
Future is a single value that arrives later; Stream is many values over time.
Future<User> fetchUser(String id) async {
final res = await api.get('/users/$id');
return User.fromJson(res.data);
}
Stream<int> countdown(int from) async* {
for (var i = from; i >= 0; i--) {
await Future.delayed(const Duration(seconds: 1));
yield i;
}
}
Rules of thumb: don't await inside a hot loop when the calls are independent — use Future.wait. And always handle errors, either with try/catch around await or .catchError.
final results = await Future.wait([
fetchUser('1'),
fetchUser('2'),
]);
Extension methods
Add behavior to types you don't own without subclassing.
extension StringCasing on String {
String get capitalized =>
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}
'flutter'.capitalized; // "Flutter"
Where to go next
Learn const constructors (they enable compile-time canonicalization and Flutter rebuild skipping), the collection-if/for syntax, and Iterable lazy evaluation. Dart rewards you for leaning into its type system — the more precise your types, the more the compiler works on your behalf.