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, safeMap just 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.