Watch the video (11 min)
On this page25 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is a Constructor?
  5. 2. The Default Constructor
  6. 3. Writing Your Own Constructor
  7. 4. The Body Is Too Late
  8. 5. Initializing Formals: this.field
  9. 6. Your Constructor Replaces the Default One
  10. 7. Named Parameters and required
  11. 8. Optional Parameters and Default Values
  12. 9. final Fields Set by the Constructor
  13. 10. Initializer Lists
  14. 11. Running Code in the Constructor Body
  15. 12. A Quick Look at assert
  16. 13. A Real-World Example
  17. Code Examples
  18. Common Beginner Mistakes
  19. Best Practices
  20. Mini Challenge
  21. Practice Questions
  22. Practice Answers
  23. Quick Revision
  24. Key Takeaways
  25. What's Next?

Lecture 54 — Constructors

Category9 — Object-Oriented Programming
Lecture054 of 140
Estimated video length11 min
TargetDart 3.12.2
LevelBeginner

Creating an object and then filling its fields one by one lets you forget a field. A constructor fills them while the object is being created — and Dart refuses to create it until every field has a value.

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 001–053:

  • final variables (Lecture 006) and nullable types (Lecture 013)
  • positional, optional [ ] and default parameters (Lecture 032)
  • named parameters and required (Lecture 033)
  • classes, fields, methods, creating objects, and cascades .. (Lecture 053)

Connection With Previous Lectures

In Lecture 053 we created an object first and filled in its fields afterwards:

dart
class Student {
  String name = '';
  int age = 0;
}

void main() {
  var ali = Student()
    ..name = 'Ali'
    ..age = 20;
  var sara = Student();
  print('${ali.name} is ${ali.age}');
  print('${sara.name} is ${sara.age}');
}
output
Ali is 20
 is 0

Nothing stopped us from forgetting Sara's details, so she became a student with no name, aged 0. The fake starting values '' and 0 hid the mistake. We need a way to say: a Student cannot exist without a name and an age.


Learning Objectives

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

  • Explain what a constructor is and when it runs
  • Recognize the default constructor Dart provides
  • Write a constructor with parameters using this.field
  • Use named parameters, required, and default values in a constructor
  • Set final fields from a constructor
  • Compute a field from other values with an initializer list
  • Run extra code in a constructor body
  • Recognize the errors Dart gives for missing or unset fields

1. What Is a Constructor?

A constructor is a special function inside a class that runs once, at the moment an object is created. Its job is to give the new object its starting values.

Analogy — think of a school admission form. A student is not enrolled until the form is filled in. The constructor is that form: the object does not exist until its details are provided.

A constructor has the same name as the class and no return type.


2. The Default Constructor

Every time we wrote Student() in Lecture 053, we were already calling a constructor. If a class declares no constructor, Dart quietly adds one with no parameters, called the default constructor:

dart
class Student {
  String name = 'Unknown';
}

void main() {
  var s = Student();
  print(s.name);
}
output
Unknown

The brackets in Student() are the call. The default constructor takes nothing, so every field needed a starting value written in the class.


3. Writing Your Own Constructor

Write the class name, a parameter list, and a body — just like a function without a return type:

dart
class Student {
  String name = '';
  int age = 0;

  Student(String newName, int newAge) {
    name = newName;
    age = newAge;
  }
}

void main() {
  var ali = Student('Ali', 20);
  print('${ali.name} is ${ali.age}');
}
output
Ali is 20

Student('Ali', 20) passes the arguments to the constructor, and the body copies them into the fields. A constructor that creates and fills a new object like this is called a generative constructor.


4. The Body Is Too Late

Those placeholder values '' and 0 are still there. Remove them and Dart objects:

dart
class Student {
  String name;
  int age;

  Student(String newName, int newAge) {
    name = newName;
    age = newAge;
  }
}
output
Error: Field 'name' should be initialized because its type 'String' doesn't allow null.
  String name;
         ^^^^
Error: Field 'age' should be initialized because its type 'int' doesn't allow null.
  int age;
      ^^^

Dart's rule: every non-nullable field must have a value before the body starts running. Inside the body, the object already exists — so its fields would be null for a moment, and String does not allow null. We need to set fields earlier.


5. Initializing Formals: this.field

Write this.name as a parameter, and Dart puts the argument straight into the field before the body runs:

dart
class Student {
  String name;
  int age;

  Student(this.name, this.age);
}

void main() {
  var sara = Student('Sara', 19);
  print('${sara.name} is ${sara.age}');
}
output
Sara is 19

This is called an initializing formal. There is no body at all — the semicolon ends the constructor. No placeholders, no copying, and Sara can no longer be created without details. this means "the object being built"; we study it properly in Lecture 056.


6. Your Constructor Replaces the Default One

Once you write a constructor, Dart no longer adds the empty default one:

dart
class Student {
  String name;
  int age;

  Student(this.name, this.age);
}

void main() {
  var s = Student();
}
output
Error: Too few positional arguments: 2 required, 0 given.
  var s = Student();
                 ^

That is exactly what we wanted: forgetting the details is now a compile error, not a silent bug.


7. Named Parameters and required

Constructors accept named parameters, exactly like the functions in Lecture 033. Combine { }, required and this.:

dart
class Student {
  String name;
  int age;

  Student({required this.name, required this.age});
}

void main() {
  var zayan = Student(age: 6, name: 'Zayan');
  print('${zayan.name} is ${zayan.age}');
}
output
Zayan is 6

The call now explains itself, and the order does not matter. Leave one out and Dart names it:

output
Error: Required named parameter 'age' must be provided.
  var s = Student(name: 'Ali');
                 ^

8. Optional Parameters and Default Values

An optional parameter needs a default value or a nullable type (Lectures 032 and 033). The same rule applies to constructors:

dart
class Student {
  String name;
  int age;
  String city;
  String? email;

  Student(this.name, {this.age = 18, this.city = 'Karachi', this.email});
}

void main() {
  var ali = Student('Ali');
  var sara = Student('Sara', age: 21, email: 'sara@mail.com');
  print('${ali.name}, ${ali.age}, ${ali.city}, ${ali.email}');
  print('${sara.name}, ${sara.age}, ${sara.city}, ${sara.email}');
}
output
Ali, 18, Karachi, null
Sara, 21, Karachi, sara@mail.com

name is required and positional; the rest are optional named parameters. email is String?, so it simply stays null when left out.


9. final Fields Set by the Constructor

A student's ID should never change after creation. Mark the field final and let the constructor set it:

dart
class Student {
  final int id;
  String name;

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

void main() {
  var ali = Student(101, 'Ali');
  ali.name = 'Ali Khan';
  print('${ali.id}: ${ali.name}');
}
output
101: Ali Khan

name can change; id cannot. Try ali.id = 202; and Dart refuses, because a final field has no way to be assigned from outside:

output
Error: The setter 'id' isn't defined for the type 'Student'.

10. Initializer Lists

Sometimes a field is not passed in but calculated from other values. Put it after a colon :, between the parameters and the (optional) body. This is an initializer list:

dart
class Product {
  final String name;
  final double price;
  final int quantity;
  final double total;

  Product(this.name, this.price, this.quantity) : total = price * quantity;
}

void main() {
  var pen = Product('Pen', 25.5, 4);
  print('${pen.name}: ${pen.total}');
}
output
Pen: 102.0

The initializer list runs before the body, so it can set even final and non-nullable fields. Separate several entries with commas.


11. Running Code in the Constructor Body

A body is still useful for work that is not just setting fields. It runs last, after every field has its value:

dart
class BankAccount {
  final String owner;
  double balance;

  BankAccount(this.owner, this.balance) {
    print('Account opened for $owner with $balance');
  }
}

void main() {
  var account = BankAccount('Sara', 1000.0);
  account.balance = account.balance + 500;
  print('Balance: ${account.balance}');
}
output
Account opened for Sara with 1000.0
Balance: 1500.0

The order is always: parameters → initializer list → body.


12. A Quick Look at assert

An initializer list can also contain an assert, a check that stops the program during development when a condition is false:

dart
class Product {
  final String name;
  final double price;

  Product(this.name, this.price) : assert(price >= 0);
}

void main() {
  var bag = Product('Bag', 1500);
  print('${bag.name}: ${bag.price}');
}
output
Bag: 1500.0

Assertions only run when enabled (for example dart run --enable-asserts), so they are a developer's safety net, not real validation. We will study assertions in Lecture 086 — Rethrowing, Exception Design & Assertions.


13. A Real-World Example

dart
class OrderLine {
  final String product;
  final double price;
  final int quantity;
  final double discountRate;
  final double total;

  OrderLine({
    required this.product,
    required this.price,
    this.quantity = 1,
    this.discountRate = 0.0,
  }) : total = price * quantity * (1 - discountRate) {
    print('Added $quantity x $product');
  }
}

void main() {
  var shoes = OrderLine(product: 'Shoes', price: 3000, discountRate: 0.1);
  var socks = OrderLine(product: 'Socks', price: 200, quantity: 3);
  print('Shoes: ${shoes.total}');
  print('Socks: ${socks.total}');
  print('Bill: ${shoes.total + socks.total}');
}
output
Added 1 x Shoes
Added 3 x Socks
Shoes: 2700.0
Socks: 600.0
Bill: 3300.0

Required named parameters, defaults, final fields, an initializer list and a body — every order line is complete and correct the moment it exists.


Code Examples

Example 1 — The Default Constructor

dart
class Car {
  String brand = 'Toyota';
  int wheels = 4;
}

void main() {
  var car = Car();
  print('${car.brand} has ${car.wheels} wheels');
}
output
Toyota has 4 wheels

Example 2 — A Constructor with a Body

dart
class Pet {
  String name = '';

  Pet(String petName) {
    name = petName;
  }
}

void main() {
  var pet = Pet('Whiskers');
  print(pet.name);
}
output
Whiskers

Example 3 — Initializing Formals

dart
class Book {
  String title;
  int pages;

  Book(this.title, this.pages);
}

void main() {
  var book = Book('Dart Basics', 240);
  print('${book.title}: ${book.pages} pages');
}
output
Dart Basics: 240 pages

Example 4 — Required Named Parameters

dart
class City {
  String name;
  int population;

  City({required this.name, required this.population});
}

void main() {
  var city = City(population: 2000000, name: 'Lahore');
  print('${city.name}: ${city.population}');
}
output
Lahore: 2000000

Example 5 — Default and Nullable Parameters

dart
class Player {
  String name;
  int level;
  String? team;

  Player(this.name, {this.level = 1, this.team});
}

void main() {
  var p1 = Player('Zayan');
  var p2 = Player('Sara', level: 5, team: 'Red');
  print('${p1.name} L${p1.level} ${p1.team ?? 'no team'}');
  print('${p2.name} L${p2.level} ${p2.team ?? 'no team'}');
}
output
Zayan L1 no team
Sara L5 Red

Example 6 — An Optional Positional Parameter

dart
class Greeting {
  String name;
  String message;

  Greeting(this.name, [this.message = 'Hello']);
}

void main() {
  var g1 = Greeting('Ali');
  var g2 = Greeting('Sara', 'Welcome');
  print('${g1.message}, ${g1.name}!');
  print('${g2.message}, ${g2.name}!');
}
output
Hello, Ali!
Welcome, Sara!

Example 7 — Student Marks

dart
class StudentResult {
  final String name;
  final int math;
  final int science;
  final int english;
  final int total;

  StudentResult(this.name, this.math, this.science, this.english)
    : total = math + science + english;
}

void main() {
  var ali = StudentResult('Ali', 80, 75, 85);
  print('${ali.name}: ${ali.total}');
}
output
Ali: 240

How it works

  1. The four this. parameters fill the mark fields.
  2. The initializer list adds them into total before the object is finished.
  3. Every field is final, so a result can never be altered after it is created.

Example 8 — Shopping Total

dart
class CartItem {
  final String product;
  final int price;
  final int quantity;
  final int total;

  CartItem({required this.product, required this.price, this.quantity = 1})
    : total = price * quantity;
}

void main() {
  var notebook = CartItem(product: 'Notebook', price: 500, quantity: 3);
  var pen = CartItem(product: 'Pen', price: 50);
  print('${notebook.product}: ${notebook.total}');
  print('${pen.product}: ${pen.total}');
}
output
Notebook: 1500
Pen: 50

How it works

  1. product and price are required; quantity defaults to 1.
  2. total is calculated once, in the initializer list.
  3. The pen left out quantity, so its total is 50 * 1.

Example 9 — Bank Balance

dart
class BankAccount {
  final String owner;
  double balance;

  BankAccount(this.owner, {this.balance = 0.0}) {
    print('Welcome, $owner!');
  }

  void deposit(double amount) {
    balance = balance + amount;
  }
}

void main() {
  var account = BankAccount('Ali', balance: 1000.0);
  account.deposit(250.5);
  print('Balance: ${account.balance}');
}
output
Welcome, Ali!
Balance: 1250.5

How it works

  1. owner is final; balance is not, because deposits change it.
  2. The body runs after both fields are set, so it can use owner.
  3. deposit is an ordinary method, as in Lecture 053.

Example 10 — Temperature Conversion

dart
class Temperature {
  final double celsius;
  final double fahrenheit;

  Temperature(this.celsius) : fahrenheit = celsius * 9 / 5 + 32;
}

void main() {
  var today = Temperature(25.0);
  print('${today.celsius} C = ${today.fahrenheit} F');
}
output
25.0 C = 77.0 F

How it works

  1. Only celsius is passed in.
  2. The initializer list converts it into fahrenheit.
  3. Both values are final and always agree with each other.

Common Beginner Mistakes

1. Leaving a non-nullable field unset

dart
class Student {
  String name;
  int age; // ✗ no constructor parameter sets it

  Student(this.name);
}
output
Error: Field 'age' should be initialized because its type 'int' doesn't allow null.

Add this.age to the constructor, give it a default, or make it int?.

2. Assigning fields in the body instead of before it

dart
class Student {
  String name; // ✗
  Student(String newName) {
    name = newName;
  }
}
output
Error: Field 'name' should be initialized because its type 'String' doesn't allow null.

The body runs too late. Use Student(this.name);.

3. Setting a final field in the body

dart
class Product {
  final double price;
  final double total;

  Product(this.price) {
    // total = price * 2; // ✗
  }
}
output
Error: The setter 'total' isn't defined for the type 'Product'.
Error: Final field 'total' is not initialized.

Calculated final fields belong in the initializer list: Product(this.price) : total = price * 2;.

4. Calling the empty default constructor after writing your own

dart
// var s = Student(); // ✗ Student(this.name, this.age) needs two arguments
output
Error: Too few positional arguments: 2 required, 0 given.

5. Passing names to positional parameters

dart
// var s = Student(name: 'Ali', age: 20); // ✗ the constructor is Student(this.name, this.age)
output
Error: Too few positional arguments: 2 required, 0 given.

Named arguments only work when the constructor declares { }.

6. Expecting assert to always run

An assert is skipped by a plain dart run. Don't rely on it to protect real data.


Best Practices

  • Prefer this.field parameters over copying values in a body.
  • Make fields final when they should not change after creation.
  • Use required named parameters when a constructor takes several values of the same type.
  • Give optional fields sensible defaults; use a nullable type only when "no value" is real.
  • Calculate derived fields once, in the initializer list.
  • Keep constructor bodies short — no body at all is common and good.
  • Avoid placeholder values like '' and 0 just to satisfy Dart.

Mini Challenge

Write a Rectangle class with final double width, final double height, and a final double area calculated in an initializer list. Create a 4.0 by 2.5 rectangle and print:

output
Area: 10.0

Try it yourself before opening the solution.

Show solution
dart
class Rectangle {
  final double width;
  final double height;
  final double area;

  Rectangle(this.width, this.height) : area = width * height;
}

void main() {
  var r = Rectangle(4.0, 2.5);
  print('Area: ${r.area}');
}
output
Area: 10.0

How it works

  1. this.width and this.height fill the two measurements.
  2. The initializer list multiplies them into area.
  3. All three fields are final, so the area can never fall out of date.

Practice Questions

Work through these in order — they run from beginner to application to challenge. Every question gives you exact values, so you can compare your output with the answers.

Beginner

  1. Write a Fruit class with a String name field and a constructor using this.name. Create Fruit('Mango') and print its name.
  2. Write a Laptop class with brand and price (an int) set by a positional constructor. Create one for 'Dell' at 150000 and print Dell: 150000.
  3. What does Dart give a class that declares no constructor? Write a Counter class with int value = 0; and no constructor, create one, and print value.

Application

  1. Write a Teacher class whose constructor takes required named name and subject. Create Sara, who teaches Math, and print Sara teaches Math.

  2. Write a Coffee class with String size defaulting to 'Medium' and bool sugar defaulting to false, both named. Print the two fields of Coffee() and of Coffee(size: 'Large', sugar: true), one object per line.

  3. Write a Score class with final String player and int points. Create Zayan with 10 points, add 15 to points, and print Zayan: 25.

  4. This class does not compile. Fix it without giving the fields starting values:

    dart
    class Student {
      String name;
      int age;
    
      Student(String n, int a) {
        name = n;
        age = a;
      }
    }

Challenge

  1. Write an Invoice class with price (double), taxRate (double, default 0.1) and a final double total equal to price + price * taxRate. Print invoice.total for Invoice(2000.0).
  2. Write a Marks class taking name, obtained and outOf (both int). Compute final double percentage in an initializer list. Print Ali: 80.0% for marks 400 out of 500.
  3. Write a Thermometer class with final double celsius and a final bool isHot that is true when celsius is above 30. Print both fields for 35.0 and 22.5 like 35.0 hot: true. The constructor must have no body.

Practice Answers

Question 1 — Solution

dart
class Fruit {
  String name;

  Fruit(this.name);
}

void main() {
  var fruit = Fruit('Mango');
  print(fruit.name);
}
output
Mango

Question 2 — Solution

dart
class Laptop {
  String brand;
  int price;

  Laptop(this.brand, this.price);
}

void main() {
  var laptop = Laptop('Dell', 150000);
  print('${laptop.brand}: ${laptop.price}');
}
output
Dell: 150000

Question 3 — Solution

Dart adds the default constructor, which takes no arguments.

dart
class Counter {
  int value = 0;
}

void main() {
  var counter = Counter();
  print(counter.value);
}
output
0

Question 4 — Solution

dart
class Teacher {
  String name;
  String subject;

  Teacher({required this.name, required this.subject});
}

void main() {
  var sara = Teacher(name: 'Sara', subject: 'Math');
  print('${sara.name} teaches ${sara.subject}');
}
output
Sara teaches Math

Question 5 — Solution

dart
class Coffee {
  String size;
  bool sugar;

  Coffee({this.size = 'Medium', this.sugar = false});
}

void main() {
  var c1 = Coffee();
  var c2 = Coffee(size: 'Large', sugar: true);
  print('${c1.size} ${c1.sugar}');
  print('${c2.size} ${c2.sugar}');
}
output
Medium false
Large true

Question 6 — Solution

dart
class Score {
  final String player;
  int points;

  Score(this.player, this.points);
}

void main() {
  var score = Score('Zayan', 10);
  score.points = score.points + 15;
  print('${score.player}: ${score.points}');
}
output
Zayan: 25

Question 7 — Solution

Use initializing formals, so the fields are set before the body would run:

dart
class Student {
  String name;
  int age;

  Student(this.name, this.age);
}

void main() {
  var s = Student('Ali', 20);
  print('${s.name} ${s.age}');
}
output
Ali 20

Question 8 — Solution

dart
class Invoice {
  final double price;
  final double taxRate;
  final double total;

  Invoice(this.price, {this.taxRate = 0.1}) : total = price + price * taxRate;
}

void main() {
  var invoice = Invoice(2000.0);
  print(invoice.total);
}
output
2200.0

Question 9 — Solution

dart
class Marks {
  final String name;
  final int obtained;
  final int outOf;
  final double percentage;

  Marks(this.name, this.obtained, this.outOf)
    : percentage = obtained / outOf * 100;
}

void main() {
  var ali = Marks('Ali', 400, 500);
  print('${ali.name}: ${ali.percentage}%');
}
output
Ali: 80.0%

/ always gives a double, so percentage prints as 80.0.

Question 10 — Solution

dart
class Thermometer {
  final double celsius;
  final bool isHot;

  Thermometer(this.celsius) : isHot = celsius > 30;
}

void main() {
  var t1 = Thermometer(35.0);
  var t2 = Thermometer(22.5);
  print('${t1.celsius} hot: ${t1.isHot}');
  print('${t2.celsius} hot: ${t2.isHot}');
}
output
35.0 hot: true
22.5 hot: false

Quick Revision

  1. When does a constructor run?
  2. What does Dart provide when a class has no constructor?
  3. What does this.name in a parameter list do?
  4. Why can't a constructor body set a non-nullable field that has no starting value?
  5. How do you make a constructor parameter named and compulsory?
  6. Where is a calculated final field set?
  7. In what order do the parameters, initializer list and body run?

Key Takeaways

  • A constructor has the class's name and runs once, when an object is created.
  • With no constructor written, Dart supplies an empty default constructor; writing your own removes it.
  • this.field parameters (initializing formals) put arguments straight into fields.
  • Non-nullable and final fields must be set before the body — by this.field, a default, or the initializer list.
  • Constructors support positional, [ ] optional, named, required and default parameters.
  • An initializer list (: total = price * quantity) computes fields from other values.
  • The body runs last, for extra work after the fields are set.
  • assert in an initializer list is a development-only check (Lecture 086).

What's Next?

Lecture 55 — Named & Constant Constructors. A class can have only one unnamed constructor, yet objects are often created in different ways — a student from a name and age, or a guest student with no details at all. Next we will give constructors their own names, and learn how const constructors create objects that never change.


Video lecture

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