Polymorphism in Dart
By Syncfusion®, Inc. · Last updated
How abstract classes and interfaces set out what a class must do, and how polymorphism lets one call run a different method for each kind of object.
What Are Abstract Classes and Interfaces?
- Abstract class = a class that is only a plan. You cannot make objects from it directly.
- Abstract method = a method with no body. It is a promise: "every child will write this".
- Interface = a contract: a list of members that a class promises to have.
- In Dart, every class is also an interface. You sign it with
implements.
Simple lesson: an abstract class says what must be done, and the child classes say how.
A Real-World Example
The school canteen takes payment in three ways: cash, card and UPI.
- Every way must be able to pay an amount.
- But each way pays differently: count notes, swipe, or scan a QR code.
- So the canteen writes one plan,
Payment, with apay()promise. - Each payment type fills in its own
pay().
Every Run box on this page runs on the course's Dart runner in your browser.
This lesson builds on Inheritance. Make sure extends, @override and super feel familiar first.
What You Will Learn
In this lesson, you will learn:
- Abstract classes and abstract methods.
- Interfaces,
implements, and using several at once. - When to use
extendsand when to useimplements. - How a parent-type variable can hold a child object.
- How Dart picks the right method while the program runs.
Abstract Classes
The abstract Keyword
abstract class Name {
ReturnType method(); // abstract: no body
ReturnType other() => ...; // concrete: has a body
}
- Concrete class = a normal class. You can make objects of it.
- Abstract class = a class marked
abstract. You cannot make objects of it.
You cannot make an object of a plan
abstract class Shape {
double area();
}
void main() {
var s = Shape();
print(s);
}
Dart stops this before running. A Shape with no real area() would be useless.
Abstract Methods
An abstract method ends with ; instead of a body. Every concrete child must give it a body:
Each shape fills in area()
abstract class Shape {
String get name;
double area();
}
class Circle extends Shape {
final double r;
Circle(this.r);
@override
String get name => 'Circle';
@override
double area() => 3.14 * r * r;
}
class Rectangle extends Shape {
final double w, h;
Rectangle(this.w, this.h);
@override
String get name => 'Rectangle';
@override
double area() => w * h;
}
void main() {
var shapes = [Circle(1.5), Rectangle(2.5, 4.2)];
for (var s in shapes) {
print('${s.name}: ${s.area().toStringAsFixed(2)}');
}
}
Forgetting a Promise
Try ticking and unticking the members below. The class only compiles when every promise is kept:
The child forgot area()
abstract class Shape {
double area();
}
class Square extends Shape {
final double side;
Square(this.side);
}
void main() {
print(Square(2.5).side);
}
The error lists the missing member: Shape.area.
Simple lesson: an abstract method is a promise, and Dart checks that every concrete child keeps it.
Concrete Members in Abstract Classes
An abstract class can also have normal fields, constructors and methods. Children inherit them as usual.
A plan with some work already done
abstract class Payment {
final int amount;
Payment(this.amount);
void pay();
void receipt() {
print('Paid Rs. $amount');
print('Thank you!');
}
}
class Cash extends Payment {
Cash(super.amount);
@override
void pay() => print('Counting notes for Rs. $amount');
}
class Upi extends Payment {
final String id;
Upi(super.amount, this.id);
@override
void pay() => print('Sending Rs. $amount to $id');
}
void main() {
var list = [Cash(40), Upi(65, 'canteen@gvs')];
for (var p in list) {
p.pay();
p.receipt();
}
}
pay()is different for each child, so it is abstract.receipt()is the same for all, so it is written once in the parent.
Simple lesson: put the shared work in the abstract class, and leave only the different parts abstract.
Interfaces
Implicit Interfaces
Implicit interface = in Dart, every class automatically defines an interface: the list of all its public members.
- There is no
interfacekeyword needed. Any class works as one. class B implements Ameans "B promises to have every member of A".- With
implements, B inherits no code. It must write every member itself.
Implementing a normal class
class Speaker {
void speak() => print('...');
}
class Robot implements Speaker {
@override
void speak() => print('Beep. I am a robot.');
}
void main() {
Speaker s = Robot();
s.speak();
print(s is Speaker);
}
Multiple Interfaces
A class can extend one parent, but implement many interfaces:
A smart board is three things at once
abstract class Display {
void show(String text);
}
abstract class Speaker {
void say(String text);
}
abstract class Charger {
int get watts;
}
class SmartBoard implements Display, Speaker, Charger {
@override
void show(String text) => print('[screen] $text');
@override
void say(String text) => print('[speaker] $text');
@override
int get watts => 65;
}
void main() {
var board = SmartBoard();
board.show('Good morning, Class 7A');
board.say('Good morning, Class 7A');
print('Charges at ${board.watts} W');
print(board is Display && board is Charger);
}
An abstract class with only abstract members is how you write a "pure" interface in Dart.
Name it after what it can do: Printable, Payable, Speaker.
Simple lesson: implements signs a contract. You get no code, only the promise to write every member.
extends or implements?
extends | implements | |
|---|---|---|
| How many? | one parent | many interfaces |
| Gets code? | yes, all public members | no, write everything |
| Must override? | only abstract members | every member |
super works? | yes | no |
| Means | "is a kind of" | "can do the job of" |
When to Use Each
extendswhen the child really is a kind of parent, and you want to reuse its code.implementswhen unrelated classes must work the same way, but share no code.- Abstract class when you have a plan with some shared work, plus some parts each child must fill in.
Both together
abstract class Printable {
void printMe();
}
class Person {
final String name;
Person(this.name);
}
class Student extends Person implements Printable {
final int marks;
Student(super.name, this.marks);
@override
void printMe() => print('$name scored $marks');
}
class Notice implements Printable {
final String text;
Notice(this.text);
@override
void printMe() => print('NOTICE: $text');
}
void main() {
List<Printable> pile = [Student('Asha', 92), Notice('Sports Day on Friday')];
for (var p in pile) {
p.printMe();
}
}
A Student and a Notice have nothing in common, except that both can be printed.
Easy way to remember
- abstract class = a plan. No objects. Some methods have no body.
- implements = sign a contract. Get no code. Write every member.
- extends = join a family. Get the code. Override only what you need.
Simple lesson: use extends to share code, and implements to share a promise.
What Is Polymorphism?
Polymorphism = "many forms". One line of code works with many kinds of objects, and each object does it its own way.
- You call the same method name:
s.area(). - A circle works out a circle's area. A square works out a square's area.
- You do not need an
iffor every kind of shape.
Simple lesson: polymorphism means "same call, different behaviour, chosen by the object".
A Real-World Example
At morning assembly, the principal says one sentence: "Everyone, show your ID."
- A student shows a student card.
- A teacher shows a staff card.
- A visitor shows a visitor pass.
- The principal does not ask each person what they are. Each person knows what to do.
Press the button and watch which area() runs for each shape:
The rest of this lesson uses Inheritance and the abstract classes from above.
Parent-Type References
A Parent Variable, a Child Object
A variable of the parent type can hold any child object:
Animal a = Dog()
class Animal {
void speak() => print('Some sound');
}
class Dog extends Animal {
@override
void speak() => print('Woof');
}
class Cow extends Animal {
@override
void speak() => print('Moo');
}
void main() {
Animal a = Dog();
a.speak();
a = Cow();
a.speak();
}
- The variable's type is
Animal. - The object inside it is a
Dog, then aCow. - Each time, the object's own
speak()runs.
What the Variable Type Allows
The variable's type decides which methods you may call. The object decides which version runs.
class Dog extends Animal {
void fetch() => print('Fetching!');
}
Animal a = Dog();
a.fetch(); // Error: The method 'fetch' isn't defined for the class 'Animal'.
- Not every
Animalcan fetch, so Dart refuses. - To use
fetch(), check the type first:if (a is Dog) a.fetch();
Check with is, then use the child's method
class Animal {
void speak() => print('Some sound');
}
class Dog extends Animal {
@override
void speak() => print('Woof');
void fetch() => print('Fetching the ball!');
}
void main() {
Animal a = Dog();
a.speak();
if (a is Dog) {
a.fetch();
}
}
After a is Dog is true, Dart promotes a to a Dog inside the if. (You saw type promotion in Null Safety.)
Simple lesson: the variable's type says what you may call, and the object says what actually runs.
Runtime Method Resolution
Runtime method resolution = Dart picks which version of a method to run while the program runs, by looking at the real object.
Which speak() runs?
- The line
p.speak()never changed. - What ran changed, because the object changed.
Simple lesson: Dart looks at the object, not the variable, to decide which method runs.
Polymorphism Through Abstract Classes
An abstract class is the most common way. It promises the method, and every child fills it in:
Every payment pays in its own way
abstract class Payment {
void pay(int rupees);
}
class Cash implements Payment {
@override
void pay(int rupees) => print('Cash: Rs. $rupees counted');
}
class Card implements Payment {
@override
void pay(int rupees) => print('Card: Rs. $rupees swiped');
}
class Upi implements Payment {
@override
void pay(int rupees) => print('UPI: Rs. $rupees sent');
}
void checkout(Payment method, int bill) {
print('Bill is Rs. $bill');
method.pay(bill);
}
void main() {
checkout(Upi(), 85);
checkout(Cash(), 40);
}
checkouttakes anyPayment. It never asks which kind.- Tomorrow you can add
Wallet implements Payment.checkoutneeds no change.
This is the big win: new kinds of objects can be added without changing the code that uses them.
Code that works on the parent type keeps working for every child, even ones written later.
Simple lesson: write functions for the parent type, and every child will fit.
Polymorphism Through Interfaces
Classes that are not related can still be used the same way, if they implement the same interface:
Things that can make a sound
abstract class Noisy {
String sound();
}
class SchoolBell implements Noisy {
@override
String sound() => 'Ring ring!';
}
class Whistle implements Noisy {
@override
String sound() => 'Phweet!';
}
class Parrot implements Noisy {
@override
String sound() => 'Hello, teacher!';
}
void main() {
List<Noisy> noisy = [SchoolBell(), Whistle(), Parrot()];
for (var n in noisy) {
print('${n.runtimeType}: ${n.sound()}');
}
}
A bell, a whistle and a parrot have no common parent. The Noisy interface is enough.
Simple lesson: an interface lets unrelated classes stand in the same line.
Practical Example: Shapes
Total area of a drawing
abstract class Shape {
String get name;
double area();
}
class Circle extends Shape {
final double r;
Circle(this.r);
@override
String get name => 'Circle';
@override
double area() => 3.14 * r * r;
}
class Square extends Shape {
final double side;
Square(this.side);
@override
String get name => 'Square';
@override
double area() => side * side;
}
class Triangle extends Shape {
final double base, height;
Triangle(this.base, this.height);
@override
String get name => 'Triangle';
@override
double area() => 0.5 * base * height;
}
void main() {
List<Shape> drawing = [Circle(2.5), Square(3.5), Triangle(4.5, 5.5)];
var total = 0.0;
for (var s in drawing) {
print('${s.name}: ${s.area().toStringAsFixed(2)}');
total += s.area();
}
print('Total area: ${total.toStringAsFixed(2)}');
}
Avoid long if (s is Circle) ... else if (s is Square) ... chains to pick a formula.
That is a sign the formula should be a method on each class. Then one call, s.area(), does the job.
Easy way to remember
- Variable type = what you are allowed to ask.
- Object = who answers, and how.
- One call, many answers: that is polymorphism.
Simple lesson: give each class its own version of the method, and let one loop call them all.
TL;DR
abstract classis a plan. Abstract methods have no body, and every concrete child must write them.- Every class is an interface.
implements A, Bpromises every member, and gives no code. extendsreuses code.implementsshares a promise.- Polymorphism:
Shape s = Circle();One call, a different method for each object. - The variable type says what you may call. The object decides what runs.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
var s = Shape(); for an abstract class | Abstract classes make objects | Make objects of a concrete child | A compile error: it is abstract. |
| Forgetting to write an abstract method | It has a default | Every concrete child must write it | A "missing implementations" error. |
Expecting code from implements | implements is like extends | implements gives no code at all | Every member must be written again. |
Calling super.x() after implements | super reaches the interface | super only reaches the extends parent | A compile error. |
abstract void pay(); | Methods need the abstract word | Only the class gets abstract | A compile error. |
| One giant interface | More members is better | Keep interfaces small and focused | Classes must write members they never use. |
Animal a = Dog(); a.fetch(); | The object is a Dog, so it works | The variable type limits the calls | A compile error. Check a is Dog first. |
Long is chains to choose behaviour | if is clearer | Put the behaviour in a method on each class | Every new class means editing the chain. |
| Thinking the parent's method runs | Animal a means Animal's version | The object's version runs | Surprise output. |
Forgetting @override | The name is enough | @override catches typos | A typo makes a new method, and the parent's runs instead. |
List<Dog> for mixed animals | Any list fits | Use the parent type: List<Animal> | A Cat cannot be added. |
Check Yourself
Exercises
What to Remember
- An abstract class is a plan that children fill in.
implementssigns a contract, and you write every member yourself.- A parent-type variable or list can hold any child object.
- Dart picks the method from the real object while the program runs.
- Prefer a method on each class over a long chain of
ischecks.
Next: Mixins. You will add ready-made abilities to classes, and new methods to types like String.