Dart Extension Methods for Null-Safe Collection Handling
The Problem
Every Dart codebase eventually runs into a nullable List<T>? or Iterable<T>?. The standard library forces you to write list?.where(...).toList() ?? [] every time you want to filter safely. That boilerplate clutters business logic and makes code harder to read.
The Extension Solution
Dart’s extension methods let you add new members to existing types without subclassing. By extending Iterable<T>? you can provide whereNotNull, safeMap, and orEmpty helpers that behave like the non‑nullable equivalents but never throw.
extension NullableIterableX<T> on Iterable<T>? {
/// Returns an empty iterable when the receiver is null.
Iterable<T> get orEmpty => this ?? const <T>[];
/// Filters out null elements from a nullable iterable of nullable items.
Iterable<T> whereNotNull() => orEmpty.where((e) => e != null).cast<T>();
/// Maps each element using [transform] and drops null results.
Iterable<R> safeMap<R>(R? Function(T) transform) =>
orEmpty.map(transform).whereNotNull();
}
Real‑World Example
Imagine a Flutter screen that receives a List<User>? from a repository. You need to display only active users, sorted by name, and you also want to extract their email addresses for an analytics call.
class User {
final String name;
final String email;
final bool isActive;
User(this.name, this.email, this.isActive);
}
List<User>? fetchUsers() => [
User('Alice', 'alice@example.com', true),
User('Bob', 'bob@example.com', false),
User('Charlie', 'charlie@example.com', true),
];
void buildActiveUserEmails() {
final users = fetchUsers(); // List?
final activeEmails = users
.orEmpty // safe iterable
.where((u) => u.isActive) // filter
.map((u) => u.email) // transform
.toList(); // List
print('Active emails: $activeEmails');
}
Without the extension you would write:
final activeEmails = (users ?? [])
.where((u) => u.isActive)
.map((u) => u.email)
.toList();
The extension version reads like a sentence and eliminates the ?? [] noise.
Why This Works
- Zero runtime overhead – the extension methods are compiled to static calls; the compiler inlines them.
- Composable – you can chain
orEmpty,whereNotNull,safeMapjust like the built‑in APIs. - Discoverable – IDE auto‑complete shows the new members on any nullable iterable, so teammates adopt them instantly.
Tip: Place the extension in a shared package:core/extensions file so every feature module can import it without pulling in heavy dependencies.
Caveats and Tips
Extensions only apply when the static type is exactly Iterable<T>?. If you have a List<T>? you still get the helpers because List implements Iterable. However, a variable typed as dynamic or Object? won’t expose them.
Prefer orEmpty over ?? [] when you intend to chain further iterable operations; it preserves laziness until you call toList() or forEach.
If you need a nullable element type (e.g., Iterable<String?>?), whereNotNull returns Iterable<String> thanks to the cast call, giving you non‑nullable downstream code.