Mixins & the with Keyword
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
}
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
}
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
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
•
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 SerializablePractical 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.