Skip to content

06 · Classes & Objects Basics

Defining a class

class Person {
  String name;
  int age;

  // Constructor
  Person(this.name, this.age);

  void introduce() {
    print("Hi, I'm $name and I'm $age years old.");
  }
}

void main() {
  var alice = Person('Alice', 30);
  alice.introduce();
  // Hi, I'm Alice and I'm 30 years old.
}

Person(this.name, this.age) is Dart's shorthand for "assign this constructor argument directly to the field of the same name" — no need to write this.name = name; by hand in the constructor body.

Named constructors

A class can have multiple constructors as long as only one is unnamed.

class Point {
  double x, y;

  Point(this.x, this.y);

  // Named constructor -- an alternative way to build a Point
  Point.origin() : x = 0, y = 0;

  Point.fromList(List<double> coords) : x = coords[0], y = coords[1];

  @override
  String toString() => 'Point($x, $y)';
}

void main() {
  var p1 = Point(3, 4);
  var p2 = Point.origin();
  var p3 = Point.fromList([5, 6]);

  print(p1);   // Point(3.0, 4.0)
  print(p2);   // Point(0.0, 0.0)
  print(p3);   // Point(5.0, 6.0)
}

Getters and setters

class Rectangle {
  double width;
  double height;

  Rectangle(this.width, this.height);

  // Computed getter -- looks like a field from the outside, but computed on access
  double get area => width * height;

  // Setter -- lets you write "rect.doubleWidth = true" style API if useful
  set scaleWidth(double factor) {
    width *= factor;
  }
}

void main() {
  var rect = Rectangle(4, 5);
  print(rect.area);   // 20.0

  rect.scaleWidth = 2;
  print(rect.area);   // 40.0
}

Private members

Dart has no private keyword — instead, any identifier starting with an underscore _ is private to its own library (usually meaning its own file).

class BankAccount {
  double _balance;   // private -- not accessible outside this file

  BankAccount(this._balance);

  double get balance => _balance;

  void deposit(double amount) {
    if (amount <= 0) throw ArgumentError('Deposit must be positive');
    _balance += amount;
  }

  void withdraw(double amount) {
    if (amount > _balance) throw StateError('Insufficient funds');
    _balance -= amount;
  }
}

void main() {
  var account = BankAccount(100);
  account.deposit(50);
  account.withdraw(30);
  print(account.balance);   // 120.0
  // print(account._balance);   // fine within this same file, but would fail from another file
}

Constructors with validation (initializer lists and asserts)

class Temperature {
  final double celsius;

  Temperature(this.celsius) : assert(celsius >= -273.15, 'Below absolute zero');

  double get fahrenheit => celsius * 9 / 5 + 32;
}

void main() {
  var temp = Temperature(25);
  print(temp.fahrenheit);   // 77.0
}

Static members

static fields and methods belong to the class itself, not to any instance.

class MathUtils {
  static const double pi = 3.14159;

  static double circleArea(double radius) => pi * radius * radius;
}

void main() {
  print(MathUtils.pi);                 // 3.14159
  print(MathUtils.circleArea(3));      // 28.27431
}

Overriding toString, ==, and hashCode

class Point2 {
  final int x, y;
  Point2(this.x, this.y);

  @override
  String toString() => 'Point2($x, $y)';

  @override
  bool operator ==(Object other) =>
      other is Point2 && other.x == x && other.y == y;

  @override
  int get hashCode => Object.hash(x, y);
}

void main() {
  var a = Point2(1, 2);
  var b = Point2(1, 2);

  print(a);              // Point2(1, 2)
  print(a == b);         // true -- value equality, thanks to our override
  print(identical(a, b)); // false -- still two distinct objects in memory
}

Cheat sheet

Feature Syntax
Constructor shorthand Person(this.name, this.age);
Named constructor Person.guest() : name = 'Guest', age = 0;
Getter double get area => width * height;
Setter set scaleWidth(double f) { width *= f; }
Private member Prefix with _, e.g. _balance
Static member static const pi = 3.14159;

How It Actually Works

Every Dart object carries a hidden pointer to its class's class descriptor (similar in spirit to a C++ vtable), which the runtime uses to resolve method calls, field offsets, and type checks (is/as). When you call instance.method(), the compiler (JIT or AOT) generates code that follows that pointer to look up method's compiled entry — for a non-overridden, final, or statically-known-concrete-type call, this can often be resolved and inlined directly at compile time; for a call through an interface/overridable method, it stays a genuine virtual dispatch resolved via the descriptor at call time.

Named constructors (Point.origin()) are not separate classes or factories under the hood — they're just alternate entry points into the same allocation routine, all producing instances of the same class layout, which is why Point.origin() is Point is always true.

static members are the one place where Dart deliberately does not allocate per-instance: a static field lives once, in the isolate's static memory associated with the class, initialized lazily the first time it (or any static member of that class) is touched — not eagerly at isolate startup. That lazy-initialization semantics is why a static final field that reads an environment or does expensive setup only pays that cost if the program actually uses it.

Overriding == and hashCode changes how the object behaves inside every hash-based collection: the default Object.==/hashCode (identity-based) are backed by an internal object identity value baked in at allocation time. Once you override them to compare fields, Set/Map lookups start hashing and comparing your fields instead of the object's identity — which is exactly what makes value-equality classes usable as map keys, but also means mutating a field used in hashCode after inserting the object into a Set/Map corrupts that collection's internal bucket structure (the object is now in the wrong bucket for its new hash).

🔀 See this in another language

Exercise

Design a Book class with private fields _title, _author, and _pages, a standard constructor, a named constructor Book.unknownAuthor(title, pages) that sets author to "Unknown", a getter summary that returns a formatted one-line description, and an override of toString(). Create a few Book instances (some via each constructor) and print their summaries in a loop.