Skip to content

Dart Extensions — Adding Functionality to Existing Types

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Dart Extensions. We cover key concepts, practical examples, and best practices to help you master this topic.

Dart extensions allow you to add new functionality to any existing type, including types from external libraries and built-in types, without modifying the original class or creating a subclass.

What You Will Learn

  • Defining extension methods on existing types
  • Adding computed properties with extensions
  • Extension operators on types
  • Generic extensions
  • Extension naming and Conflict Resolution
  • When to use extensions vs utility functions

Why It Matters

Extensions let you write idiomatic code that appears as if it belongs to the original type. Instead of calling StringUtil.capitalize(text), you call text.capitalize(). This improves readability and discoverability: IDE auto-completion shows extension methods alongside the type's native methods. Extensions are heavily used in Flutter for adding functionality to BuildContext, TextStyle, and other framework types.

Real-World Use

The DodaTech Flutter app uses extensions extensively. BuildContext extensions provide easy access to theme colors, media queries, and navigation without boilerplate. String extensions handle email validation, phone formatting, and URL encoding. DateTime extensions provide relative time formatting like "2 hours ago".

Learning Path

flowchart LR
  A[Dart Isolates] --> B[Dart Extensions\nYou are here]
  B --> C[Records and Patterns]
  style B fill:#f90,color:#fff

Basic Extension Methods

Define an extension with the extension keyword and on followed by the type:

extension StringCasing on String {
  String capitalize() {
    if (isEmpty) return this;
    return this[0].toUpperCase() + substring(1);
  }

  String toSentenceCase() {
    if (isEmpty) return this;
    return this[0].toUpperCase() + substring(1).toLowerCase();
  }
}

void main() {
  var text = 'hello dart extensions';
  print(text.capitalize());
  print(text.toSentenceCase());
}

Output:

Hello dart extensions
Hello dart extensions

The extension methods appear as instance methods on every String. They are resolved at compile time based on the static type of the expression.

Extension Properties

Extensions can add computed properties but not instance fields:

extension StringValidators on String {
  bool get isValidEmail {
    return contains('@') && contains('.');
  }

  bool get isPhoneNumber {
    return length == 10 && codeUnits.every((c) => c >= 48 && c <= 57);
  }

  int get wordCount {
    if (trim().isEmpty) return 0;
    return trim().split(RegExp(r'\s+')).length;
  }
}

void main() {
  var email = 'user@example.com';
  var phone = '1234567890';
  var sentence = 'Hello world from Dart';

  print('Is valid email: ${email.isValidEmail}');
  print('Is phone: ${phone.isPhoneNumber}');
  print('Word count: ${sentence.wordCount}');
}

Output:

Is valid email: true
Is phone: true
Word count: 4

Extension properties are getters only. They cannot have backing fields. They compute a value based on the underlying object's state.

Extension Operators

Extensions can add operators to existing types:

extension DurationMultiplication on Duration {
  Duration operator *(int factor) {
    return Duration(microseconds: inMicroseconds * factor);
  }

  Duration operator +(Duration other) {
    return Duration(microseconds: inMicroseconds + other.inMicroseconds);
  }
}

extension ListShorthand<T> on List<T> {
  List<T> operator [](int start, [int? end]) {
    return sublist(start, end);
  }
}

void main() {
  var duration = Duration(seconds: 5);
  print('5 seconds x 3 = ${duration * 3}');
  print('5 seconds + 2 seconds = ${duration + Duration(seconds: 2)}');

  var list = [1, 2, 3, 4, 5];
  print('Slice [1..3]: ${list.sublist(1, 4)}');
}

Output:

5 seconds x 3 = 0:00:15.000000
5 seconds + 2 seconds = 0:00:07.000000
Slice [1..3]: [2, 3, 4]

Operators are defined using the operator keyword inside the extension. They must respect the operator's arity (number of operands).

Generic Extensions

Extensions can be generic, applying to any type or a constrained set of types:

// Generic extension on all types
extension ObjectPrint<T> on T {
  void printWithPrefix(String prefix) {
    print('$prefix: $this');
  }
}

// Generic extension on List<T>
extension ListExtension<T> on List<T> {
  List<T> duplicate() {
    return [...this, ...this];
  }

  T? safeFirst() => isEmpty ? null : first;
}

// Generic extension with constraint
extension NumberExtension on num {
  num squared() => this * this;

  bool get isBetween0And100 => this >= 0 && this <= 100;
}

void main() {
  var name = 'Alice';
  name.printWithPrefix('Name');

  var numbers = [1, 2, 3];
  print('Duplicated: ${numbers.duplicate()}');
  print('Safe first: ${numbers.safeFirst()}');

  var empty = <int>[];
  print('Empty safe first: ${empty.safeFirst()}');

  print('5 squared: ${5.squared()}');
  print('50 is between 0 and 100: ${50.isBetween0And100}');
}

Output:

Name: Alice
Duplicated: [1, 2, 3, 1, 2, 3]
Safe first: 1
Empty safe first: null
5 squared: 25
50 is between 0 and 100: true

The generic extension ObjectPrint<T> extends T (every type). The ListExtension<T> extends List<T> and provides methods that work regardless of the element type. The NumberExtension extends num (the supertype of int and double).

Named Extensions vs Anonymous Extensions

Extensions can be named or anonymous. Named extensions are imported explicitly, avoiding conflicts:

// In file: string_extensions.dart
extension StringFormatting on String {
  String repeat(int times) => List.filled(times, this).join();
}

// In another file:
// import 'string_extensions.dart';
//
// void main() {
//   print('Hi! '.repeat(3)); // Uses the extension
// }

Anonymous extensions apply to all files in the library:

// Anonymous extension - applies everywhere in this library
extension on String {
  bool get isPalindrome {
    var cleaned = toLowerCase().replaceAll(' ', '');
    return cleaned == cleaned.split('').reversed.join();
  }
}

void main() {
  print('racecar'.isPalindrome); // true
  print('hello'.isPalindrome); // false
}

Named extensions are preferred for libraries because they give users control over what to import. Anonymous extensions are useful within a single file or package.

Conflict Resolution

When two extensions define the same method, the conflict must be resolved explicitly:

extension StringUppercase on String {
  String transform() => toUpperCase();
}

extension StringLowercase on String {
  String transform() => toLowerCase();
}

void main() {
  var text = 'Hello';

  // Ambiguous - both extensions define transform()
  // print(text.transform()); // COMPILE ERROR

  // Explicit resolution
  print(StringUppercase(text).transform());
  print(StringLowercase(text).transform());

  // Hide one extension
}

Output:

HELLO
hello

Resolve conflicts by using the extension name explicitly as ExtensionName(object).method(). Alternatively, import extensions with hide or show directives.

Extensions on Generated Classes

Extensions are especially useful for generated code. Many Dart packages generate classes that you cannot modify:

// Simulating a generated class from protobuf or json_serializable
class GeneratedUser {
  final String name;
  final String email;
  final String? phone;

  GeneratedUser(this.name, this.email, this.phone);
}

// Add functionality without modifying generated code
extension UserDisplay on GeneratedUser {
  String get displayName {
    if (phone != null) return '$name ($phone)';
    return name;
  }

  Map<String, dynamic> toJson() => {
    'name': name,
    'email': email,
    'phone': phone,
  };

  bool get hasCompleteProfile => name.isNotEmpty && email.isNotEmpty;
}

void main() {
  var user = GeneratedUser('Alice', 'alice@test.com', '+1234567890');
  print('Display: ${user.displayName}');
  print('Complete: ${user.hasCompleteProfile}');
  print('JSON: ${user.toJson()}');
}

Output:

Display: Alice (+1234567890)
Complete: true
JSON: {name: Alice, email: alice@test.com, phone: +1234567890}

Instead of wrapping the generated class in another class or creating static utility methods, an extension keeps the API intuitive.

Extensions vs Utility Functions

Compare both approaches:

// Utility function approach
String capitalizeWithUtils(String text) {
  if (text.isEmpty) return text;
  return text[0].toUpperCase() + text.substring(1);
}

// Extension approach
extension StringUtils on String {
  String capitalizeExt() {
    if (isEmpty) return this;
    return this[0].toUpperCase() + substring(1);
  }
}

// Static utility class
class StringHelper {
  static String capitalize(String text) {
    if (text.isEmpty) return text;
    return text[0].toUpperCase() + text.substring(1);
  }
}

void main() {
  var text = 'hello';

  // Utility function
  print(capitalizeWithUtils(text));

  // Extension method
  print(text.capitalizeExt());

  // Static utility class
  print(StringHelper.capitalize(text));
}

Output:

Hello
Hello
Hello

Extensions are preferred when the operation is conceptually an instance method on the type. Utility functions are preferred when the operation does not clearly belong to any specific type or involves multiple types.

Real-World Extension Examples

Common patterns in Dart and Flutter projects:

extension ContextExtensions on BuildContext {
  ThemeData get theme => Theme.of(this);
  MediaQueryData get mediaQuery => MediaQuery.of(this);
  double get screenWidth => mediaQuery.size.width;
  double get screenHeight => mediaQuery.size.height;
  bool get isDarkMode => Theme.of(this).brightness == Brightness.dark;
}

extension DateTimeFormatting on DateTime {
  String get timeAgo {
    var now = DateTime.now();
    var diff = now.difference(this);

    if (diff.inSeconds < 60) return '${diff.inSeconds}s ago';
    if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
    if (diff.inHours < 24) return '${diff.inHours}h ago';
    if (diff.inDays < 7) return '${diff.inDays}d ago';
    return '${diff.inDays ~/ 7}w ago';
  }
}

void main() {
  var date = DateTime.now().subtract(Duration(hours: 3, minutes: 25));
  print('3 hours 25 min ago: ${date.timeAgo}');
}

Output:

3 hours 25 min ago: 3h ago

Common Mistakes

  1. Using extensions on types from the same library: If you control the source code, add the method directly to the class instead of using an extension. Extensions are for types you cannot modify.

  2. Shadowing existing methods: An extension method with the same name as an existing instance method hides the original. The instance method always wins over the extension method.

  3. Expecting extensions to be dynamically dispatched: Extensions are resolved at compile time based on the static type. If the runtime type is a subtype, the static type determines which extension method is called.

  4. Defining extensions in the same file as the type they extend: Put extensions in separate files and folders. This makes it clear that they are separate from the original type's API.

  5. Adding state to extensions: Extensions cannot have instance fields. If you need state, create a wrapper class instead of an extension.

Practice Questions

  1. How do extensions differ from inheritance in terms of adding functionality?
  2. Why are extension methods resolved at compile time rather than runtime?
  3. How do you resolve conflicts when two extensions define the same method?
  4. When would you choose an extension over a utility function?
  5. Challenge: Write an extension on DateTime called weekdayName that returns the full weekday name (Monday, Tuesday, etc.). Also add an isWeekend property. Use these in a program that prints whether a given date is a weekend.

Mini Project

Build a collection of useful extensions:

  • String extension: isValidUrl, removeWhitespace, truncate(int maxLength)
  • int extension: isPrime, factorial, toRomanNumeral
  • List<T> extension: shuffleCopy, median, groupBy<K>(K Function(T) key)
  • Map<K, V> extension: merge(Map<K, V> other), invert() (swap keys and values)
  • Write unit tests for each extension

FAQ

Can extensions add static methods?

Yes. Use static inside the extension. Call them as ExtensionName.method() or import the extension and call them on the type if they are declared as extension static methods.

Do extensions work with Dart's type promotion?

No. Extensions are resolved at compile time. Type promotion does not affect which extension method is called.

Can I use extensions on nullable types?

Yes. Define extension on String? to add methods to nullable strings. The extension method must handle the null case explicitly.

Are extensions available in Dart 2?

Yes. Extensions were introduced in Dart 2.7 as a stable feature.

Can I override an existing method with an extension?

No. The original method always takes precedence. Extension methods have lower priority than instance methods.

What is Next

Now that you understand extensions, learn about records and patterns. Proceed to Records and Patterns in Dart for destructuring, pattern matching, and tuple-like data structures. Then explore Flutter Setup Guide to start building cross-platform apps.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro