Dart 3.12: 3 language features that cut Flutter constructor boilerplate

A practical guide to Dart 3.12's primary constructors, private named parameters, and dot shorthands for Flutter teams.

Read time
13 min
Word count
1.8K
Sections
11
FAQs
8
Share
Abstract glowing code brackets on a dark studio background representing Dart primary constructors
Dart 3.12 trims constructor boilerplate for Flutter teams.
On this page · 11 sections
  1. The three features at a glance
  2. Primary constructors: the header syntax
  3. Private named parameters
  4. Dot shorthands
  5. Enabling primary constructors in a Flutter 3.44 project
  6. Old syntax to new syntax: the migration map
  7. Should you migrate an existing codebase?
  8. India-specific considerations
  9. FAQ
  10. How eCorpIT can help
  11. References

Summary. Dart 3.12, released with Flutter 3.44 at Google I/O 2026, adds three ways to delete constructor boilerplate: primary constructors (an experimental preview announced on the Dart blog on May 20, 2026), private named parameters, and the now-mature dot shorthand syntax that first landed in Dart 3.10. Primary constructors collapse a five-line Point class into a single line. Private named parameters let a constructor assign a private field such as _db with no initializer list. Andrea Bizzotto, the Flutter educator behind Code With Andrea, migrated a 300,000-line codebase to the new syntax in under one second using tooling he built. This guide shows the exact syntax for each feature, how to switch it on in a Flutter 3.44 project, and when the savings are worth the experimental risk.

Dart classes have carried the same shape since version 1.0: declare the fields, then repeat their names in a constructor to assign them. For a data model with eight fields, that is the field list plus eight this.field assignments, written twice. The Dart 3.12 release targets exactly that repetition. It arrived on May 20, 2026, one of two language releases the Dart team shipped at Google I/O 2026, and it pairs with Flutter 3.44.

Two of the three features here are genuinely new in Dart 3.12: primary constructors and private named parameters. The third, dot shorthands, first appeared in Dart 3.10 and matured through Dart 3.11 and 3.12. Grouped together, they attack the same problem from different angles: less repeated typing at the point where a class is declared. Here is what each one does, in the order you will reach for them.

The three features at a glance

Feature Dart version What it removes
Primary constructors 3.12 (experimental) Repeated field declarations and the main constructor body
Private named parameters 3.12 (stable) The initializer list needed to assign private fields
Dot shorthands 3.10, matured by 3.12 The redundant type name before an enum value or static member
const primary constructors 3.12 (experimental) Boilerplate on immutable classes and Flutter widgets
Constructor shorthand (new) 3.12 (experimental) Repeated long class names in secondary constructors

Primary constructors carry the most weight, so start there.

Primary constructors: the header syntax

A primary constructor declares a class's fields and its main constructor in the class header, not the body. Take the canonical example. Before Dart 3.12 you would write:


            class Point {
  final int x;
  final int y;

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

The field names appear twice, and the constructor exists only to assign them. With a primary constructor, the same class is one line:


            class Point(final int x, final int y);
          

The call site does not change. You still write final point = Point(1, 2);, as Andrea Bizzotto shows in his walkthrough. The final modifier on each parameter induces a matching field, so x and y are still final int fields on the class.

Primary constructors are not limited to toy examples. Named parameters, required, and default values all carry over. A currency model like this:


            class CurrencyRates {
  CurrencyRates({
    required this.amount,
    this.base = Currency.usd,
    required this.date,
    required this.rates,
  });

  final double amount;
  final Currency base;
  final DateTime date;
  final Map<Currency, double> rates;
}
          

becomes a header with the same public shape:


            class CurrencyRates({
  required final double amount,
  final Currency base = Currency.usd,
  required final DateTime date,
  required final Map<Currency, double> rates,
});
          

The default value for base and the required markers survive the move. Any extra factory or named constructors stay in the body.

Enhanced enums and sealed hierarchies

Enhanced enums repeat a constructor shape across every value, so they gain a lot from this syntax. A chart time-range enum that used to declare a const constructor plus four fields can pull those into the header:


            enum ChartTimeRange(final String label, final int days, final int months, final int years) {
  oneWeek('1W', 7, 0, 0),
  oneMonth('1M', 0, 1, 0),
  threeMonths('3M', 0, 3, 0),
  oneYear('1Y', 0, 0, 1),
  fiveYears('5Y', 0, 0, 5),
  all('ALL', 0, 0, 0),
  ;
}
          

The enum values are untouched; only the separate constructor and field block disappear. Sealed hierarchies get a smaller but pleasant win. An empty class body {} collapses to a semicolon:


            sealed class DatabaseIOResult;

class DatabaseIOSuccess({final String? path}) extends DatabaseIOResult;

class DatabaseIOCancelled extends DatabaseIOResult;

class DatabaseIOError(final String message) extends DatabaseIOResult;
          

const and Flutter widgets

The detail most people miss is const. With a primary constructor, the const keyword goes between class and the class name, which reads oddly at first:


            class const Insets(final double value);

const small = Insets(8);
          

Keeping const is not optional cosmetics. It affects compile-time constant construction, canonicalisation, required const contexts such as annotations, and Flutter widget allocation. A migration that silently drops const breaks those call sites, so preserve it.

That is why the feature matters for Flutter specifically. Widget classes are immutable and repetitive. A StatelessWidget like PrimaryButton with a const constructor, two fields, and a build method can shed its constructor block:


            class const PrimaryButton({
  super.key,
  required final String label,
  required final VoidCallback? onPressed,
}) extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      child: Text(label),
    );
  }
}
          

The constructor stays const, super.key is preserved, and the widget behaves identically. For a codebase with hundreds of small widgets, that is a real reduction in lines. If you are already tracking the framework's direction, this fits the same theme as Flutter's push toward Gemini-assisted development and the Dart MCP server.

Private named parameters

The second feature is smaller but removes a daily annoyance. Before Dart 3.12, a named parameter could not begin with an show, so you could not assign a private field directly through a named argument. Initializing a private field meant an explicit initializer list:


            class SnapshotRepository {
  SnapshotRepository(AppDatabase db) : _db = db;
  final AppDatabase _db;
}
          

Dart 3.12 allows private named parameters as initializing formals. You write this._db and the compiler does the rest:


            class SnapshotRepository {
  SnapshotRepository(this._db);
  final AppDatabase _db;
}
          

The parameter name exposed to callers is the public version with the show stripped, so the public API does not change. As the team at Start Debugging documents, the rule applies only to initializing formals. A regular named parameter still cannot start with an show, because that would leak a private name into the public signature. The show also has to map to a legal public identifier: this._ and this._2x are rejected, since stripping the show leaves an empty string or an illegal name.

Primary constructors take this one step further by declaring the private field in the header:


            class SnapshotRepository(final AppDatabase _db) {
  // ...
}
          

Either form instantiates the same way: final repo = SnapshotRepository(AppDatabase());.

Dot shorthands

The third feature predates 3.12 but is worth including because it is now stable and it cuts a different kind of repetition. Dot shorthands let you drop the type name before an enum value, static member, or constructor when the compiler can infer the type from context. It landed in Dart 3.10, gained analyzer and editor support in Dart 3.11, and is dependable by the Dart 3.12 toolchain.

The core idea is context typing. If a Status is expected, .running resolves to Status.running:


            Status currentStatus = .running;
          

It works in equality checks, where the static type of the left side supplies the context:


            if (myColor == .green) {
  // .green is inferred as Color.green
}
          

It also covers static methods and const contexts:


            int port = .parse('8080');
          

Dot shorthands shine in switch statements and assignments where the enum type is obvious. They do not save whole lines the way primary constructors do, but across a large file the removed type names add up, and the code reads cleaner. For a fuller picture of what shipped this cycle, see everything Flutter and Dart announced at Google I/O 2026.

Enabling primary constructors in a Flutter 3.44 project

Private named parameters and dot shorthands are on by default in Dart 3.12. Primary constructors are experimental, so you have to opt in. The steps below follow Andrea Bizzotto's setup guide.

First, set the SDK constraint in pubspec.yaml:


            environment:
  sdk: ^3.12.0
          

Next, enable the analyzer experiment so the analyzer stops complaining:


            analyzer:
  enable-experiment:
    - primary-constructors
          

If you are on a Flutter project, run Flutter 3.44.0 or higher. Commands that compile or format code need the flag too:


            flutter run --enable-experiment=primary-constructors
flutter test --enable-experiment=primary-constructors
dart format --enable-experiment=primary-constructors <changed-dart-files>
          

If you launch from VS Code or Cursor, add the argument to .vscode/launch.json so debug runs pick it up:


            {
  "version": "0.2.0",
  "configurations": [
    {
      "name": "app",
      "request": "launch",
      "type": "dart",
      "args": ["--enable-experiment=primary-constructors"]
    }
  ]
}
          

Run flutter analyze (or dart analyze for a pure package) before touching any code. If the project already has analyzer issues, resolve those first so you do not mix them with migration noise.

Old syntax to new syntax: the migration map

Not every constructor moves into the header. Redirecting and secondary constructors stay in the body and use a new shorthand instead. The feature specification lists the full mapping. The common cases:

Original Dart syntax Abbreviated syntax When it applies
LongClassName() {} new() {} Default constructor kept in the body
LongClassName.name() {} new name() {} Named constructor in the body
const LongClassName(); const new(); Const default constructor
LongClassName(): this.other(); new(): this.other(); Redirecting constructor
factory LongClassName() = D; factory() = D; Redirecting factory

Where multiple named constructors exist, only one can become the primary constructor; the rest migrate to the new shorthand. Field initializers still work, because primary constructor parameters are in scope inside the class body.

Should you migrate an existing codebase?

Here the honest answer is: not by hand, and not everywhere. Primary constructors are experimental as of Dart 3.12, there is no dart fix for them, and the old syntax stays valid. A safe migration has to preserve constructor shapes, const behaviour, comments, generated-file boundaries, and public API compatibility. Doing that manually across a large project is slow and easy to get wrong.

Automated tooling changes the calculation. Andrea Bizzotto built a migration CLI and tested it on a fork of the Flutter Gallery project that runs to more than 300,000 lines of code. It finished the migration in under one second while preserving the details above. As he puts it: "Primary constructors are a nice quality-of-life improvement for Dart. They can remove a lot of constructor boilerplate from classes, enums, sealed hierarchies, and Flutter widgets."

That framing is the right one. This is a quality-of-life change, not a rewrite you need to rush. The practical options break down like this:

Approach Speed Risk
Do nothing Instant None; old syntax stays valid
Migrate by hand Slow High on large codebases
Ad hoc coding-agent script Variable Token-heavy, hard to verify at scale
Deterministic migration CLI Seconds Low when it preserves const and API shape

For most teams, the sensible path is to write new code with the features you get for free, private named parameters and dot shorthands, hold primary constructors for greenfield files or a deliberate tooling-assisted pass, and keep the experiment flag out of production release builds until the feature stabilises. The same caution applies to any experimental Flutter change, such as Impeller becoming the mandatory renderer: adopt on your schedule, not the changelog's.

India-specific considerations

For teams in India building client work under fixed-scope contracts, the experimental status matters more than the syntax. A feature behind an experiment flag can change before it stabilises, and a client codebase that depends on it may need rework. The pragmatic split is to use the two stable Dart 3.12 features in delivery code now, and reserve primary constructors for internal tooling, prototypes, or projects where your team controls the upgrade cadence.

There is also a hiring and review angle. Primary constructor syntax reads differently, especially the class const Name(...) form. If you run a multi-developer team, agree on a convention and update your review checklist before the syntax spreads through a repository, so pull requests stay readable for everyone.

FAQ

How eCorpIT can help

eCorpIT builds and maintains production Flutter apps for clients in India and abroad, and we track each Dart and Flutter release so upgrades do not surprise our clients. If your team wants a Dart 3.12 readiness review, a safe primary-constructors migration plan, or a second opinion on which features belong in delivery code versus internal tooling, our senior engineering team can help. Reach us through the contact page to start a conversation.

References

  1. Dart team, Announcing Dart 3.12, dart.dev, May 20, 2026.
  1. Dart documentation, Primary constructors, dart.dev.
  1. Dart documentation, Constructors: private named parameters, dart.dev.
  1. Dart documentation, Dot shorthands, dart.dev.
  1. Andrea Bizzotto, Dart Primary Constructors are Cool! Here's What They Look Like, Code With Andrea, June 12, 2026.
  1. Andrea Bizzotto, How to Safely Migrate Dart Code to Primary Constructors, Code With Andrea.
  1. Start Debugging, Dart 3.12 Drops the Initializer List for Private Fields, May 2026.
  1. Flutter documentation, Flutter 3.44.0 release notes, docs.flutter.dev.
  1. Flutter documentation, What's new in the docs, docs.flutter.dev.
  1. Dart language team, Primary constructors feature specification, GitHub.
  1. Dart documentation, What's new, dart.dev.
  1. Yuri Novicow, New in Dart 3.12, Medium, June 2026.

_Last updated: July 7, 2026._

Frequently asked

Quick answers.

01 What is a primary constructor in Dart 3.12?
A primary constructor lets you declare a class's fields and its main constructor in the class header instead of the body. Writing class Point(final int x, final int y); replaces a five-line class. It shipped as an experimental preview in Dart 3.12 on May 20, 2026, and the call site stays unchanged.
02 Are Dart 3.12 primary constructors production-ready?
No. As of Dart 3.12 they are an experimental preview, gated behind the primary-constructors flag, with no dart fix migration. The old constructor syntax stays valid, so teams can wait. Adopt them in new code or behind tooling, and keep the flag out of released builds until the feature stabilises.
03 How do private named parameters work in Dart 3.12?
Before Dart 3.12, a named parameter could not start with an show, so private fields needed an initializer list. Now you can write this._db as a named or positional initializing formal, and the compiler strips the show for the public parameter name. The field stays private and callers see no change.
04 When did dot shorthands arrive in Dart?
Dot shorthands first landed in Dart 3.10, with analyzer and editor improvements in Dart 3.11. By the Dart 3.12 toolchain they are stable. The syntax lets you write .running instead of Status.running when the context type is known, which shortens enum assignments, switch cases, and equality checks.
05 How do I enable primary constructors in a Flutter project?
Set your pubspec.yaml SDK constraint to ^3.12.0, make sure you run Flutter 3.44 or higher, and add primary-constructors under analyzer: enable-experiment in analysis_options.yaml. Then pass --enable-experiment=primary-constructors to flutter run, flutter test, and dart format, and mirror the same flag in your VS Code launch.json arguments.
06 Do primary constructors change how I call a class?
No. Primary constructors only change the declaration, not the call site. class Point(final int x, final int y); is still constructed with Point(1, 2). Named parameters, required, default values, and const all carry over, so the public API and every existing caller keep working without edits.
07 How does const work with primary constructors?
The const keyword moves to between class and the class name. const Insets(this.value) becomes class const Insets(final double value);. Keeping const matters because it affects compile-time constants, canonicalisation, annotations, and Flutter widget allocation. A safe migration preserves const rather than dropping it, so const call sites stay valid.
08 Should Flutter teams migrate their whole codebase now?
Not blindly. Because the feature is experimental with no dart fix, hand-migrating 300,000 lines is impractical and error-prone. Andrea Bizzotto's migration CLI did that job in under one second, preserving constructor shapes and const. Start with new files, keep public APIs stable, and automate any bulk change.

About the author

Manu Shukla

Founder & Director

Founder of eCorpIT. Hands-on engineer leading senior-only delivery for AI apps, custom software, and cloud systems for global clients.

Subscribe

One engineering note a week. No fluff, no spam.

Senior-architect playbooks on AI agents, mobile apps, cloud, security, data, and marketing — delivered every Wednesday.

Past the reading

Read enough. Let's build something.

A senior architect responds in 24 working hours with scope, indicative cost, and a timeline. NDA before any technical conversation.