Watch the video (12 min)
On this page23 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. The Old Problem
  5. 2. The New Form: {required this._balance}
  6. 3. The Caller Never Writes the Underscore
  7. 4. Defaults and Optional Parameters
  8. 5. Across Two Files
  9. 6. The Rules
  10. 7. It Needs Language Version 3.12
  11. 8. Language Experiments
  12. 9. Primary Constructors (Experimental)
  13. 10. Following What Is Coming
  14. 11. A Real-World Example
  15. Code Examples
  16. Common Beginner Mistakes
  17. Best Practices
  18. Mini Challenge
  19. Practice Questions
  20. Practice Answers
  21. Quick Revision
  22. Key Takeaways
  23. What's Next?

Lecture 131 — Private Named Parameters & Emerging Dart Features

Category22 — Modern Dart Features
Lecture131 of 140
Estimated video length12 min
TargetDart 3.12.2
LevelAdvanced

A private field and a named parameter used to clash: this._balance could not be named, so every class repeated itself in an initializer list. Dart 3.12 lets {required this._balance} work, and the caller simply writes balance:.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.


Prerequisites

Students should already understand, from Lectures 054–057, 070, 102, 113 and 128:

  • constructors with this.field, required named parameters and initializer lists
  • private names with a leading _, and that privacy is per file
  • extension types and their primary constructor (Lecture 070)
  • running programs with dart run and its flags
  • language versions and the // @dart= comment (Lecture 128)

Connection With Previous Lectures

In Lecture 057 we made fields private, and in Lecture 054 we gave constructors required named parameters. Put the two together and the obvious code is:

dart
class Account {
  final int _balance;
  Account({required this._balance});
}

For most of Dart's history, that line was an error, so every class with private fields wrote the same name twice. Dart 3.12 removes that repetition. This lecture shows the new rule, then looks further ahead: how Dart tries out features that are not finished yet, and how you can follow what is coming.


Learning Objectives

By the end of this lecture you will be able to:

  • Explain why private named parameters were impossible before Dart 3.12
  • Declare a private named parameter with {required this._name}
  • Call it with the public name, without the underscore
  • Recognize the rules: initializing formals only, a public name must exist, no clashes
  • Identify the language version a private named parameter needs
  • Explain what a language experiment is and enable one with --enable-experiment
  • Try the experimental primary constructors for classes
  • Find out what is changing in Dart, and when

1. The Old Problem

A named argument's name is part of how callers use a class. A name such as _balance is private to its file, so another file could never write _balance: 500. Dart therefore refused private named parameters, and the workaround was a public parameter copied into the private field:

dart
class Account {
  final String owner;
  final int _balance;

  Account({required this.owner, required int balance}) : _balance = balance;

  int get balance => _balance;
}

void main() {
  var account = Account(owner: 'Ali', balance: 500);
  print('${account.owner}: ${account.balance}');
}
output
Ali: 500

It works, but balance appears three times in one line: the parameter, the type, and the initializer list.


2. The New Form: {required this._balance}

Since Dart 3.12, an initializing formal (this.field) may name a private field inside {}. Dart gives the parameter the public name — the field's name without the underscore:

dart
class Account {
  final String owner;
  final int _balance;

  Account({required this.owner, required this._balance});

  int get balance => _balance;
}

void main() {
  var account = Account(owner: 'Ali', balance: 500);
  print('${account.owner}: ${account.balance}');
}
output
Ali: 500

The field is still private. The caller writes balance:, never _balance:. Same behaviour as section 1, with no initializer list.


3. The Caller Never Writes the Underscore

The argument name is always the public one, even in the same file:

dart
class Account {
  final int _balance;
  Account({required this._balance});
}

void main() {
  Account(_balance: 500);
}
output
Error: No named parameter with the name '_balance'.
  Account(_balance: 500);
          ^^^^^^^^

Read it as: the constructor has a parameter named balance that fills the field _balance.


4. Defaults and Optional Parameters

Private named parameters work like any other named parameter. Leave out required and give a default:

dart
class Student {
  final String _name;
  final int _marks;

  Student({required this._name, this._marks = 0});

  String get report => '$_name: $_marks';
}

void main() {
  print(Student(name: 'Sara').report);
  print(Student(name: 'Zayan', marks: 72).report);
}
output
Sara: 0
Zayan: 72

Inside the constructor's initializer list and body, the parameter is still called _marks.


5. Across Two Files

The feature matters most when the class lives in its own library, as in Lecture 102.

account.dart

dart
class Account {
  final int _balance;
  Account({required this._balance});

  int get balance => _balance;
}

main.dart

dart
import 'account.dart';

void main() {
  var account = Account(balance: 900);
  print(account.balance);
}
output
900

main.dart can create the object with balance:, but account._balance would still be "isn't defined" there, exactly as in Lecture 057. Public to call, private to touch.


6. The Rules

Three rules keep the feature simple.

Only this._field may be private. A plain named parameter still cannot start with _:

dart
class Account {
  Account({required int _balance});
}
output
Error: A named parameter that doesn't refer to an instance variable can't start with an underscore ('_').
  Account({required int _balance});
                        ^^^^^^^^

A public name must exist. Removing the _ has to leave a valid name. _1, __x and _ fail:

dart
class Report {
  final int _1;
  Report({required this._1});
}
output
Error: A private named parameter must have a corresponding public name.
  Report({required this._1});
                        ^^

The public name must be free. Two parameters cannot end up with the same name:

dart
class Account {
  final int _balance;
  Account({required this._balance, required int balance});
}
output
Error: The corresponding public name 'balance' is already the name of another parameter.
  Account({required this._balance, required int balance});
                         ^^^^^^^^

A subclass passes the value on with the public name, using a super parameter (Lecture 061): Savings({required super.balance});.


7. It Needs Language Version 3.12

A feature arrives with a language version (Lecture 128). Opt a file out with // @dart=3.11 and the old rule returns:

dart
// @dart=3.11
class Account {
  final int _balance;
  Account({required this._balance});
}

void main() {}
output
Error: The 'private-named-parameters' language feature is disabled for this library.
Try removing the `@dart=` annotation or setting the language version to 3.12 or higher.
  Account({required this._balance});
                         ^^^^^^^^

In a package, the language version comes from environment: sdk: in pubspec.yaml, so a package needs sdk: ^3.12.0 to use this. The message also names the feature: private-named-parameters. That is its experiment flag name.


8. Language Experiments

Before a feature ships, the Dart team builds it behind a flag so people can try it. Such a feature is an experiment. You turn one on for a single run with --enable-experiment:

console
$ dart run --enable-experiment=bogus main.dart
Unknown experiment: bogus

A shipped feature needs no flag, and Dart says so:

console
$ dart run --enable-experiment=private-named-parameters main.dart
'private-named-parameters' is now enabled by default; this flag is no longer required.
'private-named-parameters' is now enabled by default; this flag is no longer required.
Ali: 500

The program (section 2's Account) still runs; the message is only a reminder.

private-named-parameters was an experiment until Dart 3.12. Others are still waiting.

Experimental means experimental. Syntax behind a flag can change or disappear in any release. Never put it in code you ship or share.


9. Primary Constructors (Experimental)

In Lecture 070, extension type Marks(int value) declared its field and constructor in one go. That is a primary constructor. Try one on a class in Dart 3.12:

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

void main() {
  var p = Point(3, 4);
  print('${p.x}, ${p.y}');
}
output
Error: This requires the experimental 'primary-constructors' language feature to be enabled.
Try passing the '--enable-experiment=primary-constructors' command line option.
class Point(final int x, final int y);
           ^

The error names the experiment. With the flag, the same file runs:

console
$ dart run --enable-experiment=primary-constructors main.dart
3, 4

final int x in the header declares a field and a parameter. A body is optional — ; ends an empty one. Named and private named parameters work too:

dart
class Account(final String owner, {required final int _balance}) {
  int get balance => _balance;
}

void main() {
  var account = Account('Sara', balance: 500);
  print('${account.owner}: ${account.balance}');
}
console
$ dart run --enable-experiment=primary-constructors main.dart
Sara: 500

These examples run on Dart 3.12.2 with the flag — the details may change before the feature ships. dart analyze needs the experiment listed in analysis_options.yaml too:

yaml
analyzer:
  enable-experiment:
    - primary-constructors

10. Following What Is Coming

Dart changes every few months. Four places tell you what changed and what is planned:

  • The SDK changelog lists every release's language and library changes. Read it before you raise sdk: in pubspec.yaml.
  • The language page on dart.dev describes each shipped feature and the version that added it.
  • The dart-lang/language project on GitHub holds the proposals. Each feature is discussed there in public before it is built — primary constructors and private named parameters both started there.
  • The tools themselves. Errors that say "experimental" name the flag, and "now enabled by default" tells you an experiment has shipped.

Use experiments to learn; use stable versions to build.


11. A Real-World Example

A shop's customer account, written with stable Dart 3.12 only — private fields, public named arguments, and validating methods from Lecture 057:

dart
class CustomerAccount {
  final String _owner;
  int _balance;
  final int _limit;

  CustomerAccount({required this._owner, int balance = 0, this._limit = 5000})
    : _balance = balance;

  void buy(String item, int price) {
    if (_balance + price > _limit) {
      print('$_owner cannot buy $item: limit $_limit');
      return;
    }
    _balance += price;
    print('$_owner bought $item for $price');
  }

  String get summary => '$_owner owes $_balance of $_limit';
}

void main() {
  var sara = CustomerAccount(owner: 'Sara', limit: 3000);
  sara.buy('Shoes', 2500);
  sara.buy('Bag', 1200);
  print(sara.summary);
}
output
Sara bought Shoes for 2500
Sara cannot buy Bag: limit 3000
Sara owes 2500 of 3000

_owner and _limit use the new form. _balance keeps an ordinary balance parameter and an initializer list, to show that both styles mix freely in one constructor.


Code Examples

Example 1 — The Old Initializer List

dart
class Student {
  final String _name;
  Student({required String name}) : _name = name;
  String get name => _name;
}

void main() {
  print(Student(name: 'Ali').name);
}
output
Ali

Example 2 — The Same Class, New Form

dart
class Student {
  final String _name;
  Student({required this._name});
  String get name => _name;
}

void main() {
  print(Student(name: 'Ali').name);
}
output
Ali

Example 3 — An Optional Private Parameter

dart
class Order {
  final int _items;
  Order({this._items = 1});
  int get items => _items;
}

void main() {
  print(Order().items);
  print(Order(items: 7).items);
}
output
1
7

Example 4 — Public and Private Side by Side

dart
class Book {
  final String title;
  final int _pages;
  Book({required this.title, required this._pages});
  String get info => '$title ($_pages pages)';
}

void main() {
  print(Book(title: 'Dart Basics', pages: 240).info);
}
output
Dart Basics (240 pages)

Example 5 — Using the Parameter in the Initializer List

dart
class Square {
  final int _side;
  final int area;
  Square({required this._side}) : area = _side * _side;
  int get side => _side;
}

void main() {
  var square = Square(side: 6);
  print('${square.side} x ${square.side} = ${square.area}');
}
output
6 x 6 = 36

Example 6 — Passing It to a Superclass

dart
class Account {
  final int _balance;
  Account({required this._balance});
  int get balance => _balance;
}

class Savings extends Account {
  Savings({required super.balance});
}

void main() {
  print(Savings(balance: 1200).balance);
}
output
1200

How it works

  1. Account's constructor has a parameter named balance that fills _balance.
  2. super.balance forwards Savings's balance argument to it, by its public name.

Example 7 — Student Marks

dart
class MarkSheet {
  final String _student;
  final List<int> _marks;
  MarkSheet({required this._student, required this._marks});

  int get total => _marks.reduce((a, b) => a + b);
  String get report => '$_student: $total';
}

void main() {
  print(MarkSheet(student: 'Zayan', marks: [80, 75, 85]).report);
}
output
Zayan: 240

Example 8 — Shopping Total

dart
class CartItem {
  final String _product;
  final int _price;
  final int _quantity;
  CartItem({required this._product, required this._price, this._quantity = 1});

  int get total => _price * _quantity;
  String get line => '$_product x$_quantity = $total';
}

void main() {
  print(CartItem(product: 'Notebook', price: 500, quantity: 3).line);
  print(CartItem(product: 'Pen', price: 40).line);
}
output
Notebook x3 = 1500
Pen x1 = 40

Example 9 — Bank Balance

dart
class BankAccount {
  final String _owner;
  double _balance;
  BankAccount({required this._owner, this._balance = 0});

  void deposit(double amount) {
    if (amount > 0) _balance += amount;
  }

  String get summary => '$_owner: $_balance';
}

void main() {
  var account = BankAccount(owner: 'Sara', balance: 1000);
  account.deposit(250.5);
  account.deposit(-40);
  print(account.summary);
}
output
Sara: 1250.5

How it works

  1. balance: 1000 fills the private _balance; nobody outside the file can change it directly.
  2. deposit ignores the negative amount, so the balance stays correct.

Example 10 — Temperature (Experimental Primary Constructor)

dart
class Temperature(final double celsius) {
  double get fahrenheit => celsius * 9 / 5 + 32;
}

void main() {
  var today = Temperature(25.0);
  print('${today.celsius} C = ${today.fahrenheit} F');
}
console
$ dart run --enable-experiment=primary-constructors main.dart
25.0 C = 77.0 F

Without the flag this file does not compile. Treat it as a preview only.


Common Beginner Mistakes

1. Calling with the underscore

dart
// Account(_balance: 500); // ✗
output
Error: No named parameter with the name '_balance'.

Write Account(balance: 500).

2. Making a plain named parameter private

dart
class Account {
  // Account({required int _balance}); // ✗
}
output
Error: A named parameter that doesn't refer to an instance variable can't start with an underscore ('_').

Only this._field may be private. A plain parameter gets a public name.

3. Choosing a field name with no public form

dart
class Report {
  final int _1;
  // Report({required this._1}); // ✗
  Report(this._1);
}
output
Error: A private named parameter must have a corresponding public name.

Rename the field (_total) or keep it positional.

4. Using it in a package with an old SDK constraint

dart
// @dart=3.11
class Account {
  final int _balance;
  // Account({required this._balance}); // ✗
  Account(this._balance);
}
output
Error: The 'private-named-parameters' language feature is disabled for this library.

Set sdk: ^3.12.0 in pubspec.yaml (or remove the // @dart= comment).

5. Shipping experimental code

A program that only runs with --enable-experiment is not finished Dart. Users, tests and dart compile all need the same flag, and the syntax may change in the next release.


Best Practices

  • Use {required this._field} instead of a copying initializer list when the value goes straight into the field.
  • Keep the initializer list when the parameter must be converted or checked first.
  • Give private fields names that read well without the _ — callers see that name.
  • Raise sdk: to ^3.12.0 before using the feature in a package.
  • Try experiments in a scratch project, never in shared code.
  • Read the SDK changelog before upgrading, and watch for "now enabled by default".

Mini Challenge

Rewrite this class with private named parameters, keeping the output the same:

dart
class Ticket {
  final String _event;
  final int _seats;
  Ticket({required String event, int seats = 1})
    : _event = event,
      _seats = seats;
  String get info => '$_event: $_seats seat(s)';
}

void main() {
  print(Ticket(event: 'Concert', seats: 2).info);
  print(Ticket(event: 'Museum').info);
}
output
Concert: 2 seat(s)
Museum: 1 seat(s)

Try it yourself before opening the solution.

Show solution
dart
class Ticket {
  final String _event;
  final int _seats;
  Ticket({required this._event, this._seats = 1});
  String get info => '$_event: $_seats seat(s)';
}

void main() {
  print(Ticket(event: 'Concert', seats: 2).info);
  print(Ticket(event: 'Museum').info);
}
output
Concert: 2 seat(s)
Museum: 1 seat(s)

How it works

  1. this._event and this._seats fill the fields directly; the initializer list is gone.
  2. Callers still write event: and seats:, so main() does not change at all.

Practice Questions

Work through these in order — they run from beginner to application to challenge.

Beginner

  1. Write a class City with a private field _name and a constructor {required this._name}. Create City(name: 'Lahore') and print the name through a getter.
  2. What is the argument name for the parameter this._price?
  3. Which Dart language version first allows private named parameters?

Application

  1. Write Player with _name (required) and _score (default 0), both private named parameters, and a getter info. Print Ali: 0 and Sara: 90.
  2. Why is Box({required int _size}) an error, while Box({required this._size}) is not?
  3. Rewrite Pet({required String name}) : _name = name; with the new form.
  4. What does dart run --enable-experiment=primary-constructors main.dart do that dart run main.dart does not?

Challenge

  1. A class has a field _id. Its constructor is {required this._id, required int id}. Will it compile? Explain.
  2. Write Product with private named _name and _price, and a subclass Sale that forwards both with super parameters and adds a required discount. For Sale(name: 'Bag', price: 1500, discount: 300), print Bag: 1200.
  3. Your teammate wants to use primary constructors in the shop's app. Give two reasons to wait.

Practice Answers

Question 1 — Solution

dart
class City {
  final String _name;
  City({required this._name});
  String get name => _name;
}

void main() {
  print(City(name: 'Lahore').name);
}
output
Lahore

Question 2 — Solution

price — the field name without the leading underscore.

Question 3 — Solution

Dart 3.12. An older language version reports "The 'private-named-parameters' language feature is disabled for this library."

Question 4 — Solution

dart
class Player {
  final String _name;
  final int _score;
  Player({required this._name, this._score = 0});
  String get info => '$_name: $_score';
}

void main() {
  print(Player(name: 'Ali').info);
  print(Player(name: 'Sara', score: 90).info);
}
output
Ali: 0
Sara: 90

Question 5 — Solution

int _size is a plain parameter, which is not allowed to be private: "A named parameter that doesn't refer to an instance variable can't start with an underscore". this._size fills a field, so Dart can give it the public name size.

Question 6 — Solution

dart
class Pet {
  final String _name;
  Pet({required this._name});
  String get name => _name;
}

void main() {
  print(Pet(name: 'Milo').name);
}
output
Milo

Question 7 — Solution

It turns on the unfinished primary-constructors feature for that one run, so a class header like class Point(final int x, final int y); compiles. Without the flag, Dart reports that the feature is experimental.

Question 8 — Solution

No. this._id gets the public name id, and another parameter is already called id: "The corresponding public name 'id' is already the name of another parameter."

Question 9 — Solution

dart
class Product {
  final String _name;
  final int _price;
  Product({required this._name, required this._price});
  String get name => _name;
  int get price => _price;
}

class Sale extends Product {
  final int discount;
  Sale({required super.name, required super.price, required this.discount});
  int get finalPrice => price - discount;
}

void main() {
  var bag = Sale(name: 'Bag', price: 1500, discount: 300);
  print('${bag.name}: ${bag.finalPrice}');
}
output
Bag: 1200

Question 10 — Solution

The syntax is experimental and may change before it ships, breaking the code. And everyone — tests, builds, teammates — would need --enable-experiment and an analysis_options.yaml entry just to compile it.


Quick Revision

  1. Why were private named parameters an error before Dart 3.12?
  2. What did people write instead?
  3. What does a caller write for {required this._total}?
  4. Name the three rules for private named parameters.
  5. What is a language experiment?
  6. Which flag turns one on, and what does Dart say for a flag that has shipped?
  7. Where can you read about Dart features before and after they ship?

Key Takeaways

  • Since Dart 3.12, {required this._field} is allowed; the argument name drops the _.
  • Callers write field:; writing _field: is "No named parameter with the name".
  • Only initializing formals may be private, the public name must be valid, and it must not clash with another parameter.
  • Older language versions report the private-named-parameters feature as disabled.
  • Unfinished features are experiments, enabled per run with --enable-experiment=<name>.
  • Primary constructors (class Point(final int x, final int y);) are experimental in 3.12 — try them, don't ship them.
  • The SDK changelog, dart.dev's language pages and the language proposals keep you current.

What's Next?

Lecture 132 — Project Planning. This lecture closes Modern Dart Features — and with it, every language topic in the course. Next we start the final project: we will plan a complete Dart application, deciding its features, its files and its classes before writing a line, so everything you have learned comes together in one program.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.