In the official Dart 3.12 announcement, the Dart team shipped two language-level features: primary constructors and private named parameters. In one sentence: the field names and types you used to repeat three times — in the class body, the parameter list, and the initializer list — now collapse into a single line in the class header. Primary constructors remain experimental; private named parameters are stable. For anyone writing Flutter widgets daily, this is one of the rare updates that changes how much you actually type.
To see why it's worth stopping for, look at Dart's baggage. Since 2.0, Dart has been "safe but verbose": declaring an immutable model means final fields plus a constructor that repeats this.x for every field — easily a dozen lines of boilerplate. The community escaped this with freezed and built_value code-gen packages — at the cost of build_runner compile time, piles of .g.dart files, and macro magic newcomers can't read. Dart's own data shows more than one-fifth (>20%) of all field initializers in pub packages do nothing but assign a parameter to a private field whose name is the same minus a leading underscore. Primary constructors exist to delete exactly that boilerplate.
Who's affected? Every Flutter team — and Flutter remains a leading cross-platform stack in 2026. Dropping build_runner saves meaningful incremental-compile time on large projects; for agencies, faster model-layer work directly affects how competitively you can quote. Below: the real syntax, the trade-off vs freezed, and a risk most tutorials skip — whether to standardize on an experimental flag now.
Technical Details
Primary constructors let you declare parameters in the class header; private named parameters let a named parameter start with an underscore, and the compiler wires it to the same-named private field while exposing it publicly without the underscore.
// Before Dart 3.11: declaring an immutable model takes this much boilerplate
class Booking {
final String _id;
final DateTime _startAt;
final int _seats;
Booking({
required String id,
required DateTime startAt,
required int seats,
}) : _id = id,
_startAt = startAt,
_seats = seats;
}
// Dart 3.12: primary constructor + private named parameters
class Booking({
required String _id,
required DateTime _startAt,
required int _seats,
}) {
// Callers still write Booking(id: ..., startAt: ..., seats: ...)
}
Enable the experimental feature in analysis_options.yaml:
analyzer:
enable-experiment:
- primary-constructors
Key difference: to the caller, Booking(id: x, startAt: y, seats: z) is unchanged — the underscore is an internal convention the compiler strips from the public signature. That's the crucial distinction from simply making fields public: you keep encapsulation and drop the initializer list.
Immediate Actions
- Engineers: Upgrade the Dart SDK to 3.12. Try private named parameters (stable) on new model classes; try primary constructors behind the experiment flag in a small module. Don't rewrite everything.
- Tech leads: Audit
freezed/built_valueusage. For plain "immutable + constructor" models, evaluate replacing them and removing build_runner — but where you rely oncopyWithand union types,freezedstays irreplaceable. - Founders/agencies: Package "Dart 3.12 syntax modernization + removing unnecessary code generation" as a small tech-debt cleanup service. Short cycle, measurable result (compile time).
Comparison & Trade-offs
vs freezed: primary constructors solve "too much boilerplate," but freezed bundles "boilerplate + copyWith + equality + union types + JSON serialization." Need only the former? Drop a dependency and a build_runner pass. Lean on copyWith and sealed unions? freezed can't move yet. Migration cost: near-zero on new projects; on old ones, use new syntax only in newly added classes — don't refactor existing freezed models for stylistic uniformity.
What They Won't Tell You
- Still experimental. Shipping experimental syntax to production means tracking syntax tweaks in future releases — a bet for long-lived client projects.
- Readability cuts both ways. A long parameter list crammed into the class header isn't always diff-review friendly — five fields' types and defaults on one line can be harder to inspect than an explicit initializer list.
- The underscore-stripping rule has a learning curve —
_idbecomingidexternally isn't obvious without the docs.
Next 3 Months
Expect primary constructors to march toward stable, with IDE refactors adding one-click "convert old constructor to primary constructor." Packages like freezed may reposition around unions and serialization. Watch when official Flutter samples and starter templates adopt the new syntax by default — the "officially blessed" signal.
My Take
The mainstream line: "primary constructors finally landed, Dart is cleaner." My judgment differs: the real win isn't "less typing" — it's that the dependency on code generation is starting to loosen. Flutter's ecosystem has been hostage to build_runner. Primary constructors won't kill freezed overnight, but they make "clean models without code-gen" a default option for the first time. ScriptWalker takeaway: don't rush to rewrite old projects, but standardize on 3.12 syntax from day one on every new Flutter engagement — lighter model layer, faster compiles, better handoff. Treat "no build_runner tech debt" as an invisible edge in your quotes.