All Articles
CategoryFlutter
Reading Time
15 min read
Published
2026-09-07
Word Count
3,577words

Grab a coffee — this one is a deep dive!

Dart 3.13 Primary Constructors: Write Classes in One Line

Summary

What is a Dart primary constructor and how do you use it? var/final declaring parameter rules, the in-body constructor restriction, const classes, and Flutter widget gains — a Dart 3.13 guide.

  • Dart 3.13 (August 12, 2026) lets you write class Point(var int x, var int y); to define fields and a constructor in one line.
  • Parameters marked with var/final automatically produce fields (declaring parameters); parameters without a modifier stay plain constructor arguments.
  • A class with a primary constructor can't define a second non-redirecting generative constructor; validation logic goes in an assert in the `this :` clause instead.
  • In extension types the single parameter is always a declaring parameter; a const primary constructor can't have a body block, and every field must be final.
Dart 3.13 Primary Constructors: Write Classes in One Line

Dart 3.13 was released on August 12, 2026, bringing one of the language's most fundamental syntax innovations: primary constructors. Now you can define a class's fields and its main constructor in a single line — writing class Point(var int x, var int y); is enough. This guide walks through what primary constructors actually do, what changes when you add var/final, when the compiler stops you, and when you should and shouldn't use them in Flutter projects.

💡 Pro Tip: A primary constructor only changes how you write things, not runtime behavior — meaning you can migrate gradually without breaking your existing code's logic.

Table of Contents

What Is a Primary Constructor: The Two Faces of the Point Class

In traditional Dart, when writing a Point class you declare the fields in the class body, then re-map them with this.x, this.y inside the constructor:

dart
1// Classic syntax before Dart 3.13
2class Point {
3 final int x;
4 final int y;
5 Point(this.x, this.y);
6}

With Dart 3.13 you can write the same class in a single line:

dart
1// Dart 3.13 primary constructor
2class Point(var int x, var int y);

The Dart team's official definition is: "Primary constructors provide a concise way to declare a class's fields and its main constructor in a single line." In other words, x and y become both constructor arguments and instance variables — no separate body, no separate assignment. Critically, this isn't a performance optimization, it's a pure syntax shortcut. The Dart documentation states this explicitly: "This shorthand changes how you write the declaration, but it doesn't change runtime behavior." The compiled code behaves identically to the classic syntax.

You can even drop the curly braces for empty-body classes. The documentation adds: "An empty body of a class...can be replaced by a semicolon (;)...particularly useful when using a primary constructor to keep the entire declaration on a single line." That's exactly why the Point example above ends in a semicolon.

Declaring Parameters: What Happens When You Add var/final, and When You Don't

The heart of the primary constructor is the "declaring parameter." The rule: mark a parameter with var or final and it automatically produces an instance variable (field). Officially: "Declaring a parameter in the primary constructor with var or final implicitly induces an instance variable." Skip the modifier and nothing happens — it just behaves like a traditional constructor parameter: "If you omit the modifier, the parameter doesn't create a field. It behaves just like a parameter in a traditional constructor."

dart
1// With var/final: x and y become fields
2class Point(var int x, var int y);
3 
4// No modifier: delta does NOT create a field, it's a parameter in the field initializer
5class DeltaPoint(final int x, int delta) {
6 final int y = x + delta;
7}

This matters because it's tempting to add var to every parameter out of habit, but sometimes you only need a temporary constructor argument (like delta above) — in that case you deliberately skip the modifier.

Two different "scope" rules also come into play here. The primary initializer scope applies, in the documentation's words, "to non-late field initializers in the class body and to the primary constructor's initializer list (after this :)"; there the parameter name refers to the incoming argument. The primary parameter scope, however, is only valid for "the body block of the primary constructor (inside { ... })"; there the declaring parameter's name refers to the generated instance variable. Easy to mix up — the next section shows how the body block is written.

The In-Body Constructor Rule: Why a Second Generative Constructor Is Forbidden

A class with a primary constructor cannot define another non-redirecting generative constructor. The documentation states it plainly: "a class...with a primary constructor can't have any other non-redirecting generative in-body constructors." The ban doesn't care about the name: a named but non-redirecting generative in-body constructor like Point.origin() : x = 0, y = 0; triggers the same error. What remains free are only constructors that redirect to the primary constructor, plus factory constructors — which aren't generative at all. The documentation's private-primary-constructor pattern relies on exactly this: "forces callers to use factory methods or other constructors."

So where does validation logic like an initializer list or assert go? The primary constructor body (the this block) solves this:

dart
1class Point(var int x, var int y) {
2 this : assert(x >= 0 && y >= 0) {
3 // additional validation or side effects can run here
4 }
5}

This body part has its own restrictions: "A primary constructor body part...can't use the async, async*, or sync* modifiers, and it can't use the expression body arrow (=>) syntax." So no this : ... async { } and no => shorthand — only the classic curly-brace body.

The documentation gives the reason: "To ensure the primary constructor executes on every new instance." That guarantee is what lets declaring parameters reliably produce fields.

Named, Optional, and Super Parameters

Primary constructors also support named constructors — you just write an extra name after a dot following the class name:

dart
1// Named primary constructor
2class Point.custom(var int x, var int y);
3 
4// Private named primary constructor
5class Point._(var int x, var int y);

Named/optional parameters and the required keyword work just as they do in classic constructors. If you want to initialize a private field with a named parameter:

dart
1class User({required var String _name});
2// Calls from outside still use the public name:
3// User(name: 'John Doe')

super parameters can also be used directly inside a primary constructor when inheriting — you don't need to redeclare the superclass's fields:

dart
1class Person {
2 Person(this.name, this.age);
3 final String name;
4 final int age;
5}
6 
7class Employee(super.name, super.age, final String role) extends Person;

Here role is a new declaring parameter, while super.name and super.age pass straight to the superclass's constructor — noticeably cutting repetition when writing layered model classes in Flutter (e.g., domain models built on a BaseEntity).

Primary constructors can also be used in enums, and are implicitly const:

dart
1enum Color(final String hex) {
2 red('#FF0000'),
3 green('#00FF00'),
4 blue('#0000FF');
5}

The documentation clarifies: "Primary constructors in enums are implicitly constant." So you don't need to write the const keyword by hand when creating enum values.

Const Classes and Immutability

For immutable data classes, you can combine a primary constructor with const:

dart
1class const ConstPoint(final int x, final int y) {
2 final int z;
3 // Initializer list is allowed, a body block is not.
4 this : z = x + y;
5 
6 int get squaredDistanceFromOrigin => x * x + y * y;
7}

There's a strict rule here: a const primary constructor cannot have a curly-brace body block (this : ... { }) — even an empty one. The documentation: "Having a { ... } body is a compile-time error, even if it's empty." The restriction targets the constructor's body block, not the class body: the field, getter, and initializer list above are all fine; this : z = x + y { } would error.

There's an additional restriction on a const constructor's fields: "every instance variable must be final, can't be late, and must be definitely initialized." So final, not var, and no late. That's consistent with Dart's principle that const objects must be fully determined at compile time — declaring parameters enforce it automatically.

Practical Gains in Widget Classes

StatelessWidget and StatefulWidget subclasses in Flutter almost always follow the same pattern: a group of final fields, a constructor assigning all of them with this., and often a super.key. This is one of the most natural fits for primary constructors — since the syntax only shortens the writing without changing runtime behavior, you can cut line count without any risk to your existing widgets' render behavior:

dart
1import 'package:flutter/material.dart';
2 
3// key is a named parameter; super.key also stays named.
4class ProfileCard({
5 super.key,
6 required final String name,
7 required final String avatarUrl,
8}) extends StatelessWidget {
9 @override
10 Widget build(BuildContext context) {
11 return Card(
12 child: ListTile(title: Text(name), leading: Image.network(avatarUrl)),
13 );
14 }
15}

Here super.key passes directly to the superclass (StatelessWidget), while name and avatarUrl produce fields as final declaring parameters — classic syntax would need three separate lines (final String name;, final String avatarUrl;, the constructor body) for this. Personally, I reach for primary constructors on simple, single-responsibility "dumb" widgets that just take parameters and render; for classes with state logic, complex initializers, or multiple named constructors, I find classic syntax more readable — the next section shows exactly where that line gets drawn.

The Single-Parameter Rule in Extension Types

Using a primary constructor in extension types comes with a different restriction than in regular classes. The documentation is explicit: "For extension types, the primary constructor must have exactly one parameter. This parameter is always a declaring parameter, even if you omit the modifier." So even without var/final, that single parameter is automatically a declaring parameter — the "no modifier means no field" rule doesn't apply here.

dart
1extension type UserId(int value) {
2 bool get isValid => value > 0;
3}

Here value becomes the field the extension type represents, despite no modifier being written. This fits extension types' philosophy of being "a lightweight wrapper around a single underlying type" — since they only ever take one parameter, it's a natural design requirement that it be a field.

There's another restriction for mixin classes: "Mixin classes can only have a primary constructor with no parameters, body, or initializer list." So a mixin's primary constructor, if present, must have no parameters, no body, and no initializer list.

Compatibility With Code-Gen Packages

Dart 3.13's compatibility with code-gen packages in the ecosystem isn't yet covered in detail by the official documentation — there's no comprehensive compatibility statement on dart.dev or flutter.dev. There's no documented restriction, but given the language rules, "no modifier means no field" and "declaring parameters in a const class must be final" could conflict with the signatures these packages auto-generate, since they typically build their own constructors with classic this.-based syntax. Practical recommendation: for code-gen classes (e.g., @freezed, @JsonSerializable models), check the relevant package's release notes before migrating to primary constructors for now — beyond the dart.dev page cited in this article's sources, there's no ecosystem-specific guarantee.

A Sibling Feature in the Same Release: Concise Constructors

Alongside primary constructors, Dart 3.13 also brought a related but different shortcut: concise constructors. Dart's constructor documentation summarizes it: "In Dart 3.13 and later, you can omit the class name when declaring a generative or factory constructor inside the class body by using the modifier new or factory directly." So instead of LongClassName.name() {} you write new name() {}, and instead of factory LongClassName.name() you write factory name(). Note: "concise named constructors don't use a dot between the keyword (modifier) and the name" — it's new name, not new.name. Our focus is the primary constructor, so we won't go deeper, but it's worth knowing it shipped in the same release and can be confused for it by name alone.

When Not to Use It, and a Migration Strategy

The restrictions that come with primary constructors also answer the question of "when you shouldn't use it":

  • Don't use it if you need multiple generative constructors. If a class has more than one "primary" way of being constructed (say a full second generative constructor like Point.origin alongside Point.fromJson), the primary constructor blocks this — a name doesn't bend the rule; only redirecting and factory constructors stay free.
  • Don't use it if you need async constructor logic. A primary constructor body doesn't support async, async*, sync*, or the => shorthand.
  • Don't use it if you need to reassign a parameter later. The documentation: "Assigning to them (such as with x = 5 or x++) in a field initializer...is a compile-time error." Declaring parameters are read-only inputs.
  • Don't use it if there's a method or field with the same name. "Declaring a parameter...with the same name as a method or another field...results in a compile-time error."
  • Don't use it if you need a late or external field. "The late and external modifiers aren't allowed on parameters in the primary constructor header."

For migration, the Dart team suggests a concrete path: on 3.12 or earlier, enable the avoid_final_parameters and var_with_no_type_annotation lint rules to find and clean up final/var on parameters "ahead of upgrading to Dart 3.13." Also, final on parameters (as in void printValue(final int x) => print(x);) now triggers an extraneous_modifier error; the fix: "remove the modifier or run dart fix". If immutable parameters are a style preference, use the parameter_assignments rule instead.

Modifier Behavior Summary Table

Syntax
Creates a Field?
Notes
var int x
Yes
Mutable instance variable
final int x
Yes
Read-only instance variable
int x (no modifier)
No (in a regular class)
Just a constructor parameter
int x (in an extension type)
Yes (always)
Modifier not required in extension types, the single parameter is always a declaring parameter
late var int x
Compile-time error
late/external are forbidden on primary constructor parameters

Common Compile Errors Summary Table

Scenario
Result
Source Rule
Primary constructor + second generative constructor
Compile-time error
Non-redirecting in-body constructors are forbidden
const primary constructor + { } body block
Compile-time error
Body block is an error in a const constructor, even if empty
Assigning to a declaring parameter (x = 5)
Compile-time error
Assigning to a parameter is forbidden
Declaring parameter name collides with an existing method
Compile-time error
Name collision
Declaring the same field and also assigning it in the initializer list
Compile-time error
Double initialization is forbidden
late/external on a parameter
Compile-time error
These modifiers are forbidden in the header

GOLDEN TIP

The most valuable insight in this article

This tip holds the article's most important takeaway.

Easter Egg

You found a hidden gem!

There's a hidden detail in this section. Want to uncover it?

Reader Reward

The migration checklist is easy to lose track of when moving to primary constructors — especially when gradually converting an existing codebase, deciding which class is a good candidate takes time. Run through the checklist below before moving a class to a primary constructor; once every item is checked, the migration is safe.

FAQ

How do you write a Dart primary constructor?

Write a parameter list in parentheses right after the class name, e.g. class Point(var int x, var int y);. That single line defines both the fields and the constructor. It's enabled by default in Dart 3.13 and later — no flag needed, just a pubspec SDK lower bound of 3.13 or higher.

What's the difference between a primary constructor and a traditional constructor?

Traditionally you declare the fields in the class body, then reassign them inside the constructor with Point(this.x, this.y) — a primary constructor collapses that repetition into one line. Important restriction: a class with a primary constructor can't define another non-redirecting generative constructor; additional "in-body constructors" are subject to this rule.

Does a parameter without var/final produce a field?

No. Only parameters declared with var or final (a "declaring parameter") automatically create an instance variable; without a modifier a parameter stays a plain constructor parameter. The one exception is extension types, where the single parameter always counts as a declaring parameter even with no modifier.

Do Freezed and json_serializable work with primary constructors?

Dart's official documentation doesn't give a detailed guarantee on this. Since code-gen packages generate their own constructor signatures with classic syntax, check the current release notes of the package you're using before migrating; no primary source verifiable within this article's scope confirms either way.

How do you use a primary constructor in const classes?

Write it as class const ClassName(final int x). What's forbidden isn't the class body, but the primary constructor's curly-brace body block — a compile-time error even if empty. The documentation's example has a class body: class const ConstPoint(final int x, final int y) { final int z; this : z = x + y; }. All fields must be final, must not be late, and must be definitely initialized.

When should I never migrate to a primary constructor?

If a class needs more than one generative constructor, async logic in the constructor body, or a parameter you reassign later, a primary constructor isn't a fit — the compiler blocks you directly in these scenarios.

Conclusion

The primary constructor is one of the rare Dart 3.13 features that genuinely shortens everyday code — but not "use it everywhere," since its restrictions (single generative constructor, async ban, body ban in const) force certain design decisions. For simple data classes, immutable value objects, and plain Flutter widgets it's a clear win; for classes with complex initializer logic or multiple construction paths, classic syntax stays safer. To go deeper on Dart's overall evolution and the key innovations before 3.13, see Dart 3 New Features and Modern Capabilities. Applying this syntax to the Flutter widget layer, I'd use the layer separation in Flutter Clean Architecture as a reference; on state management, Flutter State Management With Riverpod uses similar immutable model patterns. Primary constructors don't touch runtime — for real 60fps gains, Flutter Performance Optimization matters far more. And if you're curious about the rendering engine, see Flutter 4 Impeller: The New Rendering Engine.

Sources

Tags

#Dart#Dart 3.13#Flutter#primary constructor#declaring parameter#immutability#extension type
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

Lead Mobile Engineer with 12+ years of experience. Expert in iOS, Android and cross-platform architectures with Swift, SwiftUI, Kotlin and Flutter. I build performant, user-friendly mobile apps.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

We respect your privacy. You can unsubscribe at any time.

Share