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
| Form | Syntax | Purpose |
|---|---|---|
| Generative | Point(this.x, this.y) | Create an instance |
| Named | Point.origin() : x = 0, y = 0 | Alternative construction |
| Redirecting | Point.origin() : this(0, 0) | Forward to another |
| Const | const Point(this.x, this.y) | Compile-time constant |
| Factory | factory Point.fromJson(...) | Return cached or subtype |
Field Initializers
| Form | Example |
|---|---|
| Shorthand | Point(this.x, this.y) |
| Expanded | Point(double x) : x = x |
| Computed | Circle(this.r) : area = 3.14 * r * r |
Initializer List
| Purpose | Example |
|---|---|
| Assign field | : x = 0 |
| Compute field | : area = pi * r * r |
| Assert | : assert(x >= 0) |
| Call super | : super(name) |
Named Constructor Patterns
| Pattern | Example |
|---|---|
| Zero value | Point.origin() : this(0, 0) |
| From JSON | Point.fromJson(json) |
| From another | Point.copy(other) : this(other.x, other.y) |
| With default | Point.withDefault() : this(1, 1) |
Const Requirements
| Requirement | Detail |
|---|---|
| All fields final | Yes |
| Initializer list constant | Yes |
| No body statements | Yes |
| Const superclass | If 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Final field not initialized | No initializer | Add a constructor that assigns it |
| Field used before assigned | Initializer order | Assign in the initializer list first |
| Redirecting with a body | Not allowed | Remove the body |
| Const constructor with non-final | Field not final | Make all fields final |
| Super not called | Missing super(...) | Add it to the initializer list |
| Assert not running | Production mode | Asserts 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
| Item | Value |
|---|---|
| Generative constructor | Point(this.x, this.y) |
| Named constructor | Point.origin() : this(0, 0) |
| Redirecting constructor | Forwards with this(...) |
| Field initializer shorthand | this.field in the parameter |
| Initializer list | : field = value, assert(...) |
| Super call | : super(...) |
| Const constructor | const Point(this.x, this.y) |
| Const requirements | All fields final, constant expressions |
| Initialization order | Initializer list, super, body |
| Factory constructor | Returns 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.fieldshorthand 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()andPoint.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
constconstructor creates compile-time constants. All fields must befinal, and the initializer list must contain only constant expressions. Theconstinstance is canonicalized, so two identicalconstexpressions 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!