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 a pay() 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:

  1. Abstract classes and abstract methods.
  2. Interfaces, implements, and using several at once.
  3. When to use extends and when to use implements.
  4. How a parent-type variable can hold a child object.
  5. 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 interface keyword needed. Any class works as one.
  • class B implements A means "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?

extendsimplements
How many?one parentmany interfaces
Gets code?yes, all public membersno, write everything
Must override?only abstract membersevery member
super works?yesno
Means"is a kind of""can do the job of"

When to Use Each

  • extends when the child really is a kind of parent, and you want to reuse its code.
  • implements when 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 if for 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 a Cow.
  • 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 Animal can 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);
}
  • checkout takes any Payment. It never asks which kind.
  • Tomorrow you can add Wallet implements Payment. checkout needs 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

  1. abstract class is a plan. Abstract methods have no body, and every concrete child must write them.
  2. Every class is an interface. implements A, B promises every member, and gives no code.
  3. extends reuses code. implements shares a promise.
  4. Polymorphism: Shape s = Circle(); One call, a different method for each object.
  5. The variable type says what you may call. The object decides what runs.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
var s = Shape(); for an abstract classAbstract classes make objectsMake objects of a concrete childA compile error: it is abstract.
Forgetting to write an abstract methodIt has a defaultEvery concrete child must write itA "missing implementations" error.
Expecting code from implementsimplements is like extendsimplements gives no code at allEvery member must be written again.
Calling super.x() after implementssuper reaches the interfacesuper only reaches the extends parentA compile error.
abstract void pay();Methods need the abstract wordOnly the class gets abstractA compile error.
One giant interfaceMore members is betterKeep interfaces small and focusedClasses must write members they never use.
Animal a = Dog(); a.fetch();The object is a Dog, so it worksThe variable type limits the callsA compile error. Check a is Dog first.
Long is chains to choose behaviourif is clearerPut the behaviour in a method on each classEvery new class means editing the chain.
Thinking the parent's method runsAnimal a means Animal's versionThe object's version runsSurprise output.
Forgetting @overrideThe name is enough@override catches typosA typo makes a new method, and the parent's runs instead.
List<Dog> for mixed animalsAny list fitsUse 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.
  • implements signs 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 is checks.

Next: Mixins. You will add ready-made abilities to classes, and new methods to types like String.