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.

New writing, now and then

Essays on Flutter, Dart and the decisions behind real systems. No schedule, no filler.

One click to unsubscribe, in every email.