EthicsComputerCourses
Join training +91 90532 10052

Mixins & Extension Methods

Flutter & Dart · Lesson 8 of 29 Beginner 6 min read Updated

Inheritance lets a class reuse code from one parent. But sometimes you want to share a small piece of behaviour with many unrelated classes: a shop, a doctor and a restaurant can all be "rateable", even though they have nothing else in common. Dart solves this with mixins.

You will also learn extension methods, which add new methods to types you did not write, such as int or String. Flutter and its packages use both features a lot: a State class often mixes in SingleTickerProviderStateMixin for animations, and a data class often uses with ChangeNotifier to tell the screen about changes.

Mixins: reusable pieces of a class

A mixin is declared with the mixin keyword. It can have fields and methods, but you cannot create an object from it. A class adds it with with.

Dart
mixin Rateable {
  final List<int> _ratings = [];

  void rate(int stars) {
    if (stars >= 1 && stars <= 5) _ratings.add(stars);
  }

  double get averageRating =>
      _ratings.isEmpty ? 0 : _ratings.reduce((a, b) => a + b) / _ratings.length;
}

class SweetShop with Rateable {
  final String name;
  SweetShop(this.name);
}

class Doctor with Rateable {
  final String name;
  final String speciality;
  Doctor(this.name, this.speciality);
}

void main() {
  final shop = SweetShop('Rawat Mishthan, Jaipur')
    ..rate(5)
    ..rate(4);
  final doctor = Doctor('Dr. Ananya Rao', 'Paediatrics')
    ..rate(5)
    ..rate(5)
    ..rate(3);

  print('${shop.name}: ${shop.averageRating}');
  print('${doctor.name}: ${doctor.averageRating.toStringAsFixed(2)}');
}
Output
Rawat Mishthan, Jaipur: 4.5
Dr. Ananya Rao: 4.33

SweetShop and Doctor share no parent class, yet both gained the rate method and the averageRating getter. A class can extend one class and mix in several mixins at once: class Clinic extends Building with Rateable, Bookable.

Restricting a mixin with on

Sometimes a mixin needs something from the class that uses it. The on keyword says "this mixin can only be used with subclasses of this type", so the mixin can call that type's members.

Dart
abstract class Account {
  int balance = 0;
  void credit(int amount) => balance += amount;
}

mixin CashbackOnTopUp on Account {
  int get cashbackPercent;

  void topUp(int amount) {
    credit(amount);
    final cashback = amount * cashbackPercent ~/ 100;
    credit(cashback);
    print('Topped up ₹$amount, cashback ₹$cashback');
  }
}

class StudentWallet extends Account with CashbackOnTopUp {
  @override
  int get cashbackPercent => 5;
}

void main() {
  final wallet = StudentWallet();
  wallet.topUp(400);
  wallet.topUp(200);
  print('Balance: ₹${wallet.balance}');
}
Output
Topped up ₹400, cashback ₹20
Topped up ₹200, cashback ₹10
Balance: ₹630

Because of on Account, the mixin may call credit. It also declares an abstract getter, cashbackPercent, which every class that uses the mixin must provide. If you tried class Shop with CashbackOnTopUp without extending Account, the code would not compile.

Extension methods

An extension adds methods, getters or operators to an existing type without changing or subclassing it. This is how you add a helper like .inr to every int.

Dart
extension RupeeFormat on int {
  /// Formats a whole rupee amount with Indian digit grouping, e.g. 12,34,567.
  String get inr {
    final sign = this < 0 ? '-' : '';
    final digits = abs().toString();
    if (digits.length <= 3) return '$sign₹$digits';
    final lastThree = digits.substring(digits.length - 3);
    var rest = digits.substring(0, digits.length - 3);
    final groups = <String>[];
    while (rest.length > 2) {
      groups.insert(0, rest.substring(rest.length - 2));
      rest = rest.substring(0, rest.length - 2);
    }
    if (rest.isNotEmpty) groups.insert(0, rest);
    return '$sign₹${groups.join(',')},$lastThree';
  }
}

extension TextTools on String {
  String get initials => split(' ').where((w) => w.isNotEmpty).map((w) => w[0]).join();
  bool get isIndianMobile => RegExp(r'^[6-9]\d{9}$').hasMatch(this);
}

void main() {
  print(999.inr);
  print(45000.inr);
  print(1234567.inr);
  print((-250000).inr);
  print('Kabir Singh Rathore'.initials);
  print('9876543210'.isIndianMobile);
  print('12345'.isIndianMobile);
}
Output
₹999
₹45,000
₹12,34,567
-₹2,50,000
KSR
true
false

Inside an extension, this is the value the method was called on, so abs() means this.abs(). The regular expression ^[6-9]\d{9}$ means "a digit from 6 to 9 followed by exactly nine more digits", which matches the shape of an Indian mobile number. Extensions are resolved by the static type, so you must import the file that declares the extension wherever you use it.

Extensions on generic types

You can write an extension for a whole family of types, such as every List<int> or every Iterable<T>:

Dart
extension MarksStats on List<int> {
  int get total => fold(0, (sum, m) => sum + m);
  double get average => isEmpty ? 0 : total / length;
}

extension SafeAccess<T> on List<T> {
  T? at(int index) => index >= 0 && index < length ? this[index] : null;
}

void main() {
  final marks = [88, 74, 92];
  print('Total ${marks.total}, average ${marks.average.toStringAsFixed(1)}');

  final cities = ['Pune', 'Lucknow'];
  print(cities.at(1));
  print(cities.at(5) ?? 'No city at that position');
}
Output
Total 254, average 84.7
Lucknow
No city at that position

SafeAccess<T> works for a list of any type T. Dart's own libraries already have similar helpers: list.firstOrNull, list.lastOrNull and list.elementAtOrNull(5) are built-in extensions, so check the documentation before writing your own.

Extension types: zero-cost wrappers

An extension type (Dart 3.3) gives an existing value a new type with its own methods, without creating a wrapper object at run time. It is useful when two values share a representation but must not be mixed up, such as a price in paise and a quantity, which are both int.

Dart
extension type Paise(int value) {
  Paise operator +(Paise other) => Paise(value + other.value);
  String get display => '₹${(value / 100).toStringAsFixed(2)}';
}

void main() {
  final tea = Paise(1500);
  final biscuits = Paise(2050);
  final bill = tea + biscuits;
  print(bill.display);
  print(bill.value);
}
Output
₹35.50
3550

The compiler treats Paise as its own type, so passing a plain int where a Paise is expected is an error. At run time it is just an int, so there is no extra cost. You will not need extension types in your first apps, but you will see them in packages that wrap JavaScript or platform APIs.

Common mistakes

  • Using a class as a mixin. In Dart 3, with SomeClass fails unless it is declared mixin or mixin class.
  • Forgetting to import the extension. If 45000.inr reports that the getter is not defined, the file with the extension is not imported.
  • Expecting extensions on dynamic. Extensions are chosen from the static type; a dynamic value will not find them.
  • Putting too much in a mixin. Keep mixins small and focused on one behaviour.

Key points

  • A mixin holds reusable fields and methods; classes add it with with, and can use several.
  • mixin X on Y limits a mixin to subclasses of Y and lets it call Y's members.
  • extension Name on Type { ... } adds methods and getters to existing types like int and String.
  • Extensions can be generic, such as extension SafeAccess<T> on List<T>.
  • extension type creates a distinct compile-time type with no run-time wrapper.

Exercise

Write an extension Distance on double with a getter km that formats the value as kilometres with one decimal ('12.5 km') and a getter autoFare that returns an int fare: ₹30 for the first 2 km, then ₹15 per km after that (round the extra distance up to a whole kilometre with ceil()). Also write a mixin Trackable with a List<String> history and a method log(String event), and use it in a class AutoRide. Print the fare for 1.5 km and 7.2 km, and print the ride history.

Show solution
Dart
extension Distance on double {
  String get km => '${toStringAsFixed(1)} km';
  int get autoFare => this <= 2 ? 30 : 30 + ((this - 2).ceil() * 15);
}

mixin Trackable {
  final List<String> history = [];
  void log(String event) => history.add(event);
}

class AutoRide with Trackable {
  final double distance;
  AutoRide(this.distance) {
    log('Booked for ${distance.km}');
  }

  int finish() {
    log('Finished, fare ₹${distance.autoFare}');
    return distance.autoFare;
  }
}

void main() {
  print(1.5.autoFare);
  final ride = AutoRide(7.2);
  print(ride.finish());
  print(ride.history);
}
Output
30
120
[Booked for 7.2 km, Finished, fare ₹120]

For 7.2 km the extra distance is 5.2 km, which ceil() rounds up to 6, so the fare is 30 + 6 x 15 = 120. The extension works on any double, and the mixin gives AutoRide a history without needing a parent class.

Quick Quiz

1. Which keyword adds a mixin to a class?

2. What does mixin Cashback on Account mean?

3. What is this inside extension RupeeFormat on int?

4. What does an extension type cost at run time?

Learn Flutter & Dart with a mentor

Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call