Skip to content

Constructors

Understand how to create and use constructors in Dart classes.


What is it?

Constructors are special methods that are called when creating instances of a class. They initialize the object's state, set up fields, and can perform any necessary setup operations. Dart provides various types of constructors to handle different initialization scenarios.


Why does it exist?

Constructors exist to:

  • Initialize object state when created
  • Set up fields with initial values
  • Perform validation and setup logic
  • Control how objects are created
  • Enable different creation patterns
  • Support dependency injection

Generative Constructors

Basic Constructor

class Person {
  String name;
  int age;

  // Generative constructor
  Person(this.name, this.age);

  // Constructor with validation
  Person.withValidation(this.name, this.age) {
    if (age < 0) {
      throw ArgumentError('Age cannot be negative');
    }
    if (name.isEmpty) {
      throw ArgumentError('Name cannot be empty');
    }
  }
}

// Usage
var person1 = Person('Alice', 25);
var person2 = Person.withValidation('Bob', 30);

Constructor with Initializer List

class User {
  final String id;
  final String name;
  final DateTime createdAt;

  // Constructor with initializer list
  User(String id, String name)
      : id = id,
        name = name,
        createdAt = DateTime.now() {
    // Body runs after initialization
    if (id.isEmpty) {
      throw ArgumentError('ID cannot be empty');
    }
  }

  // Multiple initializers
  User.fromEmail(String email, String name)
      : id = email.hashCode.toString(),
        name = name,
        createdAt = DateTime.now() {
    if (!email.contains('@')) {
      throw ArgumentError('Invalid email');
    }
  }
}

// Usage
var user1 = User('123', 'Alice');
var user2 = User.fromEmail('alice@example.com', 'Alice');

Named Constructors

Multiple Constructors

class Rectangle {
  double width;
  double height;

  // Default constructor
  Rectangle(this.width, this.height);

  // Named constructor - square
  Rectangle.square(double size)
      : width = size,
        height = size;

  // Named constructor - from points
  Rectangle.fromPoints(double x1, double y1, double x2, double y2)
      : width = x2 - x1,
        height = y2 - y1 {
    if (width <= 0 || height <= 0) {
      throw ArgumentError('Invalid points');
    }
  }

  // Named constructor - from string
  Rectangle.fromString(String value) : this(0, 0) {
    var parts = value.split('x');
    if (parts.length == 2) {
      width = double.parse(parts[0]);
      height = double.parse(parts[1]);
    }
  }

  double get area => width * height;
}

// Usage
var rect1 = Rectangle(10, 20);
var rect2 = Rectangle.square(15);
var rect3 = Rectangle.fromPoints(0, 0, 10, 20);
var rect4 = Rectangle.fromString('10x20');

print(rect1.area); // 200
print(rect2.area); // 225
print(rect3.area); // 200
print(rect4.area); // 200

Redirecting Constructors

Constructor Redirection

class Point {
  final double x;
  final double y;

  // Main constructor
  Point(this.x, this.y);

  // Redirecting constructor to main
  Point.zero() : this(0, 0);

  // Redirecting to another named constructor
  Point.fromPolar(double radius, double angle)
      : this(radius * math.cos(angle), radius * math.sin(angle));

  // Multiple redirects
  Point.origin() : this.zero();

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

// Usage
var p1 = Point(3, 4);
var p2 = Point.zero(); // (0, 0)
var p3 = Point.origin(); // (0, 0)
var p4 = Point.fromPolar(5, 3.14); // (-4.99, 0.05)

Factory Constructors

Creating Factory Constructors

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

  // Factory constructor (may return existing instance)
  factory Logger(String name) {
    return _cache.putIfAbsent(name, () => Logger._internal(name));
  }

  // Private constructor
  Logger._internal(this.name);

  void log(String message) {
    print('[$name] $message');
  }
}

// Usage - Always returns same instance for same name
var logger1 = Logger('App');
var logger2 = Logger('App');
print(identical(logger1, logger2)); // true

var logger3 = Logger('Network');
print(identical(logger1, logger3)); // false

logger1.log('Starting app');
logger3.log('Sending request');

Factory Constructor with Validation

class Email {
  final String value;

  // Factory constructor with validation
  factory Email(String value) {
    if (!value.contains('@')) {
      throw ArgumentError('Invalid email: $value');
    }
    return Email._internal(value);
  }

  // Private constructor
  Email._internal(this.value);

  String get domain => value.split('@')[1];
  String get username => value.split('@')[0];

  @override
  String toString() => value;
}

// Usage
var email = Email('user@example.com');
print(email.username); // user
print(email.domain); // example.com

// This throws an error
// var invalid = Email('invalid-email');

Factory Constructor from JSON

class User {
  final String id;
  final String name;
  final String email;
  final DateTime createdAt;

  // Private constructor
  User._internal({
    required this.id,
    required this.name,
    required this.email,
    required this.createdAt,
  });

  // Factory constructor from JSON
  factory User.fromJson(Map<String, dynamic> json) {
    return User._internal(
      id: json['id'] as String,
      name: json['name'] as String,
      email: json['email'] as String,
      createdAt: DateTime.parse(json['createdAt'] as String),
    );
  }

  // Factory with default values
  factory User.create(String name, String email) {
    return User._internal(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      name: name,
      email: email,
      createdAt: DateTime.now(),
    );
  }

  Map<String, dynamic> toJson() => {
    'id': id,
    'name': name,
    'email': email,
    'createdAt': createdAt.toIso8601String(),
  };
}

// Usage
var user = User.fromJson({
  'id': '123',
  'name': 'Alice',
  'email': 'alice@example.com',
  'createdAt': '2024-01-01T00:00:00.000Z',
});

var newUser = User.create('Bob', 'bob@example.com');
print(newUser.name); // Bob

Constant Constructors

Creating Constant Objects

class ImmutablePoint {
  final double x;
  final double y;

  // Constant constructor
  const ImmutablePoint(this.x, this.y);

  // Constant named constructor
  const ImmutablePoint.zero() : this(0, 0);

  // Regular constructor (can't be const)
  ImmutablePoint.fromPolar(double radius, double angle)
      : this(radius * math.cos(angle), radius * math.sin(angle));
}

// Usage - All instances are compile-time constants
const p1 = ImmutablePoint(3, 4);
const p2 = ImmutablePoint.zero();

// Equality check
const p3 = ImmutablePoint(3, 4);
print(identical(p1, p3)); // true (same constant instance)

// Non-constant factory
var p4 = ImmutablePoint.fromPolar(5, 1.5);
print(p4); // Not a constant

Private Constructors

Singleton Pattern

class Singleton {
  // Private constructor
  Singleton._internal();

  // Static instance
  static final Singleton _instance = Singleton._internal();

  // Factory constructor returns the instance
  factory Singleton() {
    return _instance;
  }

  // Static getter
  static Singleton get instance => _instance;

  void doSomething() {
    print('Singleton doing something');
  }
}

// Usage - Only one instance
var singleton1 = Singleton();
var singleton2 = Singleton();
print(identical(singleton1, singleton2)); // true

var singleton3 = Singleton.instance;
print(identical(singleton1, singleton3)); // true

singleton1.doSomething();

Utility Class (All Static Members)

class MathUtils {
  // Private constructor prevents instantiation
  MathUtils._();

  static int add(int a, int b) => a + b;
  static int subtract(int a, int b) => a - b;
  static int multiply(int a, int b) => a * b;
  static double divide(int a, int b) => a / b;

  static const double pi = 3.14159;
  static const double e = 2.71828;
}

// Usage - No instance needed
print(MathUtils.add(5, 3)); // 8
print(MathUtils.pi); // 3.14159

// Error: Can't instantiate
// var utils = MathUtils(); // Error: Private constructor

Best Practices

Use Factory Constructors for Caching

// Good: Caching with factory
class DatabaseConnection {
  static final Map<String, DatabaseConnection> _connections = {};
  final String url;

  factory DatabaseConnection(String url) {
    return _connections.putIfAbsent(url, () => DatabaseConnection._internal(url));
  }

  DatabaseConnection._internal(this.url);
}

// Bad: Creating new connections each time
class BadDatabaseConnection {
  final String url;
  BadDatabaseConnection(this.url);
}

Validate in Constructors

// Good: Validation in constructor
class PositiveNumber {
  final int value;

  PositiveNumber(this.value) {
    if (value <= 0) {
      throw ArgumentError('Value must be positive');
    }
  }
}

// Bad: No validation
class UnvalidatedNumber {
  int value;
  UnvalidatedNumber(this.value);
}

Use Initializer List for Final Fields

// Good: Using initializer list
class Product {
  final String id;
  final String name;
  final double price;

  Product(String id, String name, double price)
      : id = id,
        name = name,
        price = price {
    if (price < 0) {
      throw ArgumentError('Price cannot be negative');
    }
  }
}

// Bad: Assigning final in body
class BadProduct {
  final String id;
  final String name;

  BadProduct(String id, String name) {
    this.id = id; // Error! Can't assign to final in body
    this.name = name; // Error! Can't assign to final in body
  }
}

Common Mistakes

Forgetting to Initialize Final Fields

Wrong:

class User {
  final String id;
  final String name;

  User(this.name) {
    // id not initialized!
  }
}

Correct:

class User {
  final String id;
  final String name;

  User(this.name, this.id);
  // Or
  User(this.name) : id = DateTime.now().millisecondsSinceEpoch.toString();
}

Using const with Non-constant Fields

Wrong:

class Point {
  final double x;
  final double y;

  const Point(this.x, this.y); // Error: Can't use const with double
}

Correct:

class Point {
  final double x;
  final double y;

  const Point(this.x, this.y); // Works with const if values are constants
}

const p1 = Point(1.5, 2.5); // Works

Summary

Constructors are essential for creating and initializing objects. Dart provides various constructor types for different use cases, including generative, named, redirecting, factory, and constant constructors.


Next Steps

Now that you understand constructors, continue to:


Did You Know?

  • Dart supports multiple named constructors
  • Factory constructors can return existing instances
  • Const constructors create compile-time constants
  • Redirecting constructors call another constructor
  • Initializer lists run before the constructor body
  • Private constructors prevent instantiation
  • Factory constructors don't use this