| |

Dart 18 🎯 Classes and Objects: Constructors (Generative, Named, Redirecting)

Classes are the blueprint for objects. A class declares the fields and methods that every instance of the class shares, and the constructor is the mechanism that creates an instance and initializes its fields. Dart’s constructor system is more expressive than most: it distinguishes between generative, named, and redirecting constructors; it provides field initializers, initializer lists, and const constructors; and it handles the this reference, superclass initialization, and factory constructors. Understanding the constructor is understanding how Dart objects come into existence.

The three constructor forms have distinct roles. A generative constructor is the default: it creates a new instance and initializes its fields. A named constructor gives the class multiple ways to be constructed, each with its own name. A redirecting constructor forwards to another constructor, which is used to provide a shorter name or a different parameter shape without duplicating the initialization. Beyond these, factory constructors are a related concept: they do not have to return a new instance, so they can return a cached instance, a subtype, or an instance created by another class. Factory constructors are not part of the three generative forms, but they belong to the same topic.

This chapter covers the class declaration, fields, generative constructors, the field initializer shorthand, named constructors, redirecting constructors, initializer lists, const constructors, and the patterns that make constructors readable and correct.

Key point: A generative constructor creates a new instance and initializes its fields. A named constructor provides an additional way to construct the class. A redirecting constructor forwards to another constructor. Field initializers use this.field in the parameter list to assign directly. Initializer lists run before the constructor body and are used for validation or for computing values. A const constructor allows the instance to be a compile-time constant.


Why Dart’s constructors are expressive

The multiple-ways-to-construct problem. A class sometimes has several valid ways to be constructed: from a map, from a JSON string, from another instance, or with default values. The named constructor gives each way its own name, so the call site is self-documenting: Point.fromJson(...) is clearer than Point(...) with a flag.

The initialization-order problem. Fields must be initialized before the constructor body runs. The initializer list is the mechanism that runs before the body, and the this.field parameter shorthand is the mechanism that assigns fields directly from parameters. Understanding the order prevents the common errors of using a field before it is initialized.

The forwarding problem. Some constructors are a shorthand for another. A Point.origin() constructor that returns Point(0, 0) is a redirecting constructor: it forwards to the main constructor without duplicating the initialization. This keeps the class’s construction logic in one place.

The constant problem. A const constructor allows instances to be compile-time constants. This is what makes const Point(0, 0) a constant expression, and it is the requirement for using a class as a key in a const map or as a value in a const list.

The validation problem. Some classes must validate their inputs before creating an instance. The initializer list and the constructor body are the two places where validation runs, and the assert statement is the mechanism. The choice of where to validate depends on whether the validation needs the fields to be assigned first.


a. The class and its fields

A class is declared with the class keyword, and its fields are declared with a type and a name.

class Point {
  final double x;
  final double y;
}

The fields are the state that every instance holds. The final keyword means the field cannot be changed after it is initialized, which is the recommended default for value classes.

Fields can have default values, but a final field cannot be assigned a default in the declaration and later overridden by a constructor. The default is the value.

class Config {
  final int retries = 3;  // default value
}

Non-final fields can be declared without an initializer and assigned in the constructor:

class Counter {
  int count = 0;
}

The late keyword defers the initialization of a non-nullable field to the constructor or to the first access:

class Service {
  late final String config;
}

b. Generative constructors

A generative constructor has the same name as the class and creates a new instance. Its parameters initialize the fields.

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);
}

The this.x and this.y in the parameter list are field initializers. They assign the parameter to the field of the same name. This shorthand is the idiomatic way to write a simple constructor.

The expanded form is equivalent:

Point(double x, double y)
    : x = x,
      y = y;

The : introduces the initializer list, which runs before the constructor body. The list assigns the fields from the parameters. The shorthand this.x is a convenience for this pattern.

A constructor can have a body, which runs after the initializer list:

Point(this.x, this.y) {
  print('Created a point at ($x, $y)');
}

The body is used for side effects, not for field initialization. Fields must be initialized before the body runs.


c. Named constructors

A named constructor has a name after the class name, separated by a dot. It creates a new instance but with a different name.

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);
  Point.origin() : x = 0, y = 0;
  Point.fromJson(Map<String, dynamic> json)
      : x = json['x'] as double,
        y = json['y'] as double;
}

The Point.origin() constructor takes no parameters and initializes both fields to zero. The Point.fromJson() constructor takes a map and extracts the values.

Each named constructor has its own parameter list and initializer list. The call site uses the name:

var p1 = Point(3, 4);
var p2 = Point.origin();
var p3 = Point.fromJson({'x': 1.0, 'y': 2.0});

The named constructors make the class’s different construction paths explicit. A reader can see that Point.fromJson parses JSON and Point.origin creates the zero point, without reading the implementation.

A named constructor can use the field initializer shorthand:

Point.origin() : this(0, 0);

This is a redirecting constructor, covered next.


d. Redirecting constructors

A redirecting constructor forwards to another constructor. It does not have a body or an initializer list of its own; instead, it calls this(...) with the arguments for the target constructor.

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);
  Point.origin() : this(0, 0);
}

The Point.origin() redirects to the main constructor with the arguments 0, 0. It does not duplicate the field initialization.

A redirecting constructor can also redirect to a named constructor:

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);
  Point.origin() : this(0, 0);
  Point.unit() : this(1, 1);
}

The redirecting constructors are the shortest way to provide alternative names for the same construction logic. The initialization is in one place, and the alternative names forward to it.

A redirecting constructor cannot have a body or an initializer list. Its only statement is the this(...) call.


e. Initializer lists

The initializer list runs before the constructor body and after the fields are assigned from the parameters. It is used for validation, for computing values that depend on the parameters, and for calling the superclass constructor.

class Point {
  final double x;
  final double y;

  Point(this.x, this.y)
      : assert(x >= 0),
        assert(y >= 0);
}

The assert statements run in the initializer list. They validate the parameters before the instance is fully constructed. In production, asserts are disabled, so the validation is a development aid.

The initializer list can compute a field from the parameters:

class Circle {
  final double radius;
  final double area;

  Circle(this.radius)
      : assert(radius > 0),
        area = 3.14159 * radius * radius;
}

The area field is computed from radius in the initializer list. The order matters: radius is assigned first (through the this.radius shorthand), and then area is computed from it.

The initializer list also calls the superclass constructor:

class Animal {
  final String name;
  Animal(this.name);
}

class Dog extends Animal {
  final String breed;
  Dog(String name, this.breed) : super(name);
}

The super(name) call is in the initializer list. It runs the superclass constructor before the subclass’s fields are assigned. If the superclass has a named constructor, the call is super.name(...).


f. Const constructors

A const constructor creates compile-time constants. The instance is created at compile time, and every field must be final.

class Point {
  final double x;
  final double y;

  const Point(this.x, this.y);
}

const origin = Point(0, 0);
const points = [Point(1, 1), Point(2, 2)];

The const keyword before the constructor name marks it as a constant constructor. The const keyword before the variable makes the instance a compile-time constant. The Point(0, 0) is evaluated at compile time, and the instance is stored in the program’s read-only data.

A const constructor requires that all fields are final and that the initializer list contains only constant expressions. It cannot have a body with statements, and it cannot call a non-const superclass constructor.

The const constructor is what allows a class to be used as a value in a const context. It is the reason const Point(0, 0) is a valid constant expression and Point(0, 0) (without const) is not.

A class with a const constructor can also be instantiated without const, in which case the instance is created at runtime:

var p = Point(1, 2);        // runtime instance
const q = Point(1, 2);      // compile-time constant

The const instance is canonicalized: two const Point(1, 2) expressions refer to the same instance.


Complete Example Session

// ============================================
// PART 1: CLASS WITH FINAL FIELDS
// ============================================
class Point {
  final double x;
  final double y;
}

void main() {
  // Cannot instantiate without a constructor
}
// ============================================
// PART 2: GENERATIVE CONSTRUCTOR
// ============================================
class Point {
  final double x;
  final double y;

  Point(this.x, this.y);
}

void main() {
  var p = Point(3, 4);
  print('${p.x}, ${p.y}');  // 3.0, 4.0
}
// ============================================
// PART 3: CONSTRUCTOR WITH BODY
// ============================================
class Point {
  final double x;
  final double y;

  Point(this.x, this.y) {
    print('Created point at ($x, $y)');
  }
}
// ============================================
// PART 4: NAMED CONSTRUCTOR
// ============================================
class Point {
  final double x;
  final double y;

  Point(this.x, this.y);
  Point.origin() : x = 0, y = 0;
}

void main() {
  var origin = Point.origin();
  print('${origin.x}, ${origin.y}');  // 0.0, 0.0
}
// ============================================
// PART 5: NAMED CONSTRUCTOR FROM JSON
// ============================================
class Point {
  final double x;
  final double y;

  Point(this.x, this.y);
  Point.fromJson(Map<String, dynamic> json)
      : x = json['x'] as double,
        y = json['y'] as double;
}

void main() {
  var p = Point.fromJson({'x': 1.0, 'y': 2.0});
  print('${p.x}, ${p.y}');  // 1.0, 2.0
}
// ============================================
// PART 6: REDIRECTING CONSTRUCTOR
// ============================================
class Point {
  final double x;
  final double y;

  Point(this.x, this.y);
  Point.origin() : this(0, 0);
  Point.unit() : this(1, 1);
}
// ============================================
// PART 7: INITIALIZER LIST WITH ASSERT
// ============================================
class Point {
  final double x;
  final double y;

  Point(this.x, this.y)
      : assert(x >= 0),
        assert(y >= 0);
}
// ============================================
// PART 8: COMPUTED FIELD
// ============================================
class Circle {
  final double radius;
  final double area;

  Circle(this.radius)
      : assert(radius > 0),
        area = 3.14159 * radius * radius;
}

void main() {
  var c = Circle(2);
  print(c.area);  // 12.56636
}
// ============================================
// PART 9: SUPERCLASS INITIALIZATION
// ============================================
class Animal {
  final String name;
  Animal(this.name);
}

class Dog extends Animal {
  final String breed;
  Dog(String name, this.breed) : super(name);
}

void main() {
  var d = Dog('Rex', 'Labrador');
  print('${d.name}, ${d.breed}');  // Rex, Labrador
}
// ============================================
// PART 10: CONST CONSTRUCTOR
// ============================================
class Point {
  final double x;
  final double y;

  const Point(this.x, this.y);
}

void main() {
  const origin = Point(0, 0);
  const points = [Point(1, 1), Point(2, 2)];
  print(origin.x);       // 0.0
  print(points.length);  // 2
}

These ten parts cover a class with final fields, a generative constructor, a constructor with a body, a named constructor, a named constructor from JSON, a redirecting constructor, an initializer list with asserts, a computed field, superclass initialization, and a const constructor.


Quick Reference

Constructor Forms

FormSyntaxPurpose
GenerativePoint(this.x, this.y)Create an instance
NamedPoint.origin() : x = 0, y = 0Alternative construction
RedirectingPoint.origin() : this(0, 0)Forward to another
Constconst Point(this.x, this.y)Compile-time constant
Factoryfactory Point.fromJson(...)Return cached or subtype

Field Initializers

FormExample
ShorthandPoint(this.x, this.y)
ExpandedPoint(double x) : x = x
ComputedCircle(this.r) : area = 3.14 * r * r

Initializer List

PurposeExample
Assign field: x = 0
Compute field: area = pi * r * r
Assert: assert(x >= 0)
Call super: super(name)

Named Constructor Patterns

PatternExample
Zero valuePoint.origin() : this(0, 0)
From JSONPoint.fromJson(json)
From anotherPoint.copy(other) : this(other.x, other.y)
With defaultPoint.withDefault() : this(1, 1)

Const Requirements

RequirementDetail
All fields finalYes
Initializer list constantYes
No body statementsYes
Const superclassIf extending

Best Practices

✅ Do This:

// Use the field initializer shorthand
Point(this.x, this.y);

// Use named constructors for alternative construction
Point.fromJson(Map<String, dynamic> json) : x = ..., y = ...;

// Use redirecting constructors for shorthands
Point.origin() : this(0, 0);

// Use initializer lists for validation
Point(this.x, this.y) : assert(x >= 0), assert(y >= 0);

// Use const constructors for immutable value classes
const Point(this.x, this.y);

❌ Don’t Do This:

// Assign fields in the constructor body
Point(double x, double y) { this.x = x; }  // ❌ final fields

// Duplicate initialization in each named constructor
Point.origin() : x = 0, y = 0;  // ⚠️ redirect instead

// Give a redirecting constructor a body
Point.origin() : this(0, 0) { }  // ❌

// Use non-constant values in a const constructor
const Point(this.x) : y = compute();  // ❌

Common Pitfalls

PitfallWhy It HappensFix
Final field not initializedNo initializerAdd a constructor that assigns it
Field used before assignedInitializer orderAssign in the initializer list first
Redirecting with a bodyNot allowedRemove the body
Const constructor with non-finalField not finalMake all fields final
Super not calledMissing super(...)Add it to the initializer list
Assert not runningProduction modeAsserts are development-only

Real-World Examples

1. Simple Value Class

class Point {
  final double x;
  final double y;
  Point(this.x, this.y);
}

2. Named Constructor

Point.origin() : this(0, 0);

3. From JSON

Point.fromJson(Map<String, dynamic> json)
    : x = json['x'] as double, y = json['y'] as double;

4. Validation

Circle(this.radius) : assert(radius > 0);

5. Computed Field

Circle(this.r) : area = 3.14 * r * r;

6. Super Call

Dog(String name, this.breed) : super(name);

7. Const Constructor

const Point(this.x, this.y);

8. Copy Constructor

Point.copy(Point other) : this(other.x, other.y);

9. Default Values

Config({this.retries = 3, this.timeout = 30});

10. Named with Named Super

Dog.small(String name) : super.named(name);

Visual

Constructor Types

┌──────────────────────────────────────────────────────────────┐
│  GENERATIVE:                                                 │
│  Point(this.x, this.y);                                      │
│  └── Creates an instance, initializes fields                 │
│                                                              │
│  NAMED:                                                      │
│  Point.origin() : x = 0, y = 0;                              │
│  └── Alternative construction with its own initialization    │
│                                                              │
│  REDIRECTING:                                                │
│  Point.origin() : this(0, 0);                                │
│  └── Forwards to another constructor                         │
│                                                              │
│  FACTORY:                                                    │
│  factory Point.fromJson(...) { return ...; }                 │
│  └── Returns an instance, may be cached or a subtype         │
└──────────────────────────────────────────────────────────────┘

Initialization Order

┌──────────────────────────────────────────────────────────────┐
│  1. Initializer list runs                                    │
│     ├── Field assignments                                    │
│     ├── Assertions                                           │
│     └── Super constructor call                               │
│                                                              │
│  2. Superclass constructor body                              │
│                                                              │
│  3. Subclass constructor body                                │
│                                                              │
│  The fields are assigned before the body runs.               │
└──────────────────────────────────────────────────────────────┘

Redirecting vs Named

┌──────────────────────────────────────────────────────────────┐
│  NAMED (duplicates initialization):                          │
│  Point.origin() : x = 0, y = 0;                              │
│                                                              │
│  REDIRECTING (forwards):                                     │
│  Point.origin() : this(0, 0);                                │
│  └── The initialization is in the main constructor           │
└──────────────────────────────────────────────────────────────┘

Const Canonicalization

┌──────────────────────────────────────────────────────────────┐
│  const a = Point(1, 2);                                      │
│  const b = Point(1, 2);                                      │
│                                                              │
│  a == b is true.                                             │
│  a is identical to b — the same instance.                    │
│                                                              │
│  var c = Point(1, 2);  // runtime instance, different        │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Generative constructorPoint(this.x, this.y)
Named constructorPoint.origin() : this(0, 0)
Redirecting constructorForwards with this(...)
Field initializer shorthandthis.field in the parameter
Initializer list: field = value, assert(...)
Super call: super(...)
Const constructorconst Point(this.x, this.y)
Const requirementsAll fields final, constant expressions
Initialization orderInitializer list, super, body
Factory constructorReturns an instance, may be cached

Key takeaways:

  • A generative constructor creates a new instance. It has the same name as the class and initializes the fields through the parameter list or the initializer list.
  • The this.field shorthand assigns a parameter to a field. It is the idiomatic way to write a simple constructor, and it is equivalent to the expanded form with an initializer list.
  • A named constructor gives the class multiple construction paths. Point.origin() and Point.fromJson() are examples. Each has its own parameters and its own initialization.
  • A redirecting constructor forwards to another. It does not duplicate the initialization; it calls this(...) with the arguments for the target constructor. It cannot have a body or an initializer list of its own.
  • The initializer list runs before the constructor body. It is used for field assignments, computed values, assertions, and the super constructor call. The fields are assigned before the body runs.
  • A const constructor creates compile-time constants. All fields must be final, and the initializer list must contain only constant expressions. The const instance is canonicalized, so two identical const expressions refer to the same instance.
  • Factory constructors are a related concept. They do not create a new instance directly; they return one, which may be cached, a subtype, or an instance created by another class.

Remember: Dart’s constructor system is more expressive than most languages. The generative constructor creates the instance, the named constructor provides alternative paths, and the redirecting constructor forwards to another. The field initializer shorthand and the initializer list are the mechanisms for assigning and computing fields, and the const constructor allows immutable value classes to be compile-time constants. Understanding the initialization order — initializer list, super, body — and the distinction between generative, named, and redirecting constructors is understanding how Dart objects come into existence.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!