← Back to blog
Object-Oriented Dart · Part 7 of 11
June 30, 202613 min read

Mixins in Dart: Reusing Behavior Across Classes

DartOOPFlutter

Mixins in Dart

This is Part 7 of the Object-Oriented Dart series. We've seen inheritance (extends, one parent only) and interfaces (implements, contracts with no code). Now we fill the gap between them: mixins — Dart's way to reuse actual implementation across unrelated classes without forcing them into a single inheritance line.

If you've ever thought "I wish this Bird and this Plane could share fly() code, but they're not in the same hierarchy," mixins are the answer.


The problem: single inheritance is limiting

Dart classes can extends only one superclass. That's usually a good thing (it keeps hierarchies sane), but it creates a real problem when a behavior cuts across your hierarchy.

Say you have Bird, Airplane, and Superhero. All three can fly, but they share no sensible common ancestor — a bird isn't an airplane isn't a superhero. With only extends, your options are bad:

  • Copy-paste the fly() code into all three (duplication).
  • Invent a fake FlyingThing superclass and shoehorn everything under it (a broken hierarchy).
  • Put fly() on some base class everything extends, even non-flyers (leaks behavior to things that shouldn't have it).

implements doesn't help either — it would force each class to re-write fly(). What we want is to inject shared, working code into any class that wants it. That's a mixin.


Defining and using a mixin

You declare a mixin with the mixin keyword and apply it with with:

mixin Flyer {
  bool isGrounded = false;

  void fly() {
    if (isGrounded) {
      print('Cannot fly — grounded.');
    } else {
      print('Soaring through the sky!');
    }
  }
}

class Bird with Flyer {}
class Airplane with Flyer {}
class Superhero with Flyer {}

void main() {
  Bird().fly();      // Soaring through the sky!
  Airplane().fly();  // Soaring through the sky!
}

All three classes gained a fully-working fly() and an isGrounded field — no duplication, no fake hierarchy. A mixin is "a bundle of members you can stir into any class."

Mixing in several at once

The real power shows when you compose multiple mixins. List them after with, comma-separated:

mixin Swimmer {
  void swim() => print('Swimming');
}

mixin Walker {
  void walk() => print('Walking');
}

class Duck with Flyer, Swimmer, Walker {}

void main() {
  Duck()
    ..fly()
    ..swim()
    ..walk();
}

Duck now has three independent capabilities pulled from three sources. Compare this to inheritance, where you'd be stuck picking one parent. Mixins let you assemble a class from reusable behavior "modules."

Combining extends and with

Mixins layer on top of a normal superclass. The order is extends first, then with:

class Animal {
  void breathe() => print('breathing');
}

class Bird extends Animal with Flyer {
  // Has breathe() from Animal AND fly() from Flyer.
}

Mixins vs. inheritance vs. interfaces

It's worth pinning down where mixins sit:

| Tool | Gives you | Relationship | | --- | --- | --- | | extends | One parent's implementation | "is-a" | | implements | A contract (no code) | "behaves-as" | | with (mixin) | Reusable implementation from many sources | "can-do" / "has-the-ability-to" |

A mixin is closest to inheritance (you get real code), but you can apply many mixins, and they're not meant to stand alone as "is-a" parents. Think of mixins as horizontal code reuse (across unrelated classes) versus inheritance's vertical reuse (down a family line).

Naming convention: mixins are often named with an "-able" or "-er" adjective/role — Flyer, Comparable, Serializable, Disposable — because they describe a capability a class gains, not a thing it is.


Constraining a mixin with on

Sometimes a mixin's code only makes sense if it's applied to a particular kind of class — because it needs to call methods that come from that class. The on clause declares that requirement: "this mixin can only be applied to classes that are (or extend) X."

class Animal {
  String get name => 'animal';
  void eat() => print('$name is eating');
}

// This mixin REQUIRES being mixed onto an Animal,
// because it uses `name` and calls `eat()`.
mixin Hungry on Animal {
  void feedRepeatedly(int times) {
    for (var i = 0; i < times; i++) {
      eat(); // safe — `on Animal` guarantees eat() exists
    }
    print('$name is full');
  }
}

class Dog extends Animal with Hungry {}

// class Rock with Hungry {} // ❌ Error: Rock isn't an Animal.

The on Animal clause does two things:

  1. Restricts where the mixin can be applied — only to Animals (or subclasses). A Rock can't use it.
  2. Grants access to Animal's members inside the mixin — you can call eat() and read name, and even use super to reach the superclass's implementation.

on is what lets a mixin safely build on top of known behavior, rather than being a free-floating bundle.


Abstract members in mixins

A mixin can declare abstract members it depends on but doesn't implement, forcing the using class to supply them. This is like an interface contract, baked into the mixin:

mixin Describable {
  String get description; // abstract — the host class must provide it

  void printCard() {
    print('=== $description ==='); // uses the required member
  }
}

class Product with Describable {
  final String title;
  Product(this.title);

  @override
  String get description => 'Product: $title'; // required by the mixin
}

void main() {
  Product('Coffee').printCard(); // === Product: Coffee ===
}

Describable provides the reusable printCard() logic but says "you must tell me your description." This is a clean way to write template behavior: the mixin owns the algorithm, the host class fills in the specifics.


mixin class — both at once

Sometimes a type is genuinely useful both as a standalone class and as a mixin. Since Dart 3.0 you can declare a mixin class, which can be instantiated/extended like a class and mixed in with with:

mixin class Greeter {
  void greet() => print('Hello!');
}

class A extends Greeter {}   // used as a class
class B with Greeter {}      // used as a mixin

void main() {
  Greeter().greet(); // also instantiable on its own
}

The trade-off: a mixin class can't use an on clause and can't have non-default constructors (the usual mixin restrictions). Most of the time you'll declare a plain mixin; reach for mixin class only when you truly need both roles.

Why plain mixins can't have constructors: a mixin isn't instantiated on its own — it's folded into a host class whose own constructor runs. So a mixin can't declare a generative constructor (there's no construction step that belongs to it). It can have fields with initializers, though.


How conflicts resolve: linearization

What if two mixins define the same method? Dart doesn't error — it uses linearization: the mixins are stacked in the order written, and the last one wins.

mixin A {
  String greet() => 'A';
}

mixin B {
  String greet() => 'B';
}

class C with A, B {}        // B is applied last
class D with B, A {}        // A is applied last

void main() {
  print(C().greet()); // B  — last mixin wins
  print(D().greet()); // A
}

The mental model: class C with A, B builds a chain Object → A → B → C. A call to greet() resolves from the bottom up, so B's version (applied after A) shadows A's. Inside B you could even call super.greet() to reach A's version — this is how mixins can layer behavior, each wrapping the one before it. This stacking (sometimes called the "decorator-via-mixin" pattern) is powerful but worth keeping simple; rely on order-dependent overrides sparingly and document them.


Putting it together

A small game-entity example showing on, abstract members, and composition:

abstract class Entity {
  String get name;
  int health = 100;
}

mixin Damageable on Entity {
  void takeDamage(int amount) {
    health -= amount;
    print('$name took $amount damage (health: $health)');
    if (health <= 0) print('$name has been defeated!');
  }
}

mixin Healable on Entity {
  void heal(int amount) {
    health += amount;
    print('$name healed to $health');
  }
}

class Player extends Entity with Damageable, Healable {
  @override
  final String name;
  Player(this.name);
}

void main() {
  final hero = Player('Aria')
    ..takeDamage(30)
    ..heal(10)
    ..takeDamage(90);
  // Aria took 30 damage (health: 70)
  // Aria healed to 80
  // Aria took 90 damage (health: -10)
  // Aria has been defeated!
}

Player composes two capabilities — Damageable and Healable — both of which safely rely on Entity's name and health thanks to their on Entity constraint. Swap in a Monster extends Entity with Damageable and it reuses the exact same damage logic. That's mixins earning their keep.


Practice Challenges

Challenge 1 — Your first mixin. Define a Logger mixin with a log(String) method, and apply it to two unrelated classes Server and Client.

Show solution
mixin Logger {
  void log(String message) => print('[${DateTime.now()}] $message');
}

class Server with Logger {}
class Client with Logger {}

void main() {
  Server().log('started');
  Client().log('connected');
}

Both unrelated classes gain the same working log() without any shared superclass — the core win of mixins.

Challenge 2 — Multiple mixins. Create Walks and Barks mixins and a Dog that mixes in both. Call both behaviors.

Show solution
mixin Walks {
  void walk() => print('walking');
}

mixin Barks {
  void bark() => print('woof');
}

class Dog with Walks, Barks {}

void main() {
  Dog()
    ..walk()
    ..bark();
}

List mixins comma-separated after with; the class gains all of their members.

Challenge 3 — Constrain with on. Write a Stats mixin that can only be applied to a Character class (which has an int level) and adds levelUp().

Show solution
class Character {
  int level = 1;
}

mixin Stats on Character {
  void levelUp() {
    level++; // allowed because `on Character` guarantees level exists
    print('Leveled up to $level');
  }
}

class Hero extends Character with Stats {}

void main() {
  Hero()..levelUp()..levelUp(); // Leveled up to 2, then 3
}

on Character both restricts Stats to Characters and grants it access to level. Try class Rock with Stats {} and it won't compile.

Challenge 4 — Abstract member in a mixin. Write a Json mixin with a concrete printJson() that depends on an abstract Map<String, dynamic> toJson() the host must implement.

Show solution
mixin Json {
  Map<String, dynamic> toJson(); // abstract — host must supply
  void printJson() => print(toJson());
}

class User with Json {
  final String name;
  User(this.name);

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

void main() {
  User('Sam').printJson(); // {name: Sam}
}

The mixin owns the reusable printJson() algorithm; the host class fills in the toJson() specifics. Template behavior via a mixin.

Challenge 5 — Predict linearization. What does this print, and why?

mixin X { String tag() => 'X'; }
mixin Y { String tag() => 'Y'; }

class Z with X, Y {}

void main() => print(Z().tag());
Show answer

It prints Y. With with X, Y, mixins are applied left-to-right, so Y is applied last and its tag() shadows X's. The linearized chain is Object → X → Y → Z, and method resolution finds Y's version first. Reverse to with Y, X and it would print X.

Challenge 6 — Compose a real class. Model a SmartPhone that extends Device (has powerOn()) and mixes in Camera (takePhoto()) and GPS (locate()). Show all three behaviors.

Show solution
class Device {
  void powerOn() => print('Powering on');
}

mixin Camera {
  void takePhoto() => print('Click!');
}

mixin GPS {
  void locate() => print('Locating...');
}

class SmartPhone extends Device with Camera, GPS {}

void main() {
  SmartPhone()
    ..powerOn() // from Device (extends)
    ..takePhoto() // from Camera (mixin)
    ..locate();   // from GPS (mixin)
}

extends Device gives the "is-a" base; the two mixins layer on cross-cutting capabilities. This extends ... with A, B shape is exactly how Flutter composes much of its framework (e.g. State classes mixing in TickerProviderStateMixin).


Questions to test yourself

Q1 (basic). What keyword applies a mixin to a class, and what does the class gain?

Show answer

with. The class gains all of the mixin's members (methods, getters, fields) as real, working implementation — not just a contract.

Q2 (basic). How is a mixin different from implements?

Show answer

implements gives you only a contract — you must write every member yourself. A mixin injects actual code into your class, so you reuse the implementation. Mixins are about sharing behavior; interfaces are about promising it.

Q3 (intermediate). What two things does an on clause do for a mixin?

Show answer

(1) It restricts which classes can use the mixin — only the specified superclass (or its subclasses). (2) It grants the mixin access to that superclass's members (and super), so the mixin's code can safely call methods it knows will exist on the host.

Q4 (intermediate). Why can a regular mixin not declare a (non-default) constructor?

Show answer

A mixin is never instantiated on its own — it's folded into a host class, and the host's constructor runs during construction. There's no separate construction step belonging to the mixin, so it can't define a generative constructor. (It can still have fields with initializers.) If you need a type that's both constructible and mixable, use a mixin class, which gives up the on clause and custom constructors in exchange.

Q5 (advanced). Two mixins applied with with A, B both define run(). Which runs, and how could B.run() still use A's version?

Show answer

B's run() wins, because mixins are linearized left-to-right and the last applied mixin takes precedence (chain: Object → A → B → Class). Inside B.run() you can call super.run() to invoke the next implementation up the chain — A.run() — which is exactly how mixins can layer and wrap behavior rather than just replace it.

Q6 (advanced). When should you choose a mixin over (a) inheritance and (b) an interface? Give the deciding question for each.

Show answer

Versus inheritance: choose a mixin when the behavior is a cross-cutting capability shared by classes that don't belong to one "is-a" family, or when a class needs capabilities from multiple independent sources (single inheritance can't do that). Deciding question: "Is this a kind-of relationship, or just a shared ability?" Ability → mixin. Versus an interface: choose a mixin when you want to reuse real implementation, not just declare a contract. Deciding question: "Do I want to share code, or only require it?" Share code → mixin; only require → interface.


Wrapping up

Mixins are Dart's tool for horizontal code reuse:

  • A mixin is a reusable bundle of implementation applied with with — no fake hierarchies, no duplication.
  • You can mix in many mixins at once, and layer them on top of an extends superclass (extends X with A, B).
  • on constrains a mixin to a base type and grants it access to that base's members and super.
  • Mixins can declare abstract members the host must implement — clean template behavior.
  • mixin class serves as both a class and a mixin (at the cost of on and custom constructors).
  • Conflicts resolve by linearization — last mixin wins, with super reaching the previous layer.

In Part 8 we look at a special, restricted kind of class that you've probably used without thinking of it as OOP: enhanced enums — enums with fields, methods, and constructors.