Primary Constructor Bodies and Initializer Lists in Dart
Primary constructors are reshaping how Dart classes are written. Dive into bodies, initializer lists, and when to keep a classic constructor instead.
Published on • October 15, 2026
AI Assistant

Dart 3.13 (released August 12, 2026) shipped primary constructors, a language feature that lets you declare a class’s fields and its main constructor in a single declaration header. Where the classic pattern spread a two-field class across five lines, the primary constructor collapses it to one. But the feature is more than a party trick: it comes with its own scoping rules, initializer lists, constructor bodies, and a set of constraints that determine when you should not reach for it.
Like all recent Dart language features, primary constructors are language-versioned. They require a language version of 3.13 or later.
Before and after
The classic way to declare a small data class:
class Point {
int x;
int y;
Point(this.x, int this.y);
}
With a primary constructor, the header does all of it:
class Point(var int x, var int y);
Parameters declared with var or final in the header are declaring parameters: each one implicitly induces an instance variable. A parameter without a modifier induces no field — it behaves exactly like a traditional constructor parameter. That distinction is the foundation everything else builds on:
class Point(var int x, var int y); // Induces fields x and y.
class User(String name); // No field induced; name is just a parameter.
Any other constructor in the class body is called an in-body constructor. A class with a primary constructor cannot also have other non-redirecting generative in-body constructors — the primary constructor must run on every generative instantiation.
Two scopes: initializer scope and parameter scope
This is the part that trips people up. Primary constructor parameters are visible in different parts of the class, and the same name can mean different things depending on where you are:
- Primary initializer scope — applies to non-
latefield initializers in the class body and the primary constructor’s initializer list (afterthis :). Here,xrefers to the constructor parameter. - Primary parameter scope — applies to the constructor body block. Here, a declaring parameter’s name refers to the induced field, while a non-declaring parameter still refers to the parameter.
class ScopingDemo(var String x, String suffix) {
// Field initializer: 'x' is the constructor parameter.
final String fieldAtDeclaration = x;
final String fieldInInitializer;
this : fieldInInitializer = x { // Initializer list: still the parameter.
// Body: 'x' now refers to the induced instance variable.
x = x.toUpperCase();
// 'suffix' induces no field, so it still refers to the parameter.
print('$x$suffix');
}
}
The practical payoff: field initializers can read constructor parameters directly, with no initializer list required at all:
class DeltaPoint(final int x, int delta) {
final int y = x + delta; // Reads parameters x and delta directly.
}
Adding bodies and initializer lists
To validate input or run setup logic, attach a body to the primary constructor using the this keyword:
class Point(var int x, var int y) {
this : assert(x >= 0 && y >= 0) {
print('Point initialized at ($x, $y)');
}
}
The block can contain an initializer list after this and/or a function body. For an initializer-list-only body, end with a semicolon: this : assert(x >= 0);. The block can also carry metadata annotations (@metadata this;).
Rules for the body part:
- At most one
thisbody per class. - No
async,async*, orsync*modifiers, and no=>expression bodies. - You can’t write a
thisblock at all if the header doesn’t declare a primary constructor. - An empty body is flagged by the new
unnecessary_primary_constructor_bodylint — if the body does nothing, replace{}with;and keep the declaration on one line.
The rest of the toolkit
Constant primary constructors put const before the class name:
class const ConstPoint(final int x, final int y) {
final int z;
this : z = x + y; // Initializer list OK; body block is a compile-time error.
}
Every field must be final, non-late, and definitely initialized, and all initializing expressions must be potentially constant.
Named and private primary constructors append the name after the class:
class Point.custom(var int x, var int y);
class Point._(var int x, var int y); // Forces factory methods for creation.
Private named parameters (introduced in Dart 3.12, and now the cleanest way to initialize private fields) drop the leading underscore for callers:
class User({required var String _name});
// Call site is still: User(name: 'John Doe')
Super parameters forward straight to the superclass: class Employee(super.name, super.age, final String role) extends Person;
Enums get the same treatment, and enum primary constructors are implicitly constant:
enum Color(final String hex) {
red('#FF0000'),
green('#00FF00'),
blue('#0000FF');
}
Gotchas and breaking changes
The biggest migration hazard: final and var on parameters are now reserved for declaring parameters in primary constructors. Using them on any other parameter — a top-level function, a method, a closure, an in-body constructor — is a compile-time error (extraneous_modifier):
void printValue(final int x) => print(x); // Error on language version 3.13+.
To prepare a codebase still on 3.12 or lower, enable the avoid_final_parameters and var_with_no_type_annotation lint rules, which flag the modifiers ahead of the upgrade. Once on 3.13, dart fix removes them.
Other constraints to keep in mind:
- Declaring parameters can’t be
lateorexternal— declare those fields in the body as usual. - A declaring parameter that collides with an existing method or field name is a compile-time error.
- Parameters are read-only in the initializer scope; assigning to one (
x = 5) there is an error. - A field can’t be initialized both at its declaration and by a declaring parameter or the initializer list — double initialization is rejected.
covariantis only allowed onvar(mutable declaring) parameters.- Mixin classes can only have a trivial primary constructor: no parameters, no initializer list, no body. Extension types must have exactly one (always declaring) parameter.
- Watch out for a method literally named
factorywith no return type — the compiler parses it as a concise factory constructor. Give it an explicit return type.
When to keep a classic constructor
Primary constructors are concise, but they’re not universal. Keep the classic form when:
- You need
lateorexternalfields — declaring parameters reject both modifiers. - The class mixes concerns that don’t map cleanly onto header parameters, such as fields computed from several parameters via non-trivial logic that doesn’t fit an initializer list.
- You need multiple generative constructors — a primary constructor rules out other non-redirecting generative in-body constructors; redirecting factories remain your tool.
- You’re writing a mixin class, which only supports a trivial (parameterless) primary constructor.
For simple data classes, models, and value types — the majority of Flutter code — the primary constructor plus an initializer list or body covers nearly everything the classic form did, in a fraction of the lines. The new use_declaring_parameters lint (with an experimental use_primary_constructors variant) will point you at candidates as the ecosystem settles.
Sources
- Primary constructors — Dart language guide covering scopes, bodies, initializer lists, and constraints.
- Dart changelog — primary constructors, parameter modifier restrictions, and related lints in Dart 3.13; private named parameters in 3.12.