Dart Object-Oriented Programming

Mixins & the with Keyword

40 min Lesson 6 of 8

The Problem: Single Inheritance Is Limiting

In the previous lessons, you learned that Dart only supports single inheritance -- a class can extend exactly one parent. But what happens when you need a class to share behavior from multiple sources? For example, a SmartPhone needs camera features, GPS features, and phone features. You cannot write class SmartPhone extends Camera, GPS, Phone.

This is where mixins come in. A mixin is a way to reuse a class’s code in multiple class hierarchies without inheritance. Think of mixins as “plug-in abilities” that you can attach to any class.

What Is a Mixin?

A mixin is a class-like structure that defines methods and properties, but is meant to be “mixed into” other classes using the with keyword. Unlike a parent class, a mixin cannot be instantiated on its own and does not create an is-a relationship.

Your First Mixin

// Define mixins with the 'mixin' keyword
mixin Swimming {
  void swim() => print('Swimming through the water!');
  int get swimSpeed => 10;
}

mixin Flying {
  void fly() => print('Flying through the air!');
  int get flySpeed => 50;
}

mixin Running {
  void run() => print('Running on the ground!');
  int get runSpeed => 30;
}

// Use mixins with the 'with' keyword
class Duck extends Animal with Swimming, Flying, Running {
  Duck(String name) : super(name);
}

class Fish extends Animal with Swimming {
  Fish(String name) : super(name);
}

class Eagle extends Animal with Flying {
  Eagle(String name) : super(name);
}

class Animal {
  String name;
  Animal(this.name);

  @override
  String toString() => name;
}

void main() {
  var duck = Duck('Donald');
  duck.swim();  // Swimming through the water!
  duck.fly();   // Flying through the air!
  duck.run();   // Running on the ground!

  var fish = Fish('Nemo');
  fish.swim();  // Swimming through the water!
  // fish.fly();  // ERROR! Fish doesn't have Flying mixin

  var eagle = Eagle('Sam');
  eagle.fly();  // Flying through the air!
  // eagle.swim();  // ERROR! Eagle doesn't have Swimming mixin
}
Key Difference: With inheritance, Dog is a Animal. With mixins, Duck can swim, fly, and run. Mixins add capabilities, not identity. A class can use unlimited mixins but extend only one parent.

Mixin Syntax

There are two ways to define a mixin:

Defining Mixins

// Way 1: Using the mixin keyword (recommended)
mixin Loggable {
  void log(String message) {
    print('[${DateTime.now().toIso8601String().substring(11, 19)}] $message');
  }
}

// Way 2: Using mixin class (Dart 3+) -- can be both a mixin AND a class
mixin class Identifiable {
  String get id => hashCode.toString();

  void printId() {
    print('ID: $id');
  }
}

// mixin class can be used as a mixin OR instantiated
class User with Loggable, Identifiable {
  String name;
  User(this.name);
}

// Identifiable can also be used as a regular class
class Record extends Identifiable {
  String data;
  Record(this.data);
}

void main() {
  var user = User('Ahmed');
  user.log('User created');  // [14:30:25] User created
  user.printId();              // ID: 123456789

  var record = Record('test');
  record.printId();  // ID: 987654321
}

Mixins with Properties and State

Mixins can have properties, getters, setters, and maintain state -- just like regular classes:

Stateful Mixins

mixin Cacheable {
  final Map<String, dynamic> _cache = {};

  void cacheValue(String key, dynamic value) {
    _cache[key] = value;
  }

  dynamic getCachedValue(String key) => _cache[key];

  bool isCached(String key) => _cache.containsKey(key);

  void clearCache() => _cache.clear();

  int get cacheSize => _cache.length;
}

mixin Timestamped {
  DateTime? _createdAt;
  DateTime? _updatedAt;

  DateTime get createdAt => _createdAt ?? DateTime.now();
  DateTime? get updatedAt => _updatedAt;

  void markCreated() => _createdAt = DateTime.now();
  void markUpdated() => _updatedAt = DateTime.now();
}

class UserProfile with Cacheable, Timestamped {
  String name;
  String email;

  UserProfile(this.name, this.email) {
    markCreated();
  }

  void updateEmail(String newEmail) {
    email = newEmail;
    markUpdated();
    cacheValue('lastEmail', newEmail);
  }
}

void main() {
  var profile = UserProfile('Ahmed', 'ahmed@test.com');

  profile.updateEmail('new@test.com');
  print(profile.email);                          // new@test.com
  print(profile.getCachedValue('lastEmail'));    // new@test.com
  print(profile.cacheSize);                       // 1
  print(profile.createdAt);                       // DateTime
  print(profile.updatedAt);                       // DateTime
}

Restricting Mixins with on

You can restrict a mixin so it can only be used on classes that extend a specific type. Use the on keyword:

Mixin Restrictions

class Widget {
  void render() => print('Rendering widget...');
}

// This mixin can ONLY be used on classes that extend Widget
mixin Draggable on Widget {
  bool _isDragging = false;

  void startDrag() {
    _isDragging = true;
    print('Started dragging');
  }

  void endDrag() {
    _isDragging = false;
    print('Stopped dragging');
    render();  // Can call Widget's methods because of 'on Widget'
  }

  bool get isDragging => _isDragging;
}

mixin Resizable on Widget {
  double _scale = 1.0;

  void resize(double factor) {
    _scale *= factor;
    print('Resized to ${(_scale * 100).toInt()}%');
    render();  // Can call Widget methods
  }

  double get scale => _scale;
}

// Works -- Button extends Widget
class Button extends Widget with Draggable, Resizable {
  String label;
  Button(this.label);

  @override
  void render() => print('Rendering button: $label');
}

// ERROR -- String doesn't extend Widget
// class TextBlock extends String with Draggable {}  // Compile error!

void main() {
  var btn = Button('Submit');
  btn.startDrag();    // Started dragging
  btn.endDrag();      // Stopped dragging → Rendering button: Submit
  btn.resize(1.5);    // Resized to 150% → Rendering button: Submit
}
Flutter Example: Flutter uses this pattern extensively. For example, SingleTickerProviderStateMixin can only be used on State -- it requires access to State’s lifecycle methods. This ensures the mixin is only applied where it makes sense.

Mixin Order Matters (Linearization)

When multiple mixins define the same method, the last mixin wins. Dart applies mixins left to right, with each one layering on top of the previous. This is called linearization.

Mixin Ordering

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

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

mixin C {
  String greet() => 'Hello from C';
}

class Test1 with A, B, C {}  // C wins (last)
class Test2 with C, B, A {}  // A wins (last)
class Test3 with A, C, B {}  // B wins (last)

void main() {
  print(Test1().greet());  // Hello from C
  print(Test2().greet());  // Hello from A
  print(Test3().greet());  // Hello from B
}

// The order is: Base → A → B → C (each layers on top)
// So the last mixin's method overrides all previous ones
Be Careful: Mixin ordering conflicts can cause subtle bugs. If two mixins define the same method, you might get unexpected behavior depending on the order. To avoid this, keep mixins focused on distinct responsibilities and avoid overlapping method names.

Calling super in Mixins

Mixins can call super to invoke the next implementation in the mixin chain:

super in Mixin Chain

class Base {
  void describe() => print('I am Base');
}

mixin Logger on Base {
  @override
  void describe() {
    print('[Logger] Before');
    super.describe();  // Calls Base.describe() or next mixin
    print('[Logger] After');
  }
}

mixin Validator on Base {
  @override
  void describe() {
    print('[Validator] Before');
    super.describe();  // Calls Logger.describe()
    print('[Validator] After');
  }
}

class Service extends Base with Logger, Validator {}

void main() {
  Service().describe();
  // [Validator] Before    -- Validator runs first (last mixin)
  // [Logger] Before       -- Logger is next in chain
  // I am Base             -- Base is at the bottom
  // [Logger] After
  // [Validator] After
}

extends vs implements vs with

Summary Table:
extends -- Inherit ONE parent class. Get all its code. Override what you want. Creates is-a relationship.
implements -- Promise to implement ALL members from one or more classes. Get nothing for free. Creates can-do contract.
with -- Mix in capabilities from one or more mixins. Get all their code. No is-a relationship. Creates has-ability relationship.

You can combine all three:
class MyWidget extends StatefulWidget with TickerProviderMixin implements Serializable

Practical Example: Flutter-Style Mixins

Building a Game Character System

// Base class
class Character {
  final String name;
  int _health;
  int _level;

  Character(this.name, {int health = 100, int level = 1})
      : _health = health,
        _level = level;

  int get health => _health;
  int get level => _level;
  bool get isAlive => _health > 0;

  void takeDamage(int amount) {
    _health = (_health - amount).clamp(0, 999);
    if (!isAlive) print('$name has been defeated!');
  }

  void heal(int amount) {
    _health = (_health + amount).clamp(0, 100 + _level * 10);
    print('$name healed to $_health HP');
  }

  void levelUp() {
    _level++;
    print('$name leveled up to $_level!');
  }

  @override
  String toString() => '$name [Lv.$_level HP:$_health]';
}

// Capability mixins
mixin MeleeAttack on Character {
  int get meleeDamage => 10 + level * 2;

  void meleeAttack(Character target) {
    print('$name strikes $target with melee for $meleeDamage damage!');
    target.takeDamage(meleeDamage);
  }
}

mixin MagicCaster on Character {
  int _mana = 100;
  int get mana => _mana;
  int get magicDamage => 15 + level * 3;

  void castSpell(Character target, {String spell = 'Fireball'}) {
    if (_mana < 20) {
      print('$name: Not enough mana!');
      return;
    }
    _mana -= 20;
    print('$name casts $spell on $target for $magicDamage damage! (Mana: $_mana)');
    target.takeDamage(magicDamage);
  }

  void restoreMana(int amount) {
    _mana = (_mana + amount).clamp(0, 100 + level * 5);
  }
}

mixin Healer on Character {
  int get healPower => 20 + level * 5;

  void healAlly(Character target) {
    print('$name heals $target for $healPower HP!');
    target.heal(healPower);
  }
}

mixin Stealthy on Character {
  bool _isHidden = false;
  bool get isHidden => _isHidden;

  void hide() {
    _isHidden = true;
    print('$name vanishes into the shadows...');
  }

  void reveal() {
    _isHidden = false;
    print('$name appears from the shadows!');
  }
}

// Combine mixins to create unique character classes
class Warrior extends Character with MeleeAttack {
  Warrior(String name) : super(name, health: 120);
}

class Mage extends Character with MagicCaster, Healer {
  Mage(String name) : super(name, health: 70);
}

class Rogue extends Character with MeleeAttack, Stealthy {
  Rogue(String name) : super(name, health: 85);
}

class Paladin extends Character with MeleeAttack, MagicCaster, Healer {
  Paladin(String name) : super(name, health: 100);
}

void main() {
  var warrior = Warrior('Thor');
  var mage = Mage('Gandalf');
  var rogue = Rogue('Shadow');
  var paladin = Paladin('Arthur');

  print('--- Battle ---');
  warrior.meleeAttack(mage);
  // Thor strikes Gandalf [Lv.1 HP:70] with melee for 12 damage!

  mage.castSpell(warrior);
  // Gandalf casts Fireball on Thor [Lv.1 HP:120] for 18 damage!

  mage.healAlly(mage);
  // Gandalf heals Gandalf [Lv.1 HP:58] for 25 HP!

  rogue.hide();
  // Shadow vanishes into the shadows...

  rogue.meleeAttack(mage);
  // Shadow strikes Gandalf [Lv.1 HP:83] with melee for 12 damage!

  paladin.meleeAttack(rogue);
  paladin.castSpell(rogue, spell: 'Holy Smite');
  paladin.healAlly(warrior);

  print('\n--- Status ---');
  print(warrior);   // Thor [Lv.1 HP:102]
  print(mage);      // Gandalf [Lv.1 HP:71]
  print(rogue);     // Shadow [Lv.1 HP:55]
  print(paladin);   // Arthur [Lv.1 HP:100]
}

Practice Exercise

Open DartPad and build a smart device system: (1) Create a base class Device with name, brand, and batteryLevel properties. (2) Create mixin WiFiCapable with connect(ssid), disconnect(), and isConnected getter. (3) Create mixin BluetoothCapable with pair(deviceName), unpair(), and pairedDevices list. (4) Create mixin CameraCapable (restricted to Device with on) with takePhoto() and recordVideo(seconds) methods that decrease battery. (5) Create mixin GPSCapable with getLocation() returning a string. (6) Build: SmartPhone extends Device with WiFiCapable, BluetoothCapable, CameraCapable, GPSCapable, SmartWatch extends Device with BluetoothCapable, GPSCapable, and Laptop extends Device with WiFiCapable, BluetoothCapable, CameraCapable. (7) Create one of each device and demonstrate all their capabilities.