Ankit Ranjan
Back to Deep Dives

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.

May 24, 2026 7 topics 7 quiz questions
Share:
1

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.

The single inheritance constraintAnimalSwimmableTrainableDogcan't extendboth!

This is where mixins come in. They let us add behaviour to a class without using up our single inheritance slot.

2

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.

Mixins add behaviour without using up inheritanceAnimalbreathe()extendsmixin Swimmableswim(), dive()withmixin Trainabletrain(), trainedwithclass Doghas: breathe(), swim(), dive(), train()from: Animal + Swimmable + Trainable

3

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.

4

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');
  }
}

5

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.

Linearisation: later mixins override earlier onesclass Dog extends Animal with A, B, CAnimalbaseAoverrides baseBoverrides ACoverrides BC wins

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.

6

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.

7

Mixins vs extension methods

Dart 2.7 introduced extension methods. They also add behaviour to classes. So when do we use which?

Mixins vs Extension MethodsAspectMixinExtension MethodApplied when?At class definition (with)At call site (import)Can add state?Yes — fields allowedNo — methods onlyCan override methods?YesNoWorks on types you own?YesYesWorks on external types?NoYes — String, List, etc.

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.

Search

Loading search...