Dart 19 🎯 Const Constructors and Immutable Class Instances
A const constructor is what turns a class from a runtime type into a compile-time value. When a class has a const constructor and all its fields are final, instances can be created at compile time and stored in the program’s read-only data. The compiler canonicalizes those instances: two const Point(1, 2) expressions anywhere in the program refer to the same object in memory. This is not a minor optimization. It is the mechanism that lets immutable value objects be used in const lists, const maps, switch patterns, default parameter values, and annotations, all of which require compile-time constants.
Immutability is the precondition. A class whose fields can change cannot be a compile-time constant, because the compiler cannot freeze a value that might later be modified. The final keyword on every field, combined with a const constructor whose initializer list contains only constant expressions, is the recipe for an immutable value class. The result is an object that behaves like a primitive: it can be shared freely, compared by value, and used wherever a literal would be accepted.
This chapter covers the const constructor, the final field requirement, canonicalization, deep versus shallow immutability, the difference between const and final variables, immutable collections, the patterns for value objects, and the mistakes that break the const contract.
Key point: A const constructor requires all fields to be final and the initializer list to contain only constant expressions. const instances are canonicalized: identical const expressions refer to the same object. A const constructor makes the class usable in const contexts — const lists, maps, default values, and patterns. Immutability is shallow: a final field holding a mutable list can still have its contents changed.
Why const constructors and immutability matter
The compile-time constant problem. Some language features require values that are known at compile time: const variables, default parameter values, annotation arguments, and case patterns in a switch. An instance of a user-defined class can be such a value only if the class has a const constructor. Without it, the class cannot participate in these features.
The canonicalization problem. Two const Point(1, 2) expressions should represent the same value. Canonicalization makes them the same object, so identical(a, b) is true and a == b follows. This is what allows const instances to be used as map keys and set elements without surprises.
The immutability problem. A const instance cannot change. If a field were mutable, the compiler could not guarantee the value stays constant, and the canonicalization would be unsound. The final requirement on every field enforces the guarantee.
The performance problem. A const instance is created once and shared. A runtime instance is created each time the constructor is called. For value objects that appear frequently — colors, sizes, configurations — the const version avoids allocation and reduces memory.
The sharing problem. Immutable objects can be shared safely. Two parts of the program can hold references to the same const instance without worrying that one will change it. This is the foundation of value semantics: the object is identified by its value, not by its identity.
a. The const constructor
A const constructor is declared with the const keyword before the constructor name. It requires that all fields are final and that the initializer list contains only constant expressions.
class Point {
final double x;
final double y;
const Point(this.x, this.y);
}
The const keyword on the constructor marks it as a constant constructor. The compiler verifies that the class’s fields are all final and that the initializer list does not call any non-constant function.
A class can have both a const constructor and a non-const constructor:
class Point {
final double x;
final double y;
const Point(this.x, this.y);
Point.runtime(this.x, this.y);
}
The const constructor is used with the const keyword at the call site, and the non-const constructor is used without it. A class with a const constructor can still be instantiated at runtime by calling the constructor without const.
b. Creating const instances
A const instance is created with the const keyword before the constructor call, or in a const context where the keyword is implied.
const origin = Point(0, 0);
const unit = Point(1, 1);
Inside a const context — a const list, a const map, a const variable’s initializer, a default parameter value — the const keyword is implied for the inner expressions:
const points = [
Point(0, 0), // const implied
Point(1, 1), // const implied
];
The list is const, so its elements must be const, and the compiler infers the const for the Point expressions. Writing const Point(0, 0) inside the list is allowed but redundant.
A const instance can also be created with the const keyword outside a const context:
var p = const Point(0, 0);
The const keyword makes the instance a compile-time constant, and the variable p is a runtime variable that holds the constant.
c. Canonicalization
The compiler canonicalizes const instances. Two const expressions with the same values refer to the same object.
const a = Point(1, 2);
const b = Point(1, 2);
print(identical(a, b)); // true
print(a == b); // true
The identical function checks whether two references point to the same object. For const instances, identical expressions are canonicalized to the same object, so identical returns true.
A runtime instance is not canonicalized:
var c = Point(1, 2); // runtime instance
print(identical(a, c)); // false
print(a == c); // true if == is overridden
The runtime instance is a separate object. The == comparison may still return true if the class overrides == to compare by value, but identical returns false.
Canonicalization applies to const instances of any class, not just user-defined ones. const [1, 2] and const [1, 2] refer to the same list.
d. Deep versus shallow immutability
A const constructor makes the fields final, but final is shallow. A final field holding a mutable list can still have its contents changed.
class Config {
final List<String> hosts;
const Config(this.hosts);
}
const config = Config(['a', 'b']);
// config.hosts.add('c'); // error: cannot modify a const list
The const constructor requires the list to be const as well, because the initializer list must contain constant expressions. A const list is deeply immutable, and its contents cannot be changed.
But a class with a final field that holds a non-const list is only shallowly immutable:
class Config {
final List<String> hosts;
Config(this.hosts);
}
var config = Config(['a', 'b']);
config.hosts.add('c'); // allowed: the list is not const
The hosts field cannot be reassigned, but the list it points to can be modified. The class is immutable at the field level but not at the content level.
Deep immutability requires that every field holds an immutable value. This means using const lists, const maps, or the unmodifiable wrappers.
e. const versus final variables
The two keywords are related but distinct. A const variable is a compile-time constant. A final variable is a runtime constant that is assigned once.
| Aspect | const | final |
|---|---|---|
| When assigned | Compile time | Runtime |
| Can be a constant expression | Yes | No |
Can be used in const context | Yes | No |
| Value known at compile time | Yes | No |
| Example | const x = 5; | final x = DateTime.now(); |
const a = 5; // compile-time constant
final b = DateTime.now(); // runtime constant
A const variable is implicitly final. A final variable is not implicitly const.
A const constructor requires the instance to be created in a const context, which means all the arguments must be compile-time constants. A final variable can hold a const instance or a runtime instance.
f. Immutable collections
The dart:collection library provides unmodifiable wrappers for lists, sets, and maps.
import 'dart:collection';
final list = List<int>.unmodifiable([1, 2, 3]);
final set = Set<int>.unmodifiable({1, 2, 3});
final map = Map<String, int>.unmodifiable({'a': 1});
The wrapper prevents modification, but the underlying collection can still be changed by whoever holds a reference to it. The wrapper is a view, not a copy.
For deep immutability, the const collections are the alternative:
const list = [1, 2, 3]; // deeply immutable
const set = {1, 2, 3}; // deeply immutable
const map = {'a': 1, 'b': 2}; // deeply immutable
The const collections are canonicalized and their contents cannot be changed. They are the correct choice for immutable value objects.
A const constructor can take a const collection as a field:
class Config {
final List<String> hosts;
const Config(this.hosts);
}
const config = Config(['a', 'b']);
The ['a', 'b'] is a const list because the surrounding context is const. The config instance holds a deeply immutable list.
Complete Example Session
// ============================================
// PART 1: CONST CONSTRUCTOR
// ============================================
class Point {
final double x;
final double y;
const Point(this.x, this.y);
}
void main() {
const origin = Point(0, 0);
print('${origin.x}, ${origin.y}'); // 0.0, 0.0
}
// ============================================
// PART 2: CONST IMPLIED IN A CONST CONTEXT
// ============================================
class Point {
final double x;
final double y;
const Point(this.x, this.y);
}
void main() {
const points = [
Point(0, 0),
Point(1, 1),
Point(2, 2),
];
print(points.length); // 3
}
// ============================================
// PART 3: CANONICALIZATION
// ============================================
class Point {
final double x;
final double y;
const Point(this.x, this.y);
}
void main() {
const a = Point(1, 2);
const b = Point(1, 2);
print(identical(a, b)); // true
}
// ============================================
// PART 4: RUNTIME VS CONST INSTANCE
// ============================================
class Point {
final double x;
final double y;
const Point(this.x, this.y);
@override
bool operator ==(Object other) =>
other is Point && other.x == x && other.y == y;
@override
int get hashCode => Object.hash(x, y);
}
void main() {
const a = Point(1, 2);
var b = Point(1, 2);
print(identical(a, b)); // false
print(a == b); // true
}
// ============================================
// PART 5: CONST AS DEFAULT PARAMETER
// ============================================
class Point {
final double x;
final double y;
const Point(this.x, this.y);
}
void moveTo({Point target = const Point(0, 0)}) {
print('Moving to ${target.x}, ${target.y}');
}
// ============================================
// PART 6: CONST IN A SWITCH PATTERN
// ============================================
class Direction {
final int dx;
final int dy;
const Direction(this.dx, this.dy);
}
const north = Direction(0, -1);
const south = Direction(0, 1);
void describe(Direction d) {
switch (d) {
case north:
print('Going north');
case south:
print('Going south');
default:
print('Going somewhere');
}
}
// ============================================
// PART 7: SHALLOW IMMUTABILITY
// ============================================
class Config {
final List<String> hosts;
Config(this.hosts);
}
void main() {
var config = Config(['a', 'b']);
config.hosts.add('c'); // allowed
print(config.hosts); // [a, b, c]
}
// ============================================
// PART 8: DEEP IMMUTABILITY WITH CONST
// ============================================
class Config {
final List<String> hosts;
const Config(this.hosts);
}
void main() {
const config = Config(['a', 'b']);
// config.hosts.add('c'); // error: unmodifiable
}
// ============================================
// PART 9: UNMODIFIABLE WRAPPER
// ============================================
import 'dart:collection';
void main() {
final list = List<int>.unmodifiable([1, 2, 3]);
// list.add(4); // error: unmodifiable
}
// ============================================
// PART 10: CONST VS FINAL
// ============================================
void main() {
const a = 5; // compile-time constant
final b = DateTime.now(); // runtime constant
print('$a, $b');
}
These ten parts cover the const constructor, const implied in a const context, canonicalization, runtime versus const instances, const as a default parameter, const in a switch pattern, shallow immutability, deep immutability with const, the unmodifiable wrapper, and const versus final.
Quick Reference
Const Constructor
| Requirement | Detail |
|---|---|
| All fields final | Yes |
| Constant initializer list | Yes |
| No body statements | Yes |
| Const superclass | If extending |
Const vs Final
| Aspect | const | final |
|---|---|---|
| When assigned | Compile time | Runtime |
| Constant expression | Yes | No |
| Usable in const context | Yes | No |
| Implicitly | final | — |
Canonicalization
| Expression | identical |
|---|---|
const Point(1, 2) vs const Point(1, 2) | true |
const Point(1, 2) vs Point(1, 2) | false |
const [1, 2] vs const [1, 2] | true |
Immutability Levels
| Level | Description |
|---|---|
| Shallow | Fields are final; contents may be mutable |
| Deep | Fields are final and hold immutable values |
| Const | Compile-time constant, canonicalized |
Const Collections
| Type | Example |
|---|---|
| List | const [1, 2, 3] |
| Set | const {1, 2, 3} |
| Map | const {'a': 1} |
Unmodifiable Wrappers
| Constructor | Purpose |
|---|---|
List.unmodifiable | Read-only list view |
Set.unmodifiable | Read-only set view |
Map.unmodifiable | Read-only map view |
Best Practices
✅ Do This:
// Use const constructors for value classes
class Point {
final double x;
final double y;
const Point(this.x, this.y);
}
// Use const for instances that appear frequently
const origin = Point(0, 0);
// Use const collections for deep immutability
const config = Config(['a', 'b']);
// Use const for default parameter values
void moveTo({Point target = const Point(0, 0)}) { }
// Use const in switch patterns
switch (direction) {
case north: ...
}
❌ Don’t Do This:
// Non-final field in a const constructor
class Point {
double x; // ❌
const Point(this.x);
}
// Non-constant initializer in a const constructor
const Point(this.x) : y = compute(); // ❌
// Assume final is deeply immutable
class Config {
final List<String> hosts; // ⚠️ contents mutable
}
// Use a runtime instance where const is required
const a = Point(DateTime.now().year, 0); // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
const constructor rejected | A field is not final | Make all fields final |
| Non-constant initializer | Function call in the list | Use a constant expression |
| Shallow immutability surprise | final field holds a mutable list | Use const or unmodifiable |
identical returns false | Runtime instance | Use const for canonicalization |
| Const in a non-const context | Missing const keyword | Add const or use a const context |
| Mutable collection in a const class | Non-const list | Use const list |
Real-World Examples
1. Value Class
class Point {
final double x;
final double y;
const Point(this.x, this.y);
}
2. Color
class Color {
final int r, g, b;
const Color(this.r, this.g, this.b);
}
const red = Color(255, 0, 0);
3. Configuration
class Config {
final String host;
final int port;
const Config(this.host, this.port);
}
const defaultConfig = Config('localhost', 8080);
4. Default Parameter
void connect({Config config = const Config('localhost', 80)}) { }
5. Switch Pattern
const north = Direction(0, -1);
switch (d) { case north: ... }
6. Const List
const colors = [Color(255, 0, 0), Color(0, 255, 0)];
7. Const Map
const sizes = {'small': 1, 'large': 2};
8. Immutable Value Object
class Money {
final int cents;
final String currency;
const Money(this.cents, this.currency);
}
9. Unmodifiable List
final list = List<int>.unmodifiable([1, 2, 3]);
10. Canonicalization
const a = Point(1, 2);
const b = Point(1, 2);
print(identical(a, b)); // true
Visual
Const Constructor Requirements
┌──────────────────────────────────────────────────────────────┐
│ class Point { │
│ final double x; ← all fields final │
│ final double y; │
│ │
│ const Point(this.x, this.y); ← const keyword │
│ └── initializer list: constant expressions only │
│ } │
│ │
│ const origin = Point(0, 0); ← const context │
└──────────────────────────────────────────────────────────────┘
Canonicalization
┌──────────────────────────────────────────────────────────────┐
│ const a = Point(1, 2); │
│ const b = Point(1, 2); │
│ │
│ ┌─────────────────────┐ │
│ │ Point(1, 2) │ ← one canonical instance │
│ └─────────────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ a ─┘ b ─┘ │
│ │
│ identical(a, b) is true. │
└──────────────────────────────────────────────────────────────┘
Shallow vs Deep Immutability
┌──────────────────────────────────────────────────────────────┐
│ SHALLOW (final field, mutable list): │
│ class Config { │
│ final List<String> hosts; │
│ } │
│ └── config.hosts.add('c') is allowed │
│ │
│ DEEP (const field, const list): │
│ class Config { │
│ final List<String> hosts; │
│ const Config(this.hosts); │
│ } │
│ const config = Config(['a', 'b']); │
│ └── config.hosts.add('c') is an error │
└──────────────────────────────────────────────────────────────┘
Const vs Final Variables
┌──────────────────────────────────────────────────────────────┐
│ const a = 5; // compile-time constant │
│ └── usable in const contexts │
│ │
│ final b = DateTime.now(); // runtime constant │
│ └── not usable in const contexts │
│ │
│ const c = Point(1, 2); // const instance │
│ final d = Point(1, 2); // runtime instance │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Const constructor | const Point(this.x, this.y) |
| Requirement | All fields final, constant initializer list |
| Const instance | const Point(1, 2) |
| Canonicalization | Identical const expressions share an object |
| Const vs final | Compile time vs runtime |
| Shallow immutability | Final fields; contents may be mutable |
| Deep immutability | Const fields and const collections |
| Unmodifiable wrapper | List.unmodifiable, Set.unmodifiable |
| Const in default params | const Point(0, 0) |
| Const in patterns | case north: |
Key takeaways:
- A
constconstructor makes the class usable in compile-time contexts.constlists, maps, default parameter values, andswitchpatterns all require instances that are known at compile time. - All fields must be
final. The compiler verifies this. A mutable field would make the const guarantee unsound. - The initializer list must contain constant expressions. No function calls, no runtime values. The compiler evaluates the expressions at compile time.
constinstances are canonicalized. Two identicalconstexpressions refer to the same object, soidenticalreturnstrue. This is what makesconstinstances safe as map keys and set elements.finalis shallow immutability. Afinalfield holding a mutable list can have its contents changed. Deep immutability requiresconstcollections or unmodifiable wrappers.constandfinalare different.constis a compile-time constant,finalis a runtime constant assigned once. Aconstvariable is implicitlyfinal.- Immutable value classes are the right choice for value semantics. Objects that are compared by value, used as keys, or shared across the program benefit from the
constconstructor and the canonicalization it enables.
Remember: The const constructor is what turns a class into a compile-time value. It requires every field to be final and every initializer to be a constant expression, and in return it allows the class to be used wherever a literal would be accepted. The canonicalization means identical const expressions share one object, which is what makes value objects behave like primitives. The immutability is the precondition: a const instance cannot change, and a final field alone does not guarantee that the contents of a collection are immutable. For deep immutability, use const collections. For value classes — points, colors, configurations, money — the const constructor is the idiomatic pattern in Dart.
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!