What are extension methods and when would you use them?

Medium PriorityAsked in ~50% of Flutter interviews

3 min read

Dart Fundamentals

AspectExtension methodSubclass / wrapper
Modifies the original type?❌ No (purely additive at call site)❌ No
Polymorphic / dynamic dispatch?❌ Static — resolved by declared type✅ Yes
Works on types you don't own?✅ Yes (String, int, BuildContext)✅ Usually
Cost at runtimeZero — just a function callAllocation + indirection

The key gotcha: extensions are statically dispatched. They use the declared type of the variable, not the runtime type.


Code in action

extension StringX on String {
  String get capitalized =>
      isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';

  String truncate(int max) =>
      length <= max ? this : '${substring(0, max)}…';

  bool get isEmail =>
      RegExp(r'^[\w.+-]+@\w+(\.\w+)+$').hasMatch(this);
}

'alice'.capitalized;          // 'Alice'
'Hello world'.truncate(5);    // 'Hello…'
'nik@x.com'.isEmail;          // true
// Generic extension — works on any List<T>
extension ListX<T> on List<T> {
  T? get firstOrNull => isEmpty ? null : first;
  List<T> get unique => {...this}.toList();
}

[1, 2, 2, 3].unique;          // [1, 2, 3]
<int>[].firstOrNull;          // null — no crash

The killer use case in Flutter: BuildContext shortcuts

extension BuildContextX on BuildContext {
  ThemeData    get theme       => Theme.of(this);
  TextTheme    get textTheme   => theme.textTheme;
  ColorScheme  get colors      => theme.colorScheme;
  Size         get screenSize  => MediaQuery.sizeOf(this);

  void snack(String msg) =>
      ScaffoldMessenger.of(this).showSnackBar(SnackBar(content: Text(msg)));

  Future<T?> push<T>(Widget page) => Navigator.of(this)
      .push<T>(MaterialPageRoute(builder: (_) => page));
}

// In a widget:
Container(
  color: context.colors.primary,
  width: context.screenSize.width / 2,
  child: Text('Hi', style: context.textTheme.headlineSmall),
);

The boilerplate of Theme.of(context) / Navigator.of(context) disappears.


When to reach for an extension

SituationUse extension?
Add a helper to a type you don't own (String, DateTime, BuildContext)✅ Yes
Group related sugar around a domain type (formatters, validators)✅ Yes
Behaviour that depends on runtime type / subtype❌ Use methods or visitor — extensions dispatch statically
Replace a real class (DTO, value object)❌ Make a proper class
State or fields❌ Extensions can't add fields — only methods, getters, operators

Common mistakes to avoid

// ❌ Forgetting extensions are statically dispatched
extension on Animal { String label() => 'animal'; }
extension on Dog    { String label() => 'dog'; }

Animal a = Dog();
a.label();                    // 'animal'  ← uses DECLARED type Animal, not Dog!
// ✅ For polymorphic behaviour, use real methods, not extensions.

// ❌ Dumping unrelated helpers in one extension
extension DateTimeStuff on DateTime {
  String get formatted => ...;
  double calculateMortgage() => ...;     // 🤔 belongs nowhere near DateTime
}

// ❌ Forgetting to import the extension
// Extensions only work when their library is imported — silent failure
// where the method "doesn't exist" until you add the import.

// ❌ Trying to add a field
extension UserX on User {
  late String _cache;          // ❌ extensions can't hold state
}

// ❌ Two extensions defining the same method on the same type
// Resolution becomes ambiguous — Dart will require you to disambiguate with
// ExtensionName(value).method()

Interview follow-ups

  1. Are extension methods resolved at compile time or runtime? Compile time. The compiler picks the extension based on the static (declared) type of the receiver. That's why Animal a = Dog(); a.label(); calls the Animal extension, not the Dog one — a common gotcha.

  2. Can extensions add fields or state? No. Only methods, getters/setters, and operators. Fields would require modifying the class's memory layout, which extensions deliberately don't touch.

  3. What happens if two imported extensions define the same method on the same type? Dart raises an ambiguity error at the call site. You resolve it explicitly: ExtensionName(value).method() or by hiding one extension during import.

  4. When would you prefer a static helper function over an extension? When the function doesn't logically "belong" to the type (e.g., parseUser(json) doesn't belong on Map), when you want to keep the type's surface small, or when polymorphic dispatch matters — then prefer a real method on an interface.

How helpful was this content?

Please sign in to rate this article.