OOP in Dart
Classes, named and factory constructors, mixins, and inheritance in Dart.
Classes
class Car {
String model;
int speed = 0;
Car(this.model); // constructor shorthand — assigns the argument directly to the field
void accelerate(int amount) {
speed += amount;
}
String describe() {
return '$model is going $speed km/h';
}
}
void main() {
var car = Car('Civic');
car.accelerate(40);
print(car.describe()); // Civic is going 40 km/h
}
The Car(this.model) syntax is a common Dart shorthand: it declares a constructor parameter that's automatically assigned to the field of the same name, avoiding a manually written this.model = model; body.
Named constructors
A class can have any number of additional constructors, each with a distinct name, useful for offering multiple meaningful ways to build the same type:
class Point {
final double x;
final double y;
Point(this.x, this.y);
Point.origin() : x = 0, y = 0; // a named constructor — initializer list sets final fields
@override
String toString() => '($x, $y)';
}
void main() {
var p1 = Point(3, 4);
var p2 = Point.origin();
print('$p1 $p2'); // (3.0, 4.0) (0.0, 0.0)
}
Factory constructors
A factory constructor doesn't have to create a new instance every time it's called — it can return a cached instance, an instance of a subclass, or run logic before deciding what to construct. This is how Dart implements patterns like singletons or object pooling within ordinary constructor syntax:
class Logger {
static final Logger _instance = Logger._internal();
factory Logger() {
return _instance; // always returns the same shared instance
}
Logger._internal(); // private named constructor, only callable from within this class
void log(String message) => print('[LOG] $message');
}
void main() {
var logger1 = Logger();
var logger2 = Logger();
print(identical(logger1, logger2)); // true — both point to the same singleton instance
}
Factory constructors are also commonly used to parse and construct an object from data, such as decoding JSON into a model class:
class User {
final String name;
final int age;
User({required this.name, required this.age});
factory User.fromJson(Map<String, dynamic> json) {
return User(
name: json['name'] as String,
age: json['age'] as int,
);
}
}
void main() {
var user = User.fromJson({'name': 'Ali', 'age': 22});
print('${user.name} is ${user.age}'); // Ali is 22
}
Inheritance
class Vehicle {
int speed = 0;
void accelerate() {
speed += 10;
}
}
class SportsCar extends Vehicle {
@override
void accelerate() {
speed += 30; // overrides the parent's behavior entirely
}
}
class Truck extends Vehicle {
@override
void accelerate() {
super.accelerate(); // calls Vehicle's accelerate() first...
speed -= 2; // ...then adjusts — trucks accelerate a bit slower
}
}
void main() {
var sportsCar = SportsCar()..accelerate();
print(sportsCar.speed); // 30
var truck = Truck()..accelerate();
print(truck.speed); // 8
}
Dart only supports single inheritance (extends one class), which is exactly the gap mixins fill.
Mixins
A mixin lets you add reusable behavior to a class without using (single) inheritance to do it — Dart's answer to the same problem Ruby's modules and Swift's protocol extensions solve:
mixin Flyable {
void fly() => print('Flying!');
}
mixin Swimmable {
void swim() => print('Swimming!');
}
class Duck with Flyable, Swimmable {}
void main() {
var duck = Duck();
duck.fly(); // Flying!
duck.swim(); // Swimming!
}
A class can with multiple mixins, gaining all of their methods, while still only being able to extends a single superclass. This gives you a form of multiple behavior composition that pure single inheritance can't.
Mixins vs. inheritance — when to use which
extends (inheritance) |
with (mixin) |
|
|---|---|---|
| Count allowed | One superclass only | Any number of mixins |
| Relationship modeled | "is-a" — a true specialization | "can-do" — shared, reusable capability |
| Constructors | Inherited from the superclass | Mixins cannot define constructors at all |
Reach for inheritance when a subclass is genuinely a more specific version of its parent. Reach for a mixin when you're sharing a capability (like "can fly" or "can be serialized") across otherwise-unrelated classes.
Common mistakes
- Overusing inheritance to share behavior between classes that aren't really in an "is-a" relationship — a mixin is usually the better fit for shared capabilities.
- Forgetting a mixin can't have its own constructor — if you need constructor logic, that behavior belongs in a regular class instead.
- Not marking overridden methods with
@override— it's optional in Dart, but catches signature mismatches (e.g., a typo, or a changed parent method signature) at compile time instead of silently creating an unrelated new method.
Interview questions
Q: What's the difference between a mixin and inheritance in Dart?
Inheritance (extends) models a true "is-a" relationship and a class can only extend one superclass. A mixin (with) shares reusable behavior across otherwise-unrelated classes, and a class can apply any number of mixins — but a mixin can't define its own constructor, unlike a superclass.
Q: What is a factory constructor, and when would you use one?
A factory constructor can return an existing instance instead of always creating a new one, or run arbitrary logic to decide what to construct — useful for singletons, object pools, or constructing an instance from parsed data (like User.fromJson). A regular constructor always creates a brand-new instance of exactly its own class; a factory constructor is not bound by that restriction.
Q: Why would you make a class's constructor private (e.g., Logger._internal()) and pair it with a factory constructor?
To control how instances of the class are created from outside — typically to enforce a singleton (always returning the one shared instance) or to force callers through a named/factory constructor that validates or transforms input, rather than allowing an uncontrolled, always-new instance via a plain public constructor.