Home Pricing Explore MCP Docs Create a project

Home/Blog/Engineering

Dart Primary Constructors in Flutter: What We Changed, What dart fix Changed, and What Broke

Dart 3.13 primary constructors in Flutter widgets: what compiles, the final-parameter break, and the dart fix change that added a public field. Tested on 3.47.

Share this story
Dart Primary Constructors in Flutter: What We Changed, What dart fix Changed, and What Broke

Dart 3.13 shipped primary constructors on 12 August 2026, and Flutter 3.47 carries it; the current stable, Flutter 3.47.6, bundles Dart 3.13.5. A primary constructor declares a class's fields and its constructor together in the class header, so each property is written once instead of twice.

Most write-ups stop at the before-and-after. We wanted to know what happens to a real Flutter file, so we wrote one in the old style, ran dart fix on it with Flutter 3.47.6, and compiled the result. The short version: the syntax works well for widgets, nothing changes until you raise your SDK constraint, and one automated fix quietly added a public field we didn't ask for.

What a widget looks like now

Here's an ordinary widget, the kind every Flutter codebase and every code generator produces:

class TripCard extends StatelessWidget {
  final Trip trip;
  final VoidCallback? onTap;

  const TripCard({super.key, required this.trip, this.onTap});

  @override
  Widget build(BuildContext context) {
    return ListTile(title: Text(trip.name), onTap: onTap);
  }
}

And the same widget with a primary constructor:

class const TripCard({
  super.key,
  required final Trip trip,
  final VoidCallback? onTap,
}) extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListTile(title: Text(trip.name), onTap: onTap);
  }
}

Three things to notice:

  • final in the parameter list creates the field. The Dart docs call these declaring parameters: a parameter marked final or var "implicitly induce[s] a field" (Primary constructors). A parameter without either is a plain constructor parameter.
  • const moves in front of the class name. class const TripCard(...) is how you get a const constructor.
  • super.key still works. Super parameters go in the header like any other parameter, and extends comes after the parameter list.

A StatefulWidget converts the same way. Its State class doesn't change, and widget.start reads the field exactly as before:

class const Counter({super.key, final int start = 0}) extends StatefulWidget {
  @override
  State<Counter> createState() => _CounterState();
}

Plain data classes get even shorter. A class with an empty body can end in a semicolon:

class Trip({
  required final String id,
  required final String name,
  required final DateTime departsOn,
  final List<String> items = const [],
});

All of these passed dart analyze with no errors under Dart 3.13.5 in a project whose pubspec.yaml says sdk: ^3.13.0.

Nothing changes until you raise your SDK constraint

Dart turns language features on per package, based on the lower bound of the environment: sdk: constraint in pubspec.yaml. Upgrading Flutter alone doesn't opt you in. We checked both directions on Dart 3.13.5:

  • With sdk: ^3.12.0, every primary constructor was a compile error: "This requires the 'primary-constructors' language feature to be enabled. Try updating your pubspec.yaml to set the minimum SDK constraint to 3.13.0 or higher."
  • With sdk: ^3.13.0, they compiled.

That second step comes with a breaking change, listed in the Dart 3.13 changelog: "You can no longer use final or var on non-declaring parameters" (Dart SDK changelog). The keywords now mean "make this a field", so they're only allowed where that makes sense. Code like this, which some teams enforced with the prefer_final_parameters lint, stops compiling:

int twice(final int x) => x * 2; // error at sdk ^3.13.0: extraneous_modifier

In the same file at sdk: ^3.12.0, it compiled with no issues.

If your team used that lint, remove it as well: at sdk: ^3.13.0 the analyzer reports prefer_final_parameters as deprecated and says it shouldn't be enabled. for (final item in items) is a loop variable, not a parameter, and is unaffected.

New projects reach this point immediately. In the Flutter 3.47.6 tool source, flutter create writes sdk: ^<bundled Dart version> into the new pubspec.yaml, which is ^3.13.5 for this release. Existing apps reach it the day someone bumps the constraint, often as part of adding a package that needs a newer Dart.

What dart fix did to our file

Dart 3.13 added several lints aimed at the new syntax: use_declaring_parameters, unnecessary_type_name_in_constructor, unnecessary_primary_constructor_body, initialize_in_field_declaration, unnecessary_const_in_enum_constructor, and an experimental use_primary_constructors. None of them are in flutter_lints 6.0.0, the current version, so you only get them if you add them to analysis_options.yaml.

We enabled four of them and ran dart fix --apply on a file with a data class, a stateless widget, a stateful widget and its State, a class with two constructors, and a function with a final parameter. Across our three test files it applied 17 fixes. What came out of the main file:

Before After dart fix --apply
Trip data class with named this. parameters Primary constructor with required final parameters, but with an empty { } body split across two lines until we ran dart format
TripCard and Counter widgets Primary constructors, as shown above, const and super.key intact
_CounterState (no constructor) class _CounterState() extends State<Counter>: an empty primary constructor that adds nothing
Money with Money(...) and Money.zero() Not converted to a primary constructor. It kept a normal body and switched to the new short form for in-body constructors: new(this.cents) and new zero()
twice(final int x) twice(int x)
Box(final int s) : size = s; class Box(final int s) { final int size; this : size = s; }, which now has an extra public field s

That new form is the other half of the feature: Dart 3.13 lets you declare constructors in a class body with new or factory instead of repeating the class name (see the changelog). The unnecessary_type_name_in_constructor lint is what rewrote Money.

The last row is the one to watch. In the original class, final int s was a plain parameter that happened to carry a redundant final. The primary-constructor fix moved it into the header unchanged, and in the header final means "declare a field". The class gained a public s field next to size. Box(3).s passed analysis without complaint. Nothing breaks at compile time, but the class's API grew, and in a widget or model that's serialized or compared, an extra field can matter.

When we ran dart fix with only flutter_lints, so without use_primary_constructors, the same class came out as Box(int s) : size = s;. The final was simply dropped, which is the correct fix.

So run the two changes separately:

  1. Raise the SDK constraint, then fix only the breaking change: dart fix --apply --code=extraneous_modifier. Review that diff on its own. Because this strips the stray final first, the later conversion has nothing to turn into a field; when we ran the steps in this order, Box became class Box(int s) { ... this : size = s; } with no extra field.
  2. If you want the new style, add the lints and run dart fix --apply --code=use_primary_constructors --code=use_declaring_parameters as a separate commit, then dart format. (With use_primary_constructors alone, the fix keeps this. parameters and a field list in the body; the required final form comes from use_declaring_parameters.) Then read each converted class header looking for parameters that turned into fields.

Rules that will catch you

These come from the language docs and the Flutter team's own agent skill for the feature (dart-use-primary-constructors):

  • Only one non-redirecting generative constructor. If a class has a primary constructor, every other generative constructor must redirect to it, for example new zero() : this(0);. That's why dart fix left our two-constructor Money class alone; we converted it by hand and it compiled.
  • The primary constructor's initializer list moves into the body. Its asserts and computed fields go in a this : ...; block (other, redirecting constructors keep their own : ... lists): class Money(final int cents) { this : assert(cents >= 0); }.
  • late fields can't see constructor parameters. Non-late field initializers can use them; late ones can't, because they may run after construction.
  • Parameters can't be reassigned during initialization, and no field may be initialized twice.
  • Declaring parameters can't be late or external.

Should you migrate?

For a Flutter app, the main benefit is concrete: each property is declared once, so the field list can't drift out of sync with the constructor, and data classes with empty bodies shrink to a single declaration. Widgets save less than you might expect: after dart format, our TripCard went from 11 lines to 10. The case against is mostly cost. A full migration touches nearly every file, which buries real changes in review and in git blame, and the result is less familiar to anyone who hasn't read about the feature yet.

Package authors have a harder choice. Raising your package's SDK lower bound to 3.13 means every app that depends on it must be built with Dart 3.13 or later. The app itself doesn't have to adopt the syntax, because language versions are set per package, but its toolchain does.

A reasonable middle path for apps: raise the constraint when you're ready, fix the extraneous_modifier errors, and write new classes in the new style while leaving old ones alone until you touch them anyway. Turn on use_declaring_parameters or use_primary_constructors only when the team agrees, so the lints don't flag hundreds of untouched files.

If you use code generators that read your classes, such as JSON or data-class builders, check that the versions you use support the new syntax before converting the classes they process. We didn't test any generators for this article.

AI-written code will arrive in both styles

The Flutter team now publishes agent skills, instruction files that teach AI coding agents how to do Flutter tasks, in the flutter/agent-plugins repository. One of them, dart-use-primary-constructors, is dedicated to this feature. It tells agents that primary constructors are on by default from Dart 3.13, experimental in 3.12, and unsupported before that.

The skill suggests agents don't reliably produce the new syntax on their own yet. Until they do, code from AI assistants and app builders will mix old and new constructors, and both compile. Pick a style once and enforce it with lints in analysis_options.yaml, so dart analyze flags generated and hand-written code alike.

If your project came from an AI Flutter app builder, the steps are the same as for any other project: check the sdk: line in the generated pubspec.yaml, run dart analyze, and decide on the lints before running dart fix. Our piece on judging an AI app builder by the repo it leaves you has more on what to check in generated code, and the Material and Cupertino package migration is the other upgrade worth planning for this autumn.

How we tested. Dart 3.13.5 and Flutter 3.47.6 stable on Linux, 7 October 2026. Every example on this page passed dart analyze, except the one marked as an error. The dart fix results in the table are from real runs on our sample file, with and without the new lints enabled. We did not test code generators, IDE quick-fixes, or a large codebase.
  • Dart
  • Flutter
  • Dart 3.13
  • Migration
  • Lints

Share this post

Written by

Engineering, FlutterGo

The engineers behind FlutterGo's code generation, live preview and builds. We write about how it works and what we learn building it.

3 posts