Skip to content

Instantly share code, notes, and snippets.

@bizz84
Created June 10, 2026 16:03
Show Gist options
  • Select an option

  • Save bizz84/af998ce0e02177d6db141e0a28fd642c to your computer and use it in GitHub Desktop.

Select an option

Save bizz84/af998ce0e02177d6db141e0a28fd642c to your computer and use it in GitHub Desktop.
Migrate Dart classes and enums to the experimental primary constructors syntax. Use when asked to migrate Dart constructor boilerplate to primary constructors.
name act-dart-migrate-primary-constructors
description Migrate Dart classes and enums to the experimental primary constructors syntax. Use when asked to migrate Dart constructor boilerplate to primary constructors.
tools
Read
Glob
Grep
Edit
Bash
Task

Migrate the current package to Dart primary constructors.

Terminology

Use these definitions throughout the migration:

  • Primary constructors are the experimental Dart class and enum declaration feature that lets a declaration put its main constructor and field-inducing parameters in the declaration header. This skill targets the pre-stable experiment path: projects must use SDK >=3.12.0 and enable the primary-constructors analyzer experiment before migration can proceed.
  • Declaring parameters are primary-constructor parameters marked with final or var that induce instance fields. Preserve existing field mutability: original final fields become final declaring parameters and original mutable fields become var declaring parameters.
  • Non-redirecting generative constructors create and initialize a new instance directly. A class or enum with a primary constructor cannot also declare another non-redirecting generative constructor, but it can keep redirecting generative constructors and factory constructors.
  • Constructor declaration shorthand is syntax for constructors declared inside a class body. Generative constructors can use new instead of repeating the class name, such as new(), new withName(...), or const new(...).

Prerequisites

IMPORTANT: Before starting the migration, verify all prerequisites. If any prerequisite is not met, abort immediately. Do not edit pubspec.yaml, analysis_options.yaml, or any other setup file as part of this skill.

The user is responsible for version-control safety. Do not run git commands. Before migration, tell the user to ensure the worktree is in a state where they can review or revert changes.

SDK constraint

Verify that pubspec.yaml has an SDK constraint that allows Dart >=3.12.0.

Accept constraints such as:

environment:
  sdk: '>=3.12.0 <4.0.0'
environment:
  sdk: ^3.12.0

If the SDK constraint does not allow Dart >=3.12.0, abort and tell the user to update it.

Installed toolchain

Detect project type from pubspec.yaml.

  • Flutter projects: verify installed Flutter is >=3.44.0 using flutter --version.
  • Pure Dart packages: verify installed Dart is >=3.12.0 using dart --version.

If the installed toolchain is too old, abort and report the required version.

Analyzer experiment

Verify that analysis_options.yaml exists and enables the primary-constructors analyzer experiment:

analyzer:
  enable-experiment:
    - primary-constructors

The experiment may coexist with other analyzer settings and other experiments.

If analysis_options.yaml is missing, or the experiment is not enabled, abort and tell the user to add the block above before rerunning the migration.

Pre-migration analysis

Run analysis before editing Dart files:

  • Flutter projects: flutter analyze
  • Pure Dart packages: dart analyze

If pre-migration analysis fails, abort. Do not start the migration until the project is already analyzer-clean.

Scope

  • Work in the current package root containing pubspec.yaml.
  • Do not recursively migrate nested packages unless the user invokes the skill from that package.
  • Include all non-generated *.dart files in the package, such as files under lib/, test/, bin/, example/, and tool/ when present.
  • Skip generated files, including *.g.dart, *.freezed.dart, *.mocks.dart, generated localization files, and any file with an obvious generated marker such as // GENERATED CODE - DO NOT MODIFY.
  • Migrate ordinary classes and enums only.
  • Skip extension types, extensions, mixins, and mixin classes.

Discovery and Subagents

Launch the standard explore subagent during discovery by default. Ask it to perform read-only candidate discovery for primary-constructor migration: identify in-scope non-generated Dart files and likely class/enum migration candidates.

Do not launch act-codebase-researcher, act-flutter-patterns-researcher, or other architecture/planning agents. Primary-constructor migration is a mechanical Dart syntax migration, not a feature implementation task.

Use local Glob, Grep, and Read for prerequisite checks, validating explore findings, and targeted follow-up inspection.

The top-level agent remains responsible for migration decisions, edits, formatting, analysis, tests, and the final report.

Primary Constructors Syntax

Primary constructors let classes and enums declare their main constructor in the declaration header. Parameters marked with final or var are declaring parameters and induce instance fields.

class Point {
  int x;
  int y;

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

Migrates to:

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

For existing final fields, use final declaring parameters. For existing mutable fields, use var declaring parameters. Always preserve original field mutability.

class User {
  final String id;
  String name;

  User(this.id, this.name);
}

Migrates to:

class User(final String id, var String name);

Migration Rules

Preserve constructor API shape

Never change the constructor's call-site API shape during migration. Preserve the original constructor parameter shape exactly:

  • Positional parameters stay positional.
  • Optional positional parameters stay optional positional.
  • Named parameters stay named and remain inside {...}.
  • required stays required.
  • Optional/default values stay unchanged.
  • Parameter order stays unchanged within its original positional or named group.

Do not convert named constructor parameters into positional primary-constructor parameters. This applies to classes and enums.

Constructors to migrate

For primary-constructor header migration of classes and enums, migrate only when the constructor being moved into the declaration header is the single unnamed, non-redirecting generative constructor.

The single unnamed constructor may be zero-argument. A constructor does not need field-formal or declaring parameters to be eligible.

The declaration may keep:

  • Redirecting generative constructors
  • Factory constructors
  • Getters
  • Methods
  • Static members

Do not skip a declaration merely because it has factory constructors. Factory constructors may remain in the class or enum body when the declaration has a single unnamed, non-redirecting generative constructor that can move to the primary-constructor header. Evaluate generative constructors and factory constructors separately.

Valid class migration with a redirecting constructor left in place:

class Point {
  final int x;
  final int y;

  Point(this.x, this.y);
  Point.zero() : this(0, 0);
}

Migrates to:

class Point(final int x, final int y) {
  Point.zero() : this(0, 0);
}

Skip direct named generative initialization for primary-constructor header migration:

class Point {
  final int x;
  final int y;

  Point.zero() : x = 0, y = 0;
}

Field-formal parameters

Migrate constructors whose parameters directly initialize fields with field-formal parameters.

A class may still be migrated when it has other fields, getters, methods, overrides, or interface clauses. Remove only the fields that become declaring parameters; leave all other members unchanged.

Preserve:

  • Positional parameters
  • Optional positional parameters
  • Named parameters
  • required
  • Default values
  • Nullability
  • Class type parameters and bounds
  • Class modifiers and clauses such as base, final, interface, sealed, abstract, extends, implements, and with

Preserve field comments when moving fields to declaring parameters. If comments are directly attached to a field and can be moved unchanged, move them immediately above the corresponding declaring parameter and keep the primary constructor parameter list multiline.

Supported comments include //, ///, /* ... */, and multi-line block comments, as long as the full comment can be moved unchanged and its attachment to the field is unambiguous. Skip if comments are trailing, separated from the field by blank lines, interleaved with other declarations, or otherwise ambiguous.

Examples:

class Box<T extends Object?> {
  final T value;

  Box(this.value);
}

Migrates to:

class Box<T extends Object?>(final T value);
class User {
  final String id;
  String? nickname;

  User(this.id, {this.nickname = 'Guest'});
}

Migrates to:

class User(final String id, {var String? nickname = 'Guest'});

Const constructors

Preserve const when migrating eligible class constructors. Primary constructor syntax places const between the class keyword and the declaration name:

class InvestmentTypeColors {
  const InvestmentTypeColors({
    required this.color,
    required this.onColor,
  });

  final Color color;
  final Color onColor;
}

Migrates to:

class const InvestmentTypeColors({
  required final Color color,
  required final Color onColor,
});

For class declarations with modifiers or clauses, preserve the existing declaration shape and insert const immediately after class:

class PortfolioTrackerApp extends StatelessWidget {
  const PortfolioTrackerApp({super.key});
}

Migrates to:

class const PortfolioTrackerApp({super.key}) extends StatelessWidget;

Do not remove or adjust const at call sites. A migrated const primary constructor remains valid in const contexts, annotations, default parameter values, Flutter widget constructors, and other immutable-class use cases.

Zero-argument constructors are eligible too. Const zero-argument migrations keep the empty (), while non-const zero-argument migrations omit it. If the final const body is empty, use class const Noop();.

class CurrencyConverter {
  const CurrencyConverter();

  double convert(...) => ...;
}

Migrates to:

class const CurrencyConverter() {
  double convert(...) => ...;
}

Private fields

Migrate private fields initialized by field-formal parameters.

class User {
  final String _id;

  const User(this._id);
}

Migrates to:

class const User(final String _id);

For public named parameters that initialize private fields with the same name minus the leading underscore, migrate to private named declaring parameters.

class User {
  final String _id;

  const User({required String id}) : _id = id;
}

Migrates to:

class const User({required final String _id});

The call site remains User(id: ...).

Only apply this rule when the parameter name and private field name differ solely by the leading underscore. Skip if validation, transformation, or more complex mapping is involved.

Ordinary parameters and field initializers

Migrate ordinary constructor parameters that are used only by supported initializer-list field assignments. Ordinary parameters must not receive final or var, because they do not induce fields.

class Session {
  final String id;
  final DateTime expiresAt;

  Session({
    required this.id,
    required DateTime issuedAt,
    required Duration ttl,
  }) : expiresAt = issuedAt.add(ttl);
}

Migrates to:

class Session({
  required final String id,
  required DateTime issuedAt,
  required Duration ttl,
}) {
  final DateTime expiresAt = issuedAt.add(ttl);
}

Only move initializer-list field assignments when:

  • The target field is declared in the class body.
  • The initializer expression can move directly to that field declaration.
  • The target field name does not collide with a non-declaring parameter name.
  • Initialization ordering and dependencies remain clear.

Skip if any of these conditions is unclear.

Assertions and constructor bodies

Migrate initializer-list assert(...) entries to the primary constructor initializer list.

class Point {
  final int x;
  final int y;

  const Point(this.x, this.y) : assert(x >= 0), assert(y >= 0);
}

Migrates to:

class const Point(final int x, final int y) {
  this : assert(x >= 0), assert(y >= 0);
}

Migrate non-empty constructor bodies by moving the body verbatim into the primary constructor body.

class Point {
  final int x;
  final int y;

  Point(this.x, this.y) {
    print('Point initialized at ($x, $y)');
  }
}

Migrates to:

class Point(final int x, final int y) {
  this {
    print('Point initialized at ($x, $y)');
  }
}

Super initialization

Migrate unnamed super(...) initializer calls to the primary constructor initializer list.

class Shape {
  final String label;

  Shape(String prefix, int id) : label = '$prefix-$id';
}

class Circle extends Shape {
  final int radius;
  final int diameter;

  Circle(this.radius, int scale)
      : diameter = radius * 2,
        assert(scale > 0),
        super('circle', scale);
}

Migrates to:

class Circle(final int radius, int scale) extends Shape {
  final int diameter = radius * 2;

  this : assert(scale > 0), super('circle', scale);
}

Preserve assert(...) and unnamed super(...) entries in the same relative order in the this : ... initializer list. Move field assignments to their corresponding field declarations. If ordering or dependency semantics are unclear, skip the declaration.

Skip named super constructor initializers such as super.origin(...).

Preserve simple super parameters.

class A {
  final int a;

  const A(this.a);
}

class B extends A {
  const B(super.a);
}

Migrates to:

class const A(final int a);

class const B(super.a) extends A;

Constructor declaration shorthand

Constructor declaration shorthand is a fallback for generative constructors that must remain in a class body. This commonly happens when the public API uses named constructors, or when a class has multiple non-redirecting generative constructors and therefore cannot have a primary constructor. Always prefer primary-constructor header migration when it is allowed.

Prefer primary-constructor syntax for supported field-formal constructors, including named field-formal parameters:

class CurrencyRates {
  const CurrencyRates({required this.amount});

  final double amount;
}

Migrates to:

class const CurrencyRates({required final double amount});

Use new only when primary-constructor header migration is not allowed, such as named constructors that must remain part of the public API:

class Temperature {
  final double celsius;

  const Temperature.celsius(this.celsius);

  const Temperature.fahrenheit(double fahrenheit)
    : celsius = (fahrenheit - 32) * 5 / 9;
}

Migrates to:

class Temperature {
  final double celsius;

  const new celsius(this.celsius);

  const new fahrenheit(double fahrenheit)
    : celsius = (fahrenheit - 32) * 5 / 9;
}

Apply constructor declaration shorthand only when:

  • The declaration is an ordinary class.
  • The constructor is a generative constructor that must remain in the class body.
  • Moving the constructor to the declaration header would be invalid or would break the public constructor API.
  • The constructor is not being migrated into the primary-constructor header.
  • The constructor is not a factory constructor.
  • The constructor is not external.
  • If the constructor is const, preserve const as const new(...) or const new name(...).
  • The constructor and its parameters do not have metadata.
  • Existing initializer lists and constructor bodies can remain in place unchanged.
  • Existing class members can remain in place unchanged.
  • Comments do not need to move and rewriting the constructor name is unambiguous.

Do not apply constructor declaration shorthand to factory constructors unless the factory shorthand syntax is explicitly confirmed. Leave factory constructors unchanged for now.

Enums

Migrate simple enhanced enums with a single unnamed generative constructor.

enum ShippingSpeed {
  standard(5, 0),
  express(2, 1299),
  overnight(1, 2499);

  final int maxBusinessDays;
  final int surchargeCents;

  ShippingSpeed(this.maxBusinessDays, this.surchargeCents);

  factory ShippingSpeed.forDeliveryWindow(int maxBusinessDays) {
    return switch (maxBusinessDays) {
      <= 1 => ShippingSpeed.overnight,
      <= 2 => ShippingSpeed.express,
      _ => ShippingSpeed.standard,
    };
  }

  bool get hasSurcharge => surchargeCents > 0;
}

Migrates to:

enum ShippingSpeed(final int maxBusinessDays, final int surchargeCents) {
  standard(5, 0),
  express(2, 1299),
  overnight(1, 2499);

  factory ShippingSpeed.forDeliveryWindow(int maxBusinessDays) {
    return switch (maxBusinessDays) {
      <= 1 => ShippingSpeed.overnight,
      <= 2 => ShippingSpeed.express,
      _ => ShippingSpeed.standard,
    };
  }

  bool get hasSurcharge => surchargeCents > 0;
}

Factory constructors, getters, methods, and static members may remain in the enum body. Skip enums with more than one non-redirecting generative constructor or unsupported initializer logic.

When migrating enums, preserve the original enum constructor parameter shape exactly. If enum values currently use named arguments, the primary constructor must use named parameters and enum values must keep their named arguments unchanged.

Enum initializer-list field assignments are supported under the same safety rules as classes: the target field must be declared in the enum body, the initializer expression must be movable directly to that field declaration, and initialization ordering and dependencies must remain clear. Preserve initializer-list assert(...) entries when migrating the enum constructor.

Migrate non-empty enum constructor bodies by moving the body verbatim into the primary constructor body only when the body is not responsible for field initialization. Skip assignment-in-body field initialization and any enum initializer or body migration whose ordering, dependency, or side-effect semantics are unclear.

Empty bodies

When the final ordinary class body is empty, use the semicolon form rather than {}. Preserve modifiers and clauses. Apply this after other migrations for the declaration, because constructor and field removals may make the body empty. Skip empty-body collapse when the body contains comments, because those comments may be meaningful.

sealed class DatabaseIOResult {}

class DatabaseIOCancelled extends DatabaseIOResult {}

Migrates to:

sealed class DatabaseIOResult;

class DatabaseIOCancelled extends DatabaseIOResult;

Skip Rules

Abort the migration before editing files if any of these apply:

  • Missing prerequisites or failing pre-migration analysis.

Skip primary-constructor header migration for a declaration if any of these apply:

  • The declaration is not an ordinary class or enum.
  • The declaration has more than one non-redirecting generative constructor.
  • The constructor to move into the declaration header is not the single unnamed, non-redirecting generative constructor.
  • The constructor is external.
  • A field that would become a declaring parameter is late or external.
  • The constructor or any parameter has metadata.
  • Field comments cannot be moved unchanged or their attachment is ambiguous.
  • The constructor uses assignment-in-body field initialization.
  • The constructor invokes a named super constructor such as super.origin(...).
  • The initializer list contains unsupported entries or unclear ordering/dependencies.
  • The migration would require changing field mutability, nullability, default values, or call-site API.

Constructor declaration shorthand may still apply to ordinary class body constructors that are skipped for primary-constructor header migration, as long as the shorthand rule is satisfied.

Skip constructor declaration shorthand if any of these apply:

  • The declaration is not an ordinary class.
  • The constructor is a factory constructor.
  • The constructor is external.
  • The constructor or any parameter has metadata.
  • Comments would need to be moved or reattached.
  • Rewriting the constructor name would be ambiguous.

Process

  1. Tell the user to ensure their worktree can be reviewed or reverted. Do not run git commands.
  2. Locate the current package root containing pubspec.yaml.
  3. Verify the SDK constraint allows Dart >=3.12.0.
  4. Detect whether the package is Flutter or pure Dart.
  5. Verify the installed Flutter or Dart toolchain version.
  6. Verify analysis_options.yaml enables primary-constructors.
  7. Run pre-migration analysis and abort on failure.
  8. Scan all in-scope non-generated Dart files.
  9. Apply all supported migrations in one pass: class primary constructors, enum primary constructors, empty-body semicolon migrations, and constructor declaration shorthand.
  10. Run dart format --enable-experiment=primary-constructors on changed Dart files only.
  11. Run post-migration analysis.
  12. Fix migration-caused analyzer issues in changed files only.
  13. Run tests after analysis passes, if tests exist.
  14. Produce a detailed report.

Use these verification commands:

  • Flutter projects: flutter analyze, then flutter test --enable-experiment=primary-constructors --no-pub when tests exist.
  • Pure Dart packages: dart analyze, then dart test --enable-experiment=primary-constructors when tests exist.

Use this format command for changed Dart files:

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

If post-migration analysis or tests fail, report the failure. Do not undo edits automatically and do not chase unrelated issues outside changed files.

Report Format

## Summary of Changes

Migrated declarations: X
Changed files: W

[List migrated declarations by file and declaration name]

## Skipped Declarations

[List skipped declarations by file and declaration name, with reasons]

## Verification

Pre-migration analysis: ✅/❌ [command]
Format changed files: ✅/❌ [command]
Post-migration analysis: ✅/❌ [command]
Tests: ✅/❌/Not run [command or reason]

## Notes

[Mention any residual risks, failed checks, or user follow-up.]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment