Mixins & Extension Methods
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.
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)}');
}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.
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}');
}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.
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);
}₹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>:
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');
}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.
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);
}₹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 SomeClassfails unless it is declaredmixinormixin class. - Forgetting to import the extension. If
45000.inrreports 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; adynamicvalue will not find them. - Putting too much in a mixin. Keep mixins small and focused on one behaviour.
Key points
- A
mixinholds reusable fields and methods; classes add it withwith, and can use several. mixin X on Ylimits a mixin to subclasses ofYand lets it callY's members.extension Name on Type { ... }adds methods and getters to existing types likeintandString.- Extensions can be generic, such as
extension SafeAccess<T> on List<T>. extension typecreates 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
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);
}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?
class A with M mixes the members of M into A.
2. What does mixin Cashback on Account mean?
on restricts where the mixin can be applied.
3. What is this inside extension RupeeFormat on int?
Inside an extension, this refers to the receiver value.
4. What does an extension type cost at run time?
Extension types exist only at compile 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
