Skip to content

Dart Classes — Object-Oriented Programming Guide

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Dart Classes. We cover key concepts, practical examples, and best practices to help you master this topic.

Dart classes are blueprints for creating objects with fields, methods, constructors, getters, setters, and support for inheritance, mixins, and interfaces.

What You Will Learn

  • Declaring classes with fields and methods
  • Constructors: default, named, and factory
  • Initializer lists and redirecting constructors
  • Getters and setters for computed properties
  • Instance methods, operators, and static members
  • The this keyword and cascade notation
  • Immutable classes with final fields

Why It Matters

Object-oriented programming organizes code into classes that model real-world entities. Dart's class system is designed to be concise and safe. Named constructors eliminate the need for constructor overloading. Initializer lists ensure fields are set before the constructor body runs. Getters and setters provide encapsulation without boilerplate. Understanding classes is essential for Flutter, where every widget is a class and the widget tree is built from class instances.

Real-World Use

The DodaTech Flutter app defines a User class with named constructors for creating users from JSON, a Course class with computed properties for progress tracking, and a ThemeConfig class with static constants for the design system. All classes use final fields and immutable patterns to prevent unintended mutations.

Learning Path

flowchart LR
  A[Dart Collections] --> B[Dart Classes\nYou are here]
  B --> C[Dart Inheritance]
  style B fill:#f90,color:#fff

Basic Class Declaration

A class bundles fields and methods into a single unit:

class User {
  // Fields
  String name;
  int age;
  String email;

  // Constructor
  User(this.name, this.age, this.email);

  // Method
  void introduce() {
    print('Hi, I am $name, $age years old');
  }

  // Method with return value
  bool isAdult() => age >= 18;
}

void main() {
  var user = User('Alice', 30, 'alice@example.com');
  user.introduce();
  print('Is adult? ${user.isAdult()}');
  print('Email: ${user.email}');
}

Output:

Hi, I am Alice, 30 years old
Is adult? true
Email: alice@example.com

The constructor User(this.name, this.age, this.email) uses Dart's concise syntax to assign constructor parameters to fields with the same name. This eliminates boilerplate assignment code.

Named Constructors

Named constructors provide alternative ways to create objects:

class User {
  final String name;
  final int age;
  final String email;

  // Primary constructor
  User(this.name, this.age, this.email);

  // Named constructor for anonymous users
  User.anonymous()
      : name = 'Anonymous',
        age = 0,
        email = 'unknown@example.com';

  // Named constructor from JSON
  User.fromJson(Map<String, dynamic> json)
      : name = json['name'] as String,
        age = json['age'] as int,
        email = json['email'] as String;

  // Named constructor with default values
  User.minimal(this.name, {this.email = 'pending@example.com'})
      : age = 18;
}

void main() {
  var alice = User('Alice', 30, 'alice@test.com');
  var anonymous = User.anonymous();
  var fromJson = User.fromJson({'name': 'Bob', 'age': 25, 'email': 'bob@test.com'});
  var minimal = User.minimal('Charlie');

  print('${anonymous.name}, ${fromJson.name}, ${minimal.name}');
}

Output:

Anonymous, Bob, Charlie

Named constructors are called with ClassName.constructorName(). They are especially useful for deserialization (fromJson) and providing sensible defaults.

Initializer List

The initializer list runs before the constructor body and can initialize fields, assert conditions, and redirect to other constructors:

class Rectangle {
  final double width;
  final double height;
  final double area;

  // Initializer list computes area before body runs
  Rectangle(this.width, this.height)
      : area = width * height,
        assert(width > 0, 'Width must be positive'),
        assert(height > 0, 'Height must be positive');

  // Redirecting constructor
  Rectangle.square(double side) : this(side, side);
}

void main() {
  var rect = Rectangle(5, 3);
  print('Area: ${rect.area}');

  var square = Rectangle.square(4);
  print('Square area: ${square.area}');

  // This would throw an assertion error:
  // var bad = Rectangle(-1, 5);
}

Output:

Area: 15
Square area: 16

The initializer list is separated from the constructor parameters by :. Multiple initializers are separated by commas. Assertions in the initializer list catch invalid arguments immediately.

Factory Constructors

Factory constructors do not always create a new instance. They can return an existing instance or a subtype:

class Logger {
  static final Map<String, Logger> _cache = {};

  final String name;
  final DateTime createdAt;

  // Private constructor (starts with underscore)
  Logger._internal(this.name) : createdAt = DateTime.now();

  // Factory constructor returns cached or creates new
  factory Logger(String name) {
    return _cache.putIfAbsent(name, () => Logger._internal(name));
  }
}

void main() {
  var log1 = Logger('api');
  var log2 = Logger('api');
  var log3 = Logger('auth');

  print('log1 == log2: ${identical(log1, log2)}'); // Same instance
  print('log1 == log3: ${identical(log1, log3)}'); // Different instance
  print('log1 created: ${log1.createdAt}');
  print('log2 created: ${log2.createdAt}'); // Same timestamp as log1
}

Output:

log1 == log2: true
log1 == log3: false
log1 created: 2026-06-28 ...
log2 created: 2026-06-28 ...

Factory constructors use the factory keyword. They have access to the class but must return an instance of the class or a subtype. Common uses include caching, Singleton Patternton" >}} pattern, and returning subtypes based on input.

Getters and Setters

Getters and setters provide computed properties with the same syntax as field access:

class Temperature {
  double _celsius;

  Temperature(this._celsius);

  // Getter
  double get celsius => _celsius;

  // Setter with validation
  set celsius(double value) {
    if (value < -273.15) {
      throw ArgumentError('Temperature cannot be below absolute zero');
    }
    _celsius = value;
  }

  // Computed getter
  double get fahrenheit => (_celsius * 9 / 5) + 32;

  // Computed setter
  set fahrenheit(double value) {
    _celsius = (value - 32) * 5 / 9;
  }
}

void main() {
  var temp = Temperature(25);
  print('Celsius: ${temp.celsius}');
  print('Fahrenheit: ${temp.fahrenheit}');

  temp.fahrenheit = 100;
  print('After setting Fahrenheit:');
  print('Celsius: ${temp.celsius}');
  print('Fahrenheit: ${temp.fahrenheit}');
}

Output:

Celsius: 25
Fahrenheit: 77
After setting Fahrenheit:
Celsius: 37.77777777777778
Fahrenheit: 100

Getters and setters look like field access but execute code. They provide encapsulation without breaking the API if the internal implementation changes.

Methods and Operators

Classes can define instance methods, static methods, and operator overloads:

class Vector {
  final double x;
  final double y;

  Vector(this.x, this.y);

  // Instance method
  double magnitude() => (x * x + y * y);

  // Operator overload
  Vector operator +(Vector other) => Vector(x + other.x, y + other.y);

  Vector operator *(double scalar) => Vector(x * scalar, y * scalar);

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

  // Static method
  static Vector zero() => Vector(0, 0);
}

void main() {
  var v1 = Vector(3, 4);
  var v2 = Vector(1, 2);

  print('v1 = $v1');
  print('v1 magnitude: ${v1.magnitude()}');
  print('v1 + v2 = ${v1 + v2}');
  print('v1 * 2 = ${v1 * 2}');
  print('Zero vector: ${Vector.zero()}');
}

Output:

v1 = Vector(3, 4)
v1 magnitude: 25
v1 + v2 = Vector(4, 6)
v1 * 2 = Vector(6, 8)
Zero vector: Vector(0, 0)

Operators are defined using the operator keyword. Common operator overloads include +, -, *, /, [], ==, and <. Static methods are called on the class itself, not on instances.

Cascade Notation

The cascade operator .. lets you perform multiple operations on the same object:

class StringBuilder {
  String _buffer = '';

  void append(String text) => _buffer += text;
  void appendLine(String text) => _buffer += '$text\n';
  void clear() => _buffer = '';
  String build() => _buffer;
}

void main() {
  var result = StringBuilder()
    ..append('Hello')
    ..appendLine('World')
    ..append('From Dart')
    ..build();

  print(result);
}

Output:

HelloWorld
From Dart

The cascade .. returns the original object, not the result of the method call. This enables fluent interfaces without returning this from every method.

Immutable Classes

Immutable classes use final fields and const constructors. Once created, the object state cannot change:

class Point {
  final double x;
  final double y;

  // Const constructor enables compile-time constant instances
  const Point(this.x, this.y);

  @override
  bool operator ==(Object other) {
    if (other is! Point) return false;
    return x == other.x && y == other.y;
  }

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

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

void main() {
  const origin = Point(0, 0);
  const point1 = Point(3, 4);
  const point2 = Point(3, 4);

  print(origin);
  print('point1 == point2: ${point1 == point2}');
  print('Same instance: ${identical(point1, point2)}');
}

Output:

Point(0, 0)
point1 == point2: true
Same instance: true

With const constructors and final fields, the object is deeply immutable. Constant instances with the same values are canonicalized (identical returns true for equal const objects).

Common Mistakes

  1. Not initializing non-nullable fields: All non-nullable fields must be initialized either at declaration, in the constructor, or in the initializer list. The compiler enforces this.

  2. Using new keyword unnecessarily: Dart 2 made new optional. Use User('Alice', 30) instead of new User('Alice', 30).

  3. Forgetting @override when overriding methods: Dart's @override annotation is optional but recommended. Without it, the compiler still checks the override but does not warn if the parent method changes.

  4. Using == for string comparison in operators: The == operator on Object checks reference equality. Override == and hashCode together for value equality.

  5. Making classes mutable when immutable suffices: Use final fields and const constructors by default. Immutable objects are easier to reason about and prevent bugs from unintended mutations.

Practice Questions

  1. What is the difference between a named constructor and a factory constructor?
  2. When does the initializer list execute relative to the constructor body?
  3. How do getters and setters differ from regular methods?
  4. Why should you override hashCode when overriding ==?
  5. Challenge: Implement a BankAccount class with deposit and withdraw methods, a read-only balance property, Transaction history, and a named constructor BankAccount.savings() that sets a higher interest rate.

Mini Project

Build a library management system with classes:

  • Book class with title, author, ISBN (immutable)
  • Patron class with name, ID, borrowed books list
  • Library class with collections of books and patrons
  • Methods: addBook, borrowBook, returnBook, findByAuthor
  • A checkAvailability method using a Set for quick lookup
  • Factory constructor for Library that loads from a hardcoded JSON list

FAQ

Can a Dart class have multiple constructors?

Yes. Use named constructors for different construction patterns. Dart does not support constructor overloading by parameter types, but named constructors serve the same purpose.

What is the difference between `const` and `final` fields?

A final field is set once (in the constructor). A const field must be set to a compile-time constant. A class with all final fields can have a const constructor.

How do I make a class abstract?

Use the abstract keyword before class. Abstract classes cannot be instantiated and may contain abstract methods (methods without a body).

Can I define a class inside a function?

Yes. Dart supports local classes (classes defined inside functions). They have access to the enclosing scope and can be useful for small data holders.

What is the `late` modifier for fields?

late defers initialization. Use it for non-nullable fields that cannot be initialized at declaration but will be set before first use. Example: late final String name;.

What is Next

Now that you understand classes, learn how to reuse code through inheritance. Proceed to Dart Inheritance for subclassing, method overriding, and polymorphism. Then explore Dart Interfaces for abstract classes and implicit interfaces.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro