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.

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:
finalin the parameter list creates the field. The Dart docs call these declaring parameters: a parameter markedfinalorvar"implicitly induce[s] a field" (Primary constructors). A parameter without either is a plain constructor parameter.constmoves in front of the class name.class const TripCard(...)is how you get a const constructor.super.keystill works. Super parameters go in the header like any other parameter, andextendscomes 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:
- 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 strayfinalfirst, the later conversion has nothing to turn into a field; when we ran the steps in this order,Boxbecameclass Box(int s) { ... this : size = s; }with no extra field. - If you want the new style, add the lints and run
dart fix --apply --code=use_primary_constructors --code=use_declaring_parametersas a separate commit, thendart format. (Withuse_primary_constructorsalone, the fix keepsthis.parameters and a field list in the body; therequired finalform comes fromuse_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 whydart fixleft our two-constructorMoneyclass 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); }. latefields can't see constructor parameters. Non-late field initializers can use them;lateones 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
lateorexternal.
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.
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.


