Skip to content
← writing

February 5, 2025 · 3 min read

OOP in Dart: Classes and Objects

Classes and objects are the foundation of everything in Dart. Build the concept from the ground up — fields, constructors, getters, static members, and const.

Object-oriented programming is the backbone of Dart. Everything — even an int — is an object, and understanding classes and objects is the prerequisite for everything else: state management, models, architecture. Let's build the concept from the ground up.

Classes and objects

A class is a blueprint. An object (or instance) is a concrete thing built from that blueprint. The class Dog describes what every dog has and can do; myDog is one specific dog.

class Dog {
  String name = '';
  int age = 0;

  void bark() => print('$name says woof!');
}

void main() {
  final myDog = Dog();   // create an instance
  myDog.name = 'Rex';
  myDog.age = 3;
  myDog.bark();          // Rex says woof!
}

Constructors

Setting fields one by one is tedious. A constructor builds an object in a valid state. Dart's shorthand assigns parameters straight to fields.

class Dog {
  Dog(this.name, this.age); // shorthand: assigns to fields

  final String name;
  final int age;

  void bark() => print('$name says woof!');
}

final rex = Dog('Rex', 3);

Named parameters and defaults

Named parameters make call sites self-documenting. Mark them required when they must be provided.

class User {
  User({required this.email, this.isAdmin = false});

  final String email;
  final bool isAdmin;
}

final u = User(email: 'a@b.com');
final admin = User(email: 'root@b.com', isAdmin: true);

Named constructors

A class can offer multiple ways to be built.

class Point {
  Point(this.x, this.y);
  Point.origin() : x = 0, y = 0;      // named constructor
  Point.fromMap(Map<String, num> m)   // build from data
      : x = m['x']!.toDouble(),
        y = m['y']!.toDouble();

  final double x;
  final double y;
}

final a = Point(3, 4);
final b = Point.origin();

Fields, getters, and setters

Beyond plain fields, you can expose computed values with getters and validate writes with setters.

class Circle {
  Circle(this.radius);
  double radius;

  double get area => 3.14159 * radius * radius; // computed, read-only

  set diameter(double d) => radius = d / 2;     // validated write
}

final c = Circle(2);
print(c.area);       // 12.566...
c.diameter = 10;     // sets radius to 5

Instance vs. static members

Instance members belong to each object. Static members belong to the class itself — shared across all instances.

class Counter {
  static int instances = 0; // shared
  Counter() { instances++; }

  static Counter create() => Counter(); // factory-style helper
}

Counter();
Counter();
print(Counter.instances); // 2

Useful overrides: toString

Override toString so your objects print meaningfully — invaluable when debugging.

class Money {
  const Money(this.amount, this.currency);
  final double amount;
  final String currency;

  @override
  String toString() => '$amount $currency';
}

print(Money(9.99, 'USD')); // 9.99 USD

const constructors

If every field is final and known at compile time, a const constructor lets Dart canonicalize identical instances — the same object is reused, which is a real performance win in Flutter.

class Color {
  const Color(this.hex);
  final int hex;
}

const red = Color(0xFF0000);
const alsoRed = Color(0xFF0000);
print(identical(red, alsoRed)); // true — same instance

The takeaway

A class bundles data (fields) with behavior (methods) and guarantees a valid initial state through its constructor. Master this and you're ready for the four pillars of OOP — inheritance, encapsulation, abstraction, and polymorphism — which are all just ways of relating classes to one another.