OOP in Dart

Classes, named and factory constructors, mixins, and inheritance in Dart.

Classes

Dart
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:

Dart
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:

Dart
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:

Dart
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

Dart
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:

Dart
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.