On this page22 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. What Is Polymorphism?
- 2. One Variable, Many Objects
- 3. A List of the Parent Type
- 4. The Object Decides — Dynamic Dispatch
- 5. Functions That Accept the Parent Type
- 6. Polymorphism Through Interfaces
- 7. Reaching Subtype-Only Members with is
- 8. Why Needing is Is a Warning Sign
- 9. Replacing Type Checks with Overridden Methods
- 10. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 65 — Polymorphism
| Category | 10 — Advanced OOP |
| Lecture | 065 of 140 |
| Estimated video length | 10 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
A separate list and loop for every kind of object does not scale. With polymorphism, one variable, one list, one function handles every subtype — each running its own method.
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 059–064:
extends, overriding with@override, andsuper- abstract classes and abstract methods
- interfaces with
implements, and mixins withwith - lists and
for-inloops (Lectures 027, 039) - the type test
isand the castas(Lecture 010)
Connection With Previous Lectures
In Lecture 060, Animal pet = Dog(); followed by pet.speak() printed Woof! — the object,
not the variable, decided which version ran.
That was one object. A real program has many: circles, squares and triangles; full-time staff and freelancers. A separate list and loop for each kind means more code to change with every new kind. We want to treat them all as one group and still get the right behaviour from each.
Learning Objectives
By the end of this lecture you will be able to:
- Explain what polymorphism means
- Store objects of different subclasses in one parent-typed variable or list
- Explain dynamic dispatch: the object, not the variable, picks the method
- Write functions that accept the parent type and work with every subtype
- Reach subtype-only members with
isand type promotion - Recognize type checks as a design smell
- Replace an
if/switchon types with an overridden method
1. What Is Polymorphism?
Polymorphism means "many forms". One piece of code talks to an object through a general
type — Animal, Shape — and the object behaves in its own specific way.
Analogy — a teacher says "Everyone, introduce yourselves." One sentence, but Ali, Sara and Zayan each answer differently. The teacher does not need a separate instruction for each student.
2. One Variable, Many Objects
A variable of the parent type can hold any subclass object, and can switch to another one later:
class Animal {
void speak() => print('...');
}
class Dog extends Animal {
@override
void speak() => print('Woof!');
}
class Cat extends Animal {
@override
void speak() => print('Meow!');
}
void main() {
Animal pet = Dog();
pet.speak();
pet = Cat();
pet.speak();
}Woof!
Meow!The same line, pet.speak(), gave two results because the object inside pet changed.
3. A List of the Parent Type
A List<Shape> accepts every kind of shape, and one loop handles them all:
abstract class Shape {
double get area;
}
class Circle extends Shape {
Circle(this.radius);
final double radius;
@override
double get area => 3.14 * radius * radius;
}
class Square extends Shape {
Square(this.side);
final double side;
@override
double get area => side * side;
}
void main() {
List<Shape> shapes = [Circle(1), Square(2), Square(3)];
double total = 0;
for (var shape in shapes) {
total += shape.area;
}
print('Total area: $total');
}Total area: 16.14The loop never asks what kind of shape it has; each object computes its own area.
4. The Object Decides — Dynamic Dispatch
Before running, Dart checks the variable's type: does Animal have a speak()? While
running, it asks the object which speak() to use. This run-time choice is called
dynamic dispatch. Here, reading the code cannot tell you the answer:
class Animal {
void speak() => print('...');
}
class Dog extends Animal {
@override
void speak() => print('Woof!');
}
class Cat extends Animal {
@override
void speak() => print('Meow!');
}
Animal adopt(int ticket) => ticket.isEven ? Dog() : Cat();
void main() {
for (var ticket in [1, 2, 3]) {
adopt(ticket).speak();
}
}Meow!
Woof!
Meow!adopt promises only an Animal. Which speak() runs is decided each time, by the object.
5. Functions That Accept the Parent Type
A parameter of the parent type accepts every subtype — even ones not written yet:
abstract class Shape {
double get area;
}
class Square extends Shape {
Square(this.side);
final double side;
@override
double get area => side * side;
}
class Triangle extends Shape {
Triangle(this.base, this.height);
final double base, height;
@override
double get area => base * height / 2;
}
void describe(Shape shape) {
print('${shape.runtimeType} has area ${shape.area}');
}
void main() {
describe(Square(3));
describe(Triangle(4, 5));
}Square has area 9.0
Triangle has area 10.0Triangle was added after describe was written, and describe did not change at all.
6. Polymorphism Through Interfaces
The shared type can also be an interface (Lecture 063). Unrelated classes that implement it share one list:
abstract class Payable {
double get amountDue;
}
class Employee implements Payable {
Employee(this.salary);
final double salary;
@override
double get amountDue => salary;
}
class Invoice implements Payable {
Invoice(this.total);
final double total;
@override
double get amountDue => total;
}
void main() {
List<Payable> bills = [Employee(50000), Invoice(3600)];
for (var bill in bills) {
print(bill.amountDue);
}
}50000.0
3600.0An employee is not an invoice, but both can be paid — that is all the loop needs. A mixin type (Lecture 064) works the same way.
7. Reaching Subtype-Only Members with is
Through a parent-typed variable, Dart allows only the parent's members:
class Animal {}
class Dog extends Animal {
void fetch() => print('Fetching!');
}
void main() {
Animal pet = Dog();
pet.fetch();
}Error: The method 'fetch' isn't defined for the type 'Animal'.
Try correcting the name to the name of an existing method, or defining a method named 'fetch'.
pet.fetch();
^^^^^An is check promotes the variable: inside the if, Dart treats pet as a Dog
(Lecture 059):
class Animal {}
class Dog extends Animal {
void fetch() => print('Fetching!');
}
void main() {
Animal pet = Dog();
if (pet is Dog) {
pet.fetch();
}
}Fetching!8. Why Needing is Is a Warning Sign
is works, but a chain of type checks usually means the classes are not doing their job:
abstract class Shape {}
class Circle extends Shape {
Circle(this.radius);
final double radius;
}
class Square extends Shape {
Square(this.side);
final double side;
}
class Triangle extends Shape {}
double areaOf(Shape shape) {
if (shape is Circle) {
return 3.14 * shape.radius * shape.radius;
} else if (shape is Square) {
return shape.side * shape.side;
}
return 0;
}
void main() {
print(areaOf(Square(2)));
print(areaOf(Triangle()));
}4.0
0.0Someone added Triangle but forgot areaOf — no error, just a wrong answer. Every new shape
means hunting down every chain like this one.
9. Replacing Type Checks with Overridden Methods
Move each branch into its own class as an override, and the chain disappears:
abstract class Shape {
double get area;
}
class Circle extends Shape {
Circle(this.radius);
final double radius;
@override
double get area => 3.14 * radius * radius;
}
class Square extends Shape {
Square(this.side);
final double side;
@override
double get area => side * side;
}
void main() {
for (Shape shape in [Circle(1), Square(2)]) {
print(shape.area);
}
}3.14
4.0A new Triangle now must provide area — forgetting it is a compile error (Lecture 062),
not a silent 0.0. The same applies to a switch on runtimeType: ask the object, don't
inspect it.
Dart also has sealed classes, which let a switch on types be checked for missing cases. We
will study them in Lecture 082 — Sealed Classes.
10. A Real-World Example
A shop sells items priced in different ways. The cart treats them all as CartItem:
abstract class CartItem {
CartItem(this.name);
final String name;
double get price;
}
class FixedItem extends CartItem {
FixedItem(super.name, this.cost);
final double cost;
@override
double get price => cost;
}
class WeighedItem extends CartItem {
WeighedItem(super.name, this.kilos, this.pricePerKilo);
final double kilos, pricePerKilo;
@override
double get price => kilos * pricePerKilo;
}
double checkout(List<CartItem> cart) {
double total = 0;
for (var item in cart) {
print('${item.name}: ${item.price}');
total += item.price;
}
return total;
}
void main() {
var cart = [
FixedItem('Notebook', 500),
WeighedItem('Apples', 2, 300),
FixedItem('Bag', 750),
];
print('Total: ${checkout(cart)}');
}Notebook: 500.0
Apples: 600.0
Bag: 750.0
Total: 1850.0checkout reads price without knowing the item's kind; each class answers for itself.
Code Examples
Example 1 — Parent Variable, Child Object
class Vehicle {
void move() => print('Moving');
}
class Bike extends Vehicle {
@override
void move() => print('Pedalling');
}
void main() {
Vehicle v = Bike();
v.move();
}PedallingExample 2 — Reassigning the Variable
class Light {
String get colour => 'white';
}
class RedLight extends Light {
@override
String get colour => 'red';
}
void main() {
Light light = Light();
print(light.colour);
light = RedLight();
print(light.colour);
}white
redExample 3 — A Mixed List
abstract class Instrument {
void play();
}
class Drum extends Instrument {
@override
void play() => print('Boom');
}
class Flute extends Instrument {
@override
void play() => print('Toot');
}
void main() {
for (var i in [Drum(), Flute(), Drum()]) {
i.play();
}
}Boom
Toot
BoomExample 4 — An Inherited Version Still Counts
class Bird {
String get move => 'flies';
}
class Penguin extends Bird {
@override
String get move => 'swims';
}
class Sparrow extends Bird {}
void main() {
List<Bird> birds = [Penguin(), Sparrow()];
for (var b in birds) {
print('${b.runtimeType} ${b.move}');
}
}Penguin swims
Sparrow fliesSparrow does not override, so it uses the inherited version.
Example 5 — A Function with a Parent Parameter
abstract class Greeter {
String greet(String name);
}
class English extends Greeter {
@override
String greet(String name) => 'Hello, $name';
}
class Urdu extends Greeter {
@override
String greet(String name) => 'Assalam-o-Alaikum, $name';
}
void welcome(Greeter g) => print(g.greet('Zayan'));
void main() {
welcome(English());
welcome(Urdu());
}Hello, Zayan
Assalam-o-Alaikum, ZayanExample 6 — Through an Interface
abstract class Printable {
String describe();
}
class Book implements Printable {
@override
String describe() => 'Book: Dart Basics';
}
class Photo implements Printable {
@override
String describe() => 'Photo: Beach';
}
void main() {
List<Printable> queue = [Book(), Photo()];
for (var item in queue) {
print(item.describe());
}
}Book: Dart Basics
Photo: BeachExample 7 — Student Marks
abstract class Student {
Student(this.marks);
final int marks;
bool get passed;
}
class Regular extends Student {
Regular(super.marks);
@override
bool get passed => marks >= 50;
}
class Honors extends Student {
Honors(super.marks);
@override
bool get passed => marks >= 75;
}
void main() {
for (Student s in [Regular(60), Honors(70)]) {
print('${s.runtimeType} ${s.marks}: ${s.passed ? 'Pass' : 'Fail'}');
}
}Regular 60: Pass
Honors 70: FailExample 8 — Shopping Discounts
abstract class Discount {
double apply(double total);
}
class NoDiscount extends Discount {
@override
double apply(double total) => total;
}
class PercentOff extends Discount {
PercentOff(this.percent);
final double percent;
@override
double apply(double total) => total - total * percent / 100;
}
void main() {
for (var d in [NoDiscount(), PercentOff(10)]) {
print(d.apply(2000));
}
}2000.0
1800.0Example 9 — Bank Accounts and Monthly Fees
abstract class Account {
double balance = 20000;
double get monthlyFee;
void chargeFee() => balance -= monthlyFee;
}
class BasicAccount extends Account {
@override
double get monthlyFee => 100;
}
class PremiumAccount extends Account {
@override
double get monthlyFee => balance > 10000 ? 0 : 250;
}
void main() {
for (Account a in [BasicAccount(), PremiumAccount()]) {
a.chargeFee();
print('${a.runtimeType}: ${a.balance}');
}
}BasicAccount: 19900.0
PremiumAccount: 20000.0chargeFee lives in the parent, but monthlyFee comes from each object.
Example 10 — Temperature Sensors
abstract class Sensor {
Sensor(this.reading);
final double reading;
double get celsius;
}
class CelsiusSensor extends Sensor {
CelsiusSensor(super.reading);
@override
double get celsius => reading;
}
class FahrenheitSensor extends Sensor {
FahrenheitSensor(super.reading);
@override
double get celsius => (reading - 32) * 5 / 9;
}
void main() {
List<Sensor> sensors = [CelsiusSensor(25), FahrenheitSensor(86)];
for (var s in sensors) {
print('${s.celsius} °C');
}
}25.0 °C
30.0 °CCommon Beginner Mistakes
1. Calling a subtype-only member through the parent type
void main() {
Animal pet = Dog();
// pet.fetch(); // ✗
if (pet is Dog) pet.fetch(); // ✓
}Error: The method 'fetch' isn't defined for the type 'Animal'.Needing this often means the method belongs in the parent (section 8).
2. Putting a parent object into a child variable
void main() {
// Dog d = Animal(); // ✗ not every animal is a dog
Animal a = Dog(); // ✓ every dog is an animal
}Error: A value of type 'Animal' can't be assigned to a variable of type 'Dog'.3. Forcing the type with as
void main() {
Animal pet = Cat();
// (pet as Dog).fetch(); // ✗ crashes while running
}Unhandled exception:
type 'Cat' is not a subtype of type 'Dog' in type castas does not check first. Use is when you are not sure.
Best Practices
- Type variables, lists, and parameters with the most general type that fits.
- Put behaviour that differs by kind in an overridden method, not in
if/switchon types. - Make the parent abstract when it has no sensible default, so every child must override.
- Use an interface as the shared type when the classes are not related by "is-a".
- Treat
ischecks on your own classes as a hint to redesign; avoidasunless certain.
Mini Challenge
Create an abstract class Employee with a name and an abstract getter pay. FullTime
earns a fixed salary; PartTime earns hours * rate. Write void payroll(List<Employee> staff) that prints each employee's pay and then the total.
Your program should print exactly:
Ali: 60000.0
Sara: 24000.0
Total: 84000.0Try it yourself before opening the solution.
Show solution
abstract class Employee {
Employee(this.name);
final String name;
double get pay;
}
class FullTime extends Employee {
FullTime(super.name, this.salary);
final double salary;
@override
double get pay => salary;
}
class PartTime extends Employee {
PartTime(super.name, this.hours, this.rate);
final double hours, rate;
@override
double get pay => hours * rate;
}
void payroll(List<Employee> staff) {
double total = 0;
for (var e in staff) {
print('${e.name}: ${e.pay}');
total += e.pay;
}
print('Total: $total');
}
void main() {
payroll([FullTime('Ali', 60000), PartTime('Sara', 80, 300)]);
}Ali: 60000.0
Sara: 24000.0
Total: 84000.0payroll never checks the employee's kind — each object knows its own pay.
Practice Questions
Work through these in order — they run from beginner to application to challenge.
Beginner
- Create
Animalwithspeak()printing..., andCowoverriding it to printMoo. Store aCowin anAnimalvariable and callspeak(). - Create a
List<Animal>holding aCow, a plainAnimal, and anotherCow. Callspeak()on each in a loop. - Does
Dog d = Animal();compile? Explain in one sentence.
Application
- Create abstract
Shapewith a getterarea, andRectangle(width, height)andCircle(radius)(use3.14). Print the total area ofRectangle(2, 3)andCircle(1). - Write
void announce(Vehicle v)that printsv.sound.Carreturns'Beep',Trainreturns'Choo choo'. Call it with both. - Create an interface
Chargeablewithint get batteryLevel.PhoneandLaptopimplement it with80and45. Print the levels from aList<Chargeable>. Animalhas nofetch(), butDogdoes. In a loop over[Dog(), Cat()], callfetch()only on dogs, usingis.
Challenge
-
Rewrite this function so that it has no
ischecks, by moving the logic into the classes:String soundOf(Animal a) { if (a is Dog) return 'Woof'; if (a is Cat) return 'Meow'; return '...'; } -
Birdhas a gettercanFlyreturningtrue.Ostrichoverrides it to returnfalse;Eagledoes not override. Loop over[Eagle(), Ostrich()]and print, for example,Eagle: true. -
Predict the output, then explain it in one sentence:
class A { String get name => 'A'; void show() => print('I am $name'); } class B extends A { @override String get name => 'B'; } void main() { A x = B(); x.show(); }
Practice Answers
Question 1 — Solution
class Animal {
void speak() => print('...');
}
class Cow extends Animal {
@override
void speak() => print('Moo');
}
void main() {
Animal a = Cow();
a.speak();
}MooQuestion 2 — Solution
class Animal {
void speak() => print('...');
}
class Cow extends Animal {
@override
void speak() => print('Moo');
}
void main() {
List<Animal> animals = [Cow(), Animal(), Cow()];
for (var a in animals) {
a.speak();
}
}Moo
...
MooQuestion 3 — Solution
No — not every animal is a dog: A value of type 'Animal' can't be assigned to a variable of type 'Dog'.
Question 4 — Solution
abstract class Shape {
double get area;
}
class Rectangle extends Shape {
Rectangle(this.width, this.height);
final double width, height;
@override
double get area => width * height;
}
class Circle extends Shape {
Circle(this.radius);
final double radius;
@override
double get area => 3.14 * radius * radius;
}
void main() {
List<Shape> shapes = [Rectangle(2, 3), Circle(1)];
double total = 0;
for (var s in shapes) {
total += s.area;
}
print(total);
}9.14Question 5 — Solution
abstract class Vehicle {
String get sound;
}
class Car extends Vehicle {
@override
String get sound => 'Beep';
}
class Train extends Vehicle {
@override
String get sound => 'Choo choo';
}
void announce(Vehicle v) => print(v.sound);
void main() {
announce(Car());
announce(Train());
}Beep
Choo chooQuestion 6 — Solution
abstract class Chargeable {
int get batteryLevel;
}
class Phone implements Chargeable {
@override
int get batteryLevel => 80;
}
class Laptop implements Chargeable {
@override
int get batteryLevel => 45;
}
void main() {
List<Chargeable> devices = [Phone(), Laptop()];
for (var d in devices) {
print(d.batteryLevel);
}
}80
45Question 7 — Solution
class Animal {}
class Dog extends Animal {
void fetch() => print('Fetching!');
}
class Cat extends Animal {}
void main() {
for (Animal a in [Dog(), Cat()]) {
if (a is Dog) {
a.fetch();
}
}
}Fetching!Question 8 — Solution
class Animal {
String get sound => '...';
}
class Dog extends Animal {
@override
String get sound => 'Woof';
}
class Cat extends Animal {
@override
String get sound => 'Meow';
}
void main() {
for (var a in [Dog(), Cat(), Animal()]) {
print(a.sound);
}
}Woof
Meow
...Question 9 — Solution
class Bird {
bool get canFly => true;
}
class Eagle extends Bird {}
class Ostrich extends Bird {
@override
bool get canFly => false;
}
void main() {
for (Bird b in [Eagle(), Ostrich()]) {
print('${b.runtimeType}: ${b.canFly}');
}
}Eagle: true
Ostrich: falseQuestion 10 — Solution
I am Bshow() is written in A, but name is looked up on the object, a B — dynamic dispatch
works even inside the parent's own methods.
Quick Revision
- What does "polymorphism" mean?
- Which types of objects can a
List<Shape>hold? - Who decides which override runs — the variable's type or the object?
- Why should a function's parameter use the parent type?
- What does an
ischeck do to a variable inside theif? - What replaces an
if/switchon types, and why?
Key Takeaways
- Polymorphism: one general type, many specific behaviours; a parent-typed variable or list holds any subtype object.
- Dynamic dispatch: Dart checks the variable's type before running, but the object chooses the method while running.
- Functions that accept the parent type work with every subtype, even future ones.
- An interface gives unrelated classes a shared type.
ispromotes a variable to reach subtype-only members — but needing it often is a design smell.- Replace
if/switchon types with an overridden method; an abstract parent makes sure no subtype forgets it.
What's Next?
Lecture 66 — Composition vs Inheritance. Polymorphism makes inheritance look powerful — but not every relationship is "is-a". A car is not an engine; it has one. Next we will learn composition, building objects out of other objects, and how to choose between it and inheritance.