Polymorphism and Method Overriding in Dart – Detailed Notes
Polymorphism and method overriding are important Object-Oriented Programming
concepts in Dart. They allow a program to work with objects of different classes through a common interface while
still executing the appropriate child-class behavior.
JustAcademy's Flutter curriculum includes Dart OOP concepts such as classes, objects, constructors,
inheritance, polymorphism, and abstraction as part of Dart Programming Fundamentals.
Explore JustAcademy's Flutter Training
.
:contentReference[oaicite:0]{index=0}
1. What is Polymorphism?
The word polymorphism comes from two words:
- Poly – many
- Morph – forms
Therefore, polymorphism means "many forms." In object-oriented programming, polymorphism allows
the same operation or interface to behave differently depending on the actual object involved.
For example, different animals can have a sound() method, but each animal can implement that method
differently.
class Animal {
void sound() {
print('Animal makes a sound');
}
}
class Dog extends Animal {
@override
void sound() {
print('Dog barks');
}
}
class Cat extends Animal {
@override
void sound() {
print('Cat meows');
}
}
The method name is the same, sound(), but its behavior changes according to the object.
2. What is Method Overriding?
Method overriding occurs when a child class provides its own implementation of a method that it
inherits from a parent class.
The child method normally has the same name and a compatible method signature as the inherited method.
class Animal {
void sound() {
print('Animal makes a sound');
}
}
class Dog extends Animal {
@override
void sound() {
print('Dog barks');
}
}
Here, Animal provides the original sound() method, while Dog overrides it.
3. Relationship Between Polymorphism and Method Overriding
Method overriding is one of the main mechanisms used to achieve runtime polymorphic behavior in an inheritance
hierarchy.
Parent Class
|
| inherited method
v
Child Class
|
| overrides method
v
Different implementation
For example:
class Animal {
void sound() {
print('Animal sound');
}
}
class Dog extends Animal {
@override
void sound() {
print('Dog bark');
}
}
class Cat extends Animal {
@override
void sound() {
print('Cat meow');
}
}
Both Dog and Cat have the same method name, but the behavior is different.
4. Basic Method Overriding Example
class Parent {
void showMessage() {
print('Message from Parent');
}
}
class Child extends Parent {
@override
void showMessage() {
print('Message from Child');
}
}
void main() {
Child child = Child();
child.showMessage();
}
Output:
Message from Child
The child version is executed because Child overrides the inherited method.
5. The @override Annotation
Dart provides the @override annotation to indicate that a member is intentionally overriding an
inherited member.
class Animal {
void sound() {
print('Animal sound');
}
}
class Dog extends Animal {
@override
void sound() {
print('Dog bark');
}
}
Using @override improves readability and allows Dart's analyzer to help identify mistakes in
overriding inherited members.
6. Why Use Method Overriding?
Method overriding is useful when a child class needs behavior that is more specific than the parent behavior.
- To customize inherited behavior.
- To provide specialized implementations.
- To support polymorphism.
- To create flexible application designs.
- To avoid duplicating unrelated class structures.
- To allow different subclasses to respond differently to the same method call.
7. Simple Real-World Example: Animals
Suppose an application needs to represent different animals. All animals have a sound, but each animal produces
a different sound.
class Animal {
void sound() {
print('Animal makes a sound');
}
}
class Dog extends Animal {
@override
void sound() {
print('Dog says Woof');
}
}
class Cat extends Animal {
@override
void sound() {
print('Cat says Meow');
}
}
class Cow extends Animal {
@override
void sound() {
print('Cow says Moo');
}
}
void main() {
Dog dog = Dog();
Cat cat = Cat();
Cow cow = Cow();
dog.sound();
cat.sound();
cow.sound();
}
Output:
Dog says Woof
Cat says Meow
Cow says Moo
8. Runtime Polymorphism
Runtime polymorphism occurs when a parent-type variable refers to a child object and the overridden implementation
associated with the actual object is executed.
class Animal {
void sound() {
print('Animal sound');
}
}
class Dog extends Animal {
@override
void sound() {
print('Dog barks');
}
}
void main() {
Animal animal = Dog();
animal.sound();
}
Output:
Dog barks
The variable animal has the static type Animal, but it refers to a Dog
object. The overridden Dog.sound() implementation is therefore selected at runtime.
9. Multiple Child Classes with Polymorphism
class Animal {
void sound() {
print('Generic animal sound');
}
}
class Dog extends Animal {
@override
void sound() {
print('Dog barks');
}
}
class Cat extends Animal {
@override
void sound() {
print('Cat meows');
}
}
class Cow extends Animal {
@override
void sound() {
print('Cow moos');
}
}
void main() {
Animal animal1 = Dog();
Animal animal2 = Cat();
Animal animal3 = Cow();
animal1.sound();
animal2.sound();
animal3.sound();
}
Output:
Dog barks
Cat meows
Cow moos
This is a useful example of polymorphism because all three variables have the parent type
Animal, while their actual objects are different child classes.
10. Polymorphism with a List
One practical benefit of polymorphism is that a collection can contain objects of different child classes when
they share a common parent type.
class Animal {
void sound() {
print('Animal sound');
}
}
class Dog extends Animal {
@override
void sound() {
print('Dog barks');
}
}
class Cat extends Animal {
@override
void sound() {
print('Cat meows');
}
}
class Cow extends Animal {
@override
void sound() {
print('Cow moos');
}
}
void main() {
List<Animal> animals = [
Dog(),
Cat(),
Cow(),
];
for (Animal animal in animals) {
animal.sound();
}
}
Output:
Dog barks
Cat meows
Cow moos
This design is especially useful when an application needs to process different types of objects using a common
interface.
11. Using super with Method Overriding
Sometimes the child class wants to keep the parent's behavior and then add additional behavior.
In that case, the child can call the parent implementation using super.
class Animal {
void sound() {
print('Animal makes a sound');
}
}
class Dog extends Animal {
@override
void sound() {
super.sound();
print('Dog specifically barks');
}
}
void main() {
Dog dog = Dog();
dog.sound();
}
Output:
Animal makes a sound
Dog specifically barks
12. Overriding a Method with Properties
Dart also allows inherited getters and setters to be overridden when the overriding declaration is compatible with
the inherited member.
class Person {
String get role {
return 'Person';
}
}
class Student extends Person {
@override
String get role {
return 'Student';
}
}
void main() {
Person person = Student();
print(person.role);
}
Output:
Student
13. Practical Example: Employee Polymorphism
Consider an application that manages different types of employees. Every employee can have a
calculateSalary() operation, but the calculation can differ for each employee type.
class Employee {
double calculateSalary() {
return 0;
}
}
class FullTimeEmployee extends Employee {
double monthlySalary;
FullTimeEmployee(this.monthlySalary);
@override
double calculateSalary() {
return monthlySalary;
}
}
class PartTimeEmployee extends Employee {
double hourlyRate;
int hours;
PartTimeEmployee(this.hourlyRate, this.hours);
@override
double calculateSalary() {
return hourlyRate * hours;
}
}
void main() {
Employee employee1 =
FullTimeEmployee(60000);
Employee employee2 =
PartTimeEmployee(500, 80);
print('Full-time salary: ${employee1.calculateSalary()}');
print('Part-time salary: ${employee2.calculateSalary()}');
}
Output:
Full-time salary: 60000.0
Part-time salary: 40000.0
The same calculateSalary() method call produces different results depending on the actual object.
14. Practical Example: Payment System
Polymorphism is useful in payment systems where different payment methods need a common operation such as
pay().
class Payment {
void pay(double amount) {
print('Processing payment of ₹$amount');
}
}
class CreditCardPayment extends Payment {
@override
void pay(double amount) {
print('Paid ₹$amount using Credit Card');
}
}
class UpiPayment extends Payment {
@override
void pay(double amount) {
print('Paid ₹$amount using UPI');
}
}
class CashPayment extends Payment {
@override
void pay(double amount) {
print('Paid ₹$amount using Cash');
}
}
void main() {
List<Payment> payments = [
CreditCardPayment(),
UpiPayment(),
CashPayment(),
];
for (Payment payment in payments) {
payment.pay(1000);
}
}
Output:
Paid ₹1000.0 using Credit Card
Paid ₹1000.0 using UPI
Paid ₹1000.0 using Cash
15. Practical Example: Notification System
class NotificationService {
void send(String message) {
print('Sending notification: $message');
}
}
class EmailNotification extends NotificationService {
@override
void send(String message) {
print('Email notification: $message');
}
}
class SmsNotification extends NotificationService {
@override
void send(String message) {
print('SMS notification: $message');
}
}
class PushNotification extends NotificationService {
@override
void send(String message) {
print('Push notification: $message');
}
}
void main() {
List<NotificationService> services = [
EmailNotification(),
SmsNotification(),
PushNotification(),
];
for (NotificationService service in services) {
service.send('Your order has been shipped.');
}
}
The application can work with NotificationService without needing to hard-code separate processing
logic for every notification type.
16. Practical Example: E-Commerce Products
class Product {
String name;
double price;
Product(this.name, this.price);
void displayDetails() {
print('$name - ₹$price');
}
}
class ElectronicsProduct extends Product {
ElectronicsProduct(
String name,
double price,
) : super(name, price);
@override
void displayDetails() {
print('Electronics: $name - ₹$price');
}
}
class ClothingProduct extends Product {
ClothingProduct(
String name,
double price,
) : super(name, price);
@override
void displayDetails() {
print('Clothing: $name - ₹$price');
}
}
void main() {
List<Product> products = [
ElectronicsProduct('Laptop', 65000),
ClothingProduct('T-Shirt', 1200),
];
for (Product product in products) {
product.displayDetails();
}
}
Output:
Electronics: Laptop - ₹65000.0
Clothing: T-Shirt - ₹1200.0
17. Practical Example: Shape Calculation
A common polymorphism example is a shape hierarchy. Each shape can provide its own implementation of
area().
class Shape {
double area() {
return 0;
}
}
class Circle extends Shape {
double radius;
Circle(this.radius);
@override
double area() {
return 3.14159 * radius * radius;
}
}
class Rectangle extends Shape {
double width;
double height;
Rectangle(this.width, this.height);
@override
double area() {
return width * height;
}
}
void main() {
List<Shape> shapes = [
Circle(5),
Rectangle(10, 4),
];
for (Shape shape in shapes) {
print('Area: ${shape.area()}');
}
}
18. Practical Example: Vehicles
class Vehicle {
void start() {
print('Vehicle is starting');
}
}
class Car extends Vehicle {
@override
void start() {
print('Car engine started');
}
}
class Bike extends Vehicle {
@override
void start() {
print('Bike engine started');
}
}
class ElectricCar extends Vehicle {
@override
void start() {
print('Electric motor started silently');
}
}
void main() {
List<Vehicle> vehicles = [
Car(),
Bike(),
ElectricCar(),
];
for (Vehicle vehicle in vehicles) {
vehicle.start();
}
}
19. Polymorphism in Flutter
Polymorphism is useful in Flutter when different classes share a common abstraction but provide different
implementations. Flutter's widget architecture also makes extensive use of class hierarchies and overridden
methods.
JustAcademy's Flutter curriculum includes Dart OOP concepts such as inheritance, polymorphism, and abstraction,
followed by Flutter Widgets and UI Design. :contentReference[oaicite:1]{index=1}
StatelessWidget Example
import 'package:flutter/material.dart';
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('Welcome to Flutter'),
),
);
}
}
In this example, WelcomeScreen extends StatelessWidget and provides its own
implementation of the inherited build() method.
20. Method Overriding in Flutter
A common pattern in Flutter is extending a framework class and overriding a method to provide application-specific
behavior.
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: Text('Home Page'),
),
);
}
}
The build() method is supplied by the inherited widget contract and is implemented specifically for
HomePage.
21. Polymorphism with Abstract Classes
Polymorphism is frequently used together with abstract classes. An abstract class can define a common contract
that child classes implement differently.
abstract class Payment {
void pay(double amount);
}
class UpiPayment extends Payment {
@override
void pay(double amount) {
print('Paid ₹$amount using UPI');
}
}
class CardPayment extends Payment {
@override
void pay(double amount) {
print('Paid ₹$amount using Card');
}
}
void main() {
List<Payment> payments = [
UpiPayment(),
CardPayment(),
];
for (Payment payment in payments) {
payment.pay(500);
}
}
The abstract class establishes a common operation, while each concrete child class supplies its own
implementation.
22. Method Overriding vs Method Overloading
These two terms are often confused, but they describe different concepts.
Feature |
Method Overriding |
Method Overloading |
|---|
Meaning |
Child class provides a different implementation of an inherited member. |
Multiple methods/functions use the same name with different parameter lists in languages that support traditional overloading. |
Inheritance Required? |
Yes, for inherited member overriding. |
Not necessarily. |
Dart Usage |
Supported. |
Dart does not provide traditional method overloading based only on different parameter lists. |
Example |
@override void sound() |
Use optional/named parameters or different method names instead. |
23. Method Overriding vs Inheritance
Concept |
Purpose |
|---|
Inheritance |
Allows a child class to reuse and extend functionality from a parent class. |
Method Overriding |
Allows a child class to replace inherited behavior with its own implementation. |
Polymorphism |
Allows the same interface or operation to work with objects having different implementations. |
24. Static Type vs Runtime Object
One important idea behind polymorphism is the distinction between the variable's declared type and the object's
actual runtime type.
Animal animal = Dog();
Here:
Animal is the declared/static type of the variable.
Dog() creates the actual object.
- The object is a
Dog.
- Overridden behavior is selected according to the actual object.
25. A Practical Polymorphic Function
Polymorphism becomes especially useful when a function accepts a parent type rather than a specific child type.
class Animal {
void sound() {
print('Animal sound');
}
}
class Dog extends Animal {
@override
void sound() {
print('Dog barks');
}
}
class Cat extends Animal {
@override
void sound() {
print('Cat meows');
}
}
void makeSound(Animal animal) {
animal.sound();
}
void main() {
makeSound(Dog());
makeSound(Cat());
}
Output:
Dog barks
Cat meows
The makeSound() function does not need separate versions for Dog and Cat.
It works with the common Animal type.
26. Practical Example: Delivery System
abstract class Delivery {
void deliver(String orderId);
}
class BikeDelivery extends Delivery {
@override
void deliver(String orderId) {
print('Order $orderId delivered by bike');
}
}
class TruckDelivery extends Delivery {
@override
void deliver(String orderId) {
print('Order $orderId delivered by truck');
}
}
class DroneDelivery extends Delivery {
@override
void deliver(String orderId) {
print('Order $orderId delivered by drone');
}
}
void processDelivery(
Delivery delivery,
String orderId,
) {
delivery.deliver(orderId);
}
void main() {
processDelivery(BikeDelivery(), 'ORD101');
processDelivery(TruckDelivery(), 'ORD102');
processDelivery(DroneDelivery(), 'ORD103');
}
This design makes it easy to add another delivery type without changing the function that processes deliveries.
27. Advantages of Polymorphism
- Flexibility: The same interface can work with different object types.
- Reusability: Common processing logic can be reused.
- Maintainability: Specialized behavior stays inside the relevant class.
- Extensibility: New child classes can be introduced with minimal changes to existing code.
- Loose Coupling: Code can depend on a common abstraction rather than concrete implementations.
- Cleaner Architecture: Different behaviors can be separated into specialized classes.
28. Advantages of Method Overriding
- Allows child classes to customize parent behavior.
- Supports runtime polymorphism.
- Encourages specialized implementations.
- Reduces the need for large conditional statements.
- Makes class hierarchies more flexible.
- Works naturally with abstract classes and interfaces.
29. Common Mistakes
Mistake 1: Forgetting @override
Prefer:
class Dog extends Animal {
@override
void sound() {
print('Dog barks');
}
}
Mistake 2: Changing the Contract Incorrectly
An overriding method needs to remain compatible with the inherited member's contract. Avoid changing the method
in a way that violates the expectations of code using the parent type.
Mistake 3: Confusing Overriding with Overloading
Overriding involves an inherited member and a subclass implementation. Traditional method overloading based only
on different parameter lists is not a Dart feature.
Mistake 4: Calling Child-Specific Members Through a Parent Type
Animal animal = Dog();
// animal.bark(); // Not available through Animal type
The variable's static type determines which members are directly accessible through that variable.
30. Best Practices
- Use
@override for overridden members.
- Design parent classes around meaningful common behavior.
- Use polymorphism when different classes need to satisfy a common contract.
- Prefer abstractions when application code should not depend on specific implementations.
- Keep each overridden method focused on the responsibility of its child class.
- Avoid deep and unnecessarily complicated inheritance hierarchies.
- Use
super when the parent implementation should also participate in the behavior.
- Use composition when inheritance does not represent a genuine "is-a" relationship.
31. Quick Revision Program
class Animal {
void sound() {
print('Animal sound');
}
}
class Dog extends Animal {
@override
void sound() {
print('Dog barks');
}
}
class Cat extends Animal {
@override
void sound() {
print('Cat meows');
}
}
void makeSound(Animal animal) {
animal.sound();
}
void main() {
List<Animal> animals = [
Dog(),
Cat(),
];
for (Animal animal in animals) {
makeSound(animal);
}
}
Output:
Dog barks
Cat meows
32. Polymorphism and Method Overriding – Quick Comparison
Topic |
Meaning |
Example |
|---|
Inheritance |
Child class receives and extends parent functionality. |
class Dog extends Animal |
Method Overriding |
Child class provides its own implementation of an inherited method. |
@override void sound() |
Polymorphism |
Same interface or operation can produce different behavior for different objects. |
Animal animal = Dog() |
super |
Accesses parent functionality. |
super.sound() |
@override |
Marks an intentional override. |
@override |
33. Practice Exercises
- Create an
Animal class with a sound() method.
- Create
Dog, Cat, and Cow classes that override sound().
- Create a parent
Vehicle class and override start() in Car and Bike.
- Create a
Payment class and implement UPIPayment, CardPayment, and CashPayment.
- Create a list of parent-type objects and call the overridden method for every object.
- Create a function that accepts a parent type and demonstrate polymorphism.
- Use
super inside an overridden method.
- Create an abstract
Shape class and implement Circle and Rectangle.
- Create a Flutter widget by extending
StatelessWidget and identify the overridden build() method.
- Build a small e-commerce example where different product classes override a common
displayDetails() method.
34. Key Takeaways
- Polymorphism means that one common interface can represent different behaviors.
- Method overriding allows a child class to provide its own implementation of an inherited method.
- The
@override annotation identifies an intentional override.
- The
super keyword can be used to call parent functionality.
- A parent-type variable can refer to an object of a child class.
- Runtime polymorphism allows the appropriate overridden implementation to execute for the actual object.
- Lists can contain objects of different child classes through a common parent type.
- Polymorphism helps reduce conditional logic and improve extensibility.
- Abstract classes can provide common contracts for polymorphic designs.
- Flutter uses class hierarchies and overridden methods extensively, so these Dart concepts are important for Flutter development.
35. Learn Flutter with JustAcademy
JustAcademy's Flutter training curriculum includes Dart Programming Fundamentals with Object-Oriented Programming,
classes, objects, constructors, inheritance, polymorphism, and abstraction. The curriculum then progresses to
Flutter widgets and UI design and further application-development topics. :contentReference[oaicite:2]{index=2}
View JustAcademy Flutter Training
Register for JustAcademy Course Demo