Mixins in Dart — Reusable Behaviour Without Inheritance
When inheritance forces us to choose one parent, mixins let us combine behaviours freely. Here's how they work and when to use them.
The problem with single inheritance
We saw in the previous episode that a class can only extend one other class. Dog extends Animal. That's it. One parent.
But what if Dog also needs to be Swimmable? And Trainable? And Serializable?
class Animal {
void breathe() => print('breathing');
}
class Swimmable {
void swim() => print('swimming');
}
class Dog extends Animal, Swimmable { // ERROR!
// Dart doesn't allow multiple inheritance
}
We could use interfaces, but then we have to reimplement every method. If Swimmable has 10 methods, every swimming creature must write those 10 methods from scratch. That's not reuse — that's duplication.
This is where mixins come in. They let us add behaviour to a class without using up our single inheritance slot.
What is a mixin?
A mixin is a class that provides methods for other classes to use — but it's not meant to be instantiated on its own.
mixin Swimmable {
void swim() => print('swimming');
void dive() => print('diving deep');
}
mixin Trainable {
bool trained = false;
void train() {
trained = true;
print('training complete');
}
}
class Dog extends Animal with Swimmable, Trainable {
Dog(String name) : super(name);
}
var dog = Dog('Buddy');
dog.swim(); // swimming
dog.train(); // training complete
dog.breathe(); // breathing (from Animal)
The keyword is with. A class can extend one parent and mix in any number of mixins. The dog gets behaviour from Animal, Swimmable, and Trainable — all without code duplication.
Key insight: Mixins are about sharing implementation, not just interfaces. When Dog mixes in Swimmable, it gets the actual
swim() code. It doesn't have to write it.
Mixin vs class — when to use which
Before Dart 3, you could use a class as a mixin with the mixin class declaration. But this blurred the line between classes and mixins. Modern Dart encourages a cleaner separation.
Use a mixin when:
• You're defining reusable behaviour (not identity)
• The behaviour doesn't need its own constructor
• Multiple unrelated classes need this behaviour
Use a class when:
• You're defining a concrete thing (a User, a File, a Button)
• You need constructors and initialisers
• Inheritance makes semantic sense (a Dog is-a Animal)
// Good: mixin for behaviour
mixin Loggable {
void log(String message) => print('[\$runtimeType] \$message');
}
// Good: class for identity
class User with Loggable {
final String name;
User(this.name);
}
// Bad: class pretending to be reusable behaviour
class LoggingCapability { // should be a mixin
void log(String msg) => print(msg);
}
The practical test: If you'd never write var x = MyThing() to create an instance, it should probably be a mixin.
Restricting mixins with 'on'
Sometimes a mixin only makes sense on certain types of classes. A mixin that accesses this.name only works if the class has a name field.
The on keyword restricts which classes can use a mixin.
class Named {
final String name;
Named(this.name);
}
mixin Greeter on Named {
void greet() => print('Hello, I am \$name');
}
class Person extends Named with Greeter {
Person(String name) : super(name);
}
class Robot with Greeter { // ERROR!
// Robot doesn't extend Named
}
The mixin Greeter can only be applied to classes that extend (or implement) Named. This gives us compile-time safety — if the mixin needs certain members, the on clause guarantees they exist.
mixin Persistable on Model {
Future<void> save() async {
// 'id' and 'toJson()' come from Model
await database.save(id, toJson());
}
}
abstract class Model {
int get id;
Map<String, dynamic> toJson();
}
class User extends Model with Persistable {
@override
int get id => _id;
@override
Map<String, dynamic> toJson() => {'name': name};
}
Multiple constraints: A mixin can require multiple supertypes.
mixin Auditable on Named, Timestamped {
void audit() {
print('\$name modified at \$lastModified');
}
}
Linearisation — the order of mixins matters
When multiple mixins define the same method, which one wins? Dart uses linearisation — the rightmost mixin takes precedence.
mixin A {
void greet() => print('A');
}
mixin B {
void greet() => print('B');
}
class Test with A, B {}
void main() {
Test().greet(); // prints: B
}
B comes after A, so B's greet() wins. The order is: first the superclass, then mixins left to right. Later entries override earlier ones.
Calling super in mixins. A mixin can call
super.method() to invoke the previous version in the chain.
mixin Logger {
void log(String msg) => print('LOG: \$msg');
}
mixin TimestampLogger on Object {
void log(String msg) {
var time = DateTime.now();
super.log('[\$time] \$msg'); // calls previous log()
}
}
class App with Logger, TimestampLogger {}
App().log('started');
// prints: LOG: [2026-05-24 10:30:00] started
TimestampLogger calls super.log(), which resolves to Logger's log() method. This is called the super chain.
Real-world mixin patterns
Mixins shine when you have cross-cutting concerns — behaviours that span multiple unrelated classes.
Pattern 1: Diagnostics and debugging
mixin DiagnosticableMixin {
String get debugLabel => '\$runtimeType#\$hashCode';
void debugPrint(String message) {
print('[\$debugLabel] \$message');
}
Map<String, dynamic> debugInfo() => {
'type': '\$runtimeType',
'hashCode': hashCode,
};
}
Pattern 2: State management helpers
mixin ChangeNotifierMixin {
final _listeners = <void Function()>[];
void addListener(void Function() listener) {
_listeners.add(listener);
}
void notifyListeners() {
for (var listener in _listeners) {
listener();
}
}
}
class Counter with ChangeNotifierMixin {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
Pattern 3: Validation
mixin Validatable {
List<String> validate();
bool get isValid => validate().isEmpty;
void assertValid() {
var errors = validate();
if (errors.isNotEmpty) {
throw ValidationError(errors);
}
}
}
class User with Validatable {
final String email;
final int age;
User(this.email, this.age);
@override
List<String> validate() {
var errors = <String>[];
if (!email.contains('@')) errors.add('Invalid email');
if (age < 0) errors.add('Age cannot be negative');
return errors;
}
}
These patterns work because the mixin provides a template — some behaviour is implemented, some is left for the class to define.
Mixins vs extension methods
Dart 2.7 introduced extension methods. They also add behaviour to classes. So when do we use which?
Use extensions when:
• Adding convenience methods to types you don't control (String, int, List)
• The method doesn't need instance state
• You want optional, import-based activation
extension StringHelpers on String {
String get reversed => split('').reversed.join();
bool get isBlank => trim().isEmpty;
}
'hello'.reversed; // 'olleh'
Use mixins when:
• Adding state (fields) as well as methods
• The behaviour is part of the class's identity
• You need to override methods or participate in the type hierarchy
mixin Identifiable {
late final String id = _generateId();
String _generateId() => DateTime.now().millisecondsSinceEpoch.toString();
}
class Document with Identifiable {
final String title;
Document(this.title);
}
var doc = Document('Report');
print(doc.id); // unique ID per instance
Test your understanding
7 questions
Seven questions covering mixins, linearisation, and when to use mixins vs other patterns.