Understanding Polymorphism and Method Overriding in Dart
Polymorphism and method overriding are important concepts of Object-Oriented Programming (OOP) in Dart. They allow different classes to provide different implementations of the same method while still being accessed through a common parent type.
These concepts are especially useful in Flutter applications because Flutter and Dart rely heavily on classes, inheritance, abstract classes, interfaces, and overridden methods. JustAcademy's Flutter curriculum includes Object-Oriented Programming in Dart, inheritance, polymorphism, and abstraction as part of its Dart Programming Fundamentals module. Explore JustAcademy's Flutter Training Course .
1. What is Polymorphism?
The word polymorphism comes from two Greek words:
- Poly = Many
- Morph = Forms
Therefore, polymorphism means "one interface or reference, many forms of behavior."
In Dart, polymorphism allows a parent class reference to refer to objects of different child classes. When an overridden method is called, Dart executes the implementation belonging to the actual object.
Simple Example
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");
}
}
void main() {
Animal animal1 = Dog();
Animal animal2 = Cat();
animal1.sound();
animal2.sound();
}
Output:
Dog barks
Cat meows
Here, Animal is the common parent type, while Dog and Cat provide different implementations of sound().
2. What is Method Overriding?
Method overriding occurs when a child class provides its own implementation of a method that is already defined in its parent class.
The child class keeps the same method purpose but changes its behavior according to its own requirements.
Basic Syntax
class Parent {
void display() {
print("Parent method");
}
}
class Child extends Parent {
@override
void display() {
print("Child method");
}
}
The @override annotation tells Dart that the child class is intentionally overriding an inherited member.
3. Relationship Between Inheritance, Overriding, and Polymorphism
These three concepts are closely related but are not the same.
- Inheritance: Allows a child class to reuse and extend a parent class.
- Method overriding: Allows a child class to provide a new implementation of an inherited method.
- Polymorphism: Allows a common parent type to work with objects having different implementations.
The relationship can be represented as:
Parent Class
|
| extends
v
Child Class
|
| overrides method
v
Different Behavior
|
v
Polymorphism
4. Why Do We Use Polymorphism?
Polymorphism helps developers create flexible and reusable applications.
Major Benefits
- Reduces unnecessary conditional logic.
- Improves code reusability.
- Makes applications easier to extend.
- Supports loose coupling.
- Allows different objects to follow a common interface.
- Makes large applications easier to maintain.
- Works naturally with abstract classes and interfaces.
- Is useful for Flutter widgets, services, repositories, models, and application architecture.
5. Understanding the @override Annotation
Dart provides the @override annotation to indicate that a method, getter, or setter is overriding an inherited member.
class Animal {
void eat() {
print("Animal is eating");
}
}
class Dog extends Animal {
@override
void eat() {
print("Dog is eating");
}
}
Using @override is recommended because it makes the programmer's intention clear and helps catch mistakes when the inherited member does not match what is being overridden.
6. Basic Method Overriding Example
class Vehicle {
void start() {
print("Vehicle is starting");
}
}
class Car extends Vehicle {
@override
void start() {
print("Car starts with a key");
}
}
void main() {
Car car = Car();
car.start();
}
Output:
Car starts with a key
The Car class inherits from Vehicle, but it provides its own implementation of start().
7. Calling the Parent Method Using super
Sometimes a child class needs to extend the behavior of the parent method instead of completely replacing it. Dart provides super for this purpose.
class Animal {
void sound() {
print("Animal makes a sound");
}
}
class Dog extends Animal {
@override
void sound() {
super.sound();
print("Dog barks");
}
}
void main() {
Dog dog = Dog();
dog.sound();
}
Output:
Animal makes a sound
Dog barks
Here, super.sound() calls the implementation defined in the parent class.
8. Parent Reference and Child Object
One of the most important parts of polymorphism is that a variable declared using a parent type can hold an object created from a child class.
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
Although the variable is declared as Animal, the actual object is a Dog. Therefore, the overridden Dog.sound() implementation is executed.
9. Multiple Child Classes with Polymorphism
Multiple child classes can override the same parent method in different ways.
class Payment {
void pay() {
print("Making payment");
}
}
class CreditCardPayment extends Payment {
@override
void pay() {
print("Payment made using Credit Card");
}
}
class UpiPayment extends Payment {
@override
void pay() {
print("Payment made using UPI");
}
}
class CashPayment extends Payment {
@override
void pay() {
print("Payment made using Cash");
}
}
void main() {
Payment payment1 = CreditCardPayment();
Payment payment2 = UpiPayment();
Payment payment3 = CashPayment();
payment1.pay();
payment2.pay();
payment3.pay();
}
Output:
Payment made using Credit Card
Payment made using UPI
Payment made using Cash
The same pay() operation behaves differently depending on the actual object.
10. Polymorphism with a List
Polymorphism becomes especially useful when multiple child objects need to be processed through 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 animals = [
Dog(),
Cat(),
Cow()
];
for (Animal animal in animals) {
animal.sound();
}
}
Output:
Dog barks
Cat meows
Cow moos
This approach is useful because the loop does not need to know whether the object is a dog, cat, or cow. It only needs to know that every object is an Animal.
11. Practical Example: Employee and Developer
class Employee {
String name;
Employee(this.name);
void work() {
print("$name is working");
}
}
class Developer extends Employee {
Developer(String name) : super(name);
@override
void work() {
print("$name is writing code");
}
}
class Designer extends Employee {
Designer(String name) : super(name);
@override
void work() {
print("$name is designing UI");
}
}
void main() {
List employees = [
Developer("Rahul"),
Designer("Priya")
];
for (Employee employee in employees) {
employee.work();
}
}
Output:
Rahul is writing code
Priya is designing UI
The application can treat developers and designers as employees while allowing each class to perform its own version of work().
12. Practical Example: Notification System
class Notification {
void send() {
print("Sending notification");
}
}
class EmailNotification extends Notification {
@override
void send() {
print("Sending notification through Email");
}
}
class SmsNotification extends Notification {
@override
void send() {
print("Sending notification through SMS");
}
}
class PushNotification extends Notification {
@override
void send() {
print("Sending Push Notification");
}
}
void processNotification(Notification notification) {
notification.send();
}
void main() {
processNotification(EmailNotification());
processNotification(SmsNotification());
processNotification(PushNotification());
}
The function processNotification() accepts the common parent type Notification. It does not need separate functions for every notification type.
13. Practical Example: E-Commerce Product
class Product {
void calculatePrice() {
print("Calculating product price");
}
}
class ElectronicsProduct extends Product {
@override
void calculatePrice() {
print("Calculating electronics product price");
}
}
class ClothingProduct extends Product {
@override
void calculatePrice() {
print("Calculating clothing product price");
}
}
class GroceryProduct extends Product {
@override
void calculatePrice() {
print("Calculating grocery product price");
}
}
void main() {
List products = [
ElectronicsProduct(),
ClothingProduct(),
GroceryProduct()
];
for (Product product in products) {
product.calculatePrice();
}
}
This structure can be extended later by adding more product classes without changing the common processing code.
14. Practical Example: Shape and Area
Polymorphism is commonly used when different objects have a common operation but require different calculations.
abstract class Shape {
double calculateArea();
}
class Circle extends Shape {
double radius;
Circle(this.radius);
@override
double calculateArea() {
return 3.14 * radius * radius;
}
}
class Rectangle extends Shape {
double width;
double height;
Rectangle(this.width, this.height);
@override
double calculateArea() {
return width * height;
}
}
void main() {
List shapes = [
Circle(5),
Rectangle(10, 4)
];
for (Shape shape in shapes) {
print(shape.calculateArea());
}
}
Both Circle and Rectangle implement calculateArea(), but the calculation is different for each shape.
15. Polymorphism with Abstract Classes
Abstract classes are frequently used as a common contract for polymorphic objects.
abstract class Payment {
void makePayment();
}
class UpiPayment extends Payment {
@override
void makePayment() {
print("Payment made through UPI");
}
}
class CardPayment extends Payment {
@override
void makePayment() {
print("Payment made through Card");
}
}
void processPayment(Payment payment) {
payment.makePayment();
}
void main() {
processPayment(UpiPayment());
processPayment(CardPayment());
}
The Payment class defines what a payment object must provide, while individual classes define how the operation is performed.
16. Overriding Getters
Dart also allows inherited getters to be overridden.
class Person {
String get role {
return "Person";
}
}
class Teacher extends Person {
@override
String get role {
return "Teacher";
}
}
void main() {
Person person = Teacher();
print(person.role);
}
Output:
Teacher
17. Overriding Setters
Setters can also be overridden when a child class needs different behavior.
class Account {
double balance = 0;
set amount(double value) {
balance = value;
}
}
class SavingsAccount extends Account {
@override
set amount(double value) {
if (value >= 0) {
balance = value;
}
}
}
The child class can introduce additional validation while retaining the same conceptual property.
18. Polymorphic Functions
A function can accept a parent type and work with many different child objects.
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 makeAnimalSound(Animal animal) {
animal.sound();
}
void main() {
makeAnimalSound(Dog());
makeAnimalSound(Cat());
}
This is a clean example of polymorphism because the function does not need to know the exact child class.
19. Static Type vs Runtime Object
Understanding the difference between the declared type and the actual object is important for understanding polymorphism.
Animal animal = Dog();
- Static/declared type:
Animal
- Runtime object:
Dog
The variable can use members available through the Animal type, while overridden behavior can come from the actual Dog object.
20. Method Overriding vs Method Overloading
| Feature |
Method Overriding |
Method Overloading |
| Purpose |
Change inherited behavior |
Provide different ways to call an operation |
| Classes |
Usually involves parent and child classes |
Does not inherently require inheritance |
| Method name |
Same inherited member |
Usually the same name with different parameter signatures |
| Dart support |
Supported |
Traditional same-name parameter-list overloading is not supported |
| Common Dart alternative |
@override |
Optional/named parameters or separate method names |
Example of Dart's Optional Parameters Instead of Traditional Overloading
class Calculator {
int add(int a, int b, [int c = 0]) {
return a + b + c;
}
}
void main() {
print(Calculator().add(10, 20));
print(Calculator().add(10, 20, 30));
}
21. Method Overriding vs Inheritance
| Concept |
Meaning |
| Inheritance |
Allows a child class to inherit and extend a parent class. |
| Overriding |
Allows the child to replace or specialize inherited behavior. |
| Polymorphism |
Allows common code to work with different object implementations. |
22. Flutter Example of Method Overriding
Flutter uses inheritance and method overriding extensively. For example, custom widgets commonly extend Flutter widget classes and implement the required behavior.
import 'package:flutter/material.dart';
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Welcome"),
),
body: const Center(
child: Text("Hello Flutter"),
),
);
}
}
Here, WelcomeScreen extends StatelessWidget and overrides the build() method to define the widget's UI.
23. Flutter Example with Polymorphism
abstract class AppButton {
Widget buildButton();
}
class LoginButton extends AppButton {
@override
Widget buildButton() {
return const Text("Login");
}
}
class LogoutButton extends AppButton {
@override
Widget buildButton() {
return const Text("Logout");
}
}
Both button classes follow the same AppButton contract while providing different implementations.
24. Practical Delivery System Example
abstract class Delivery {
void deliver();
}
class BikeDelivery extends Delivery {
@override
void deliver() {
print("Delivering order by bike");
}
}
class TruckDelivery extends Delivery {
@override
void deliver() {
print("Delivering order by truck");
}
}
class DroneDelivery extends Delivery {
@override
void deliver() {
print("Delivering order by drone");
}
}
void processDelivery(Delivery delivery) {
delivery.deliver();
}
void main() {
processDelivery(BikeDelivery());
processDelivery(TruckDelivery());
processDelivery(DroneDelivery());
}
This design allows the delivery system to support new delivery methods without changing the processDelivery() function.
25. Practical Login/User Example
class User {
void accessDashboard() {
print("User dashboard");
}
}
class Admin extends User {
@override
void accessDashboard() {
print("Admin dashboard with management controls");
}
}
class Customer extends User {
@override
void accessDashboard() {
print("Customer dashboard with shopping options");
}
}
void openDashboard(User user) {
user.accessDashboard();
}
void main() {
openDashboard(Admin());
openDashboard(Customer());
}
The dashboard operation changes according to the actual user type.
26. A Complete Polymorphism Example
abstract class Employee {
String name;
Employee(this.name);
void calculateSalary();
}
class Developer extends Employee {
Developer(String name) : super(name);
@override
void calculateSalary() {
print("$name receives developer salary");
}
}
class Designer extends Employee {
Designer(String name) : super(name);
@override
void calculateSalary() {
print("$name receives designer salary");
}
}
class Manager extends Employee {
Manager(String name) : super(name);
@override
void calculateSalary() {
print("$name receives manager salary");
}
}
void processSalary(Employee employee) {
employee.calculateSalary();
}
void main() {
List employees = [
Developer("Amit"),
Designer("Neha"),
Manager("Rohit")
];
for (Employee employee in employees) {
processSalary(employee);
}
}
This example demonstrates inheritance, abstraction, method overriding, and polymorphism together.
27. Advantages of Method Overriding
- Allows specialized child-class behavior.
- Supports runtime polymorphic behavior.
- Improves flexibility of application design.
- Promotes code reuse through inheritance.
- Allows common parent-level APIs.
- Makes code easier to extend.
- Works well with abstract classes and interfaces.
- Is useful for scalable Flutter application architecture.
28. Advantages of Polymorphism
- Flexibility: The same operation can work with different object types.
- Reusability: Common processing logic can be written once.
- Maintainability: Changes can often be isolated to individual implementations.
- Extensibility: New child classes can be added without rewriting common logic.
- Loose coupling: Code can depend on an abstraction instead of a concrete implementation.
- Cleaner architecture: Different responsibilities can be represented by different classes.
29. Disadvantages and Limitations
- Excessive inheritance can make class relationships difficult to understand.
- Deep inheritance hierarchies can become difficult to maintain.
- Overriding too many methods can make behavior harder to trace.
- Incorrect use of inheritance can create unnecessary coupling.
- Developers must understand the parent contract before overriding its members.
30. Important Rules for Method Overriding in Dart
- The child class must inherit from or otherwise implement the relevant parent contract.
- The overridden member should be compatible with the inherited declaration.
- Use
@override to clearly indicate intentional overriding.
- Do not arbitrarily change the meaning of a method in a way that violates the parent contract.
- Use
super when the parent implementation also needs to run.
- Keep overridden methods focused on the responsibility represented by the parent API.
31. Common Mistakes
Mistake 1: Forgetting @override
Although Dart can override inherited members without requiring the annotation in every situation, using @override makes the intent explicit and helps catch errors.
Mistake 2: Incorrect Method Signature
class Parent {
void display(String name) {
print(name);
}
}
When overriding an inherited method, the child implementation must be compatible with the inherited member rather than arbitrarily changing its contract.
Mistake 3: Confusing Overriding with Overloading
Overriding changes inherited behavior. Traditional same-name method overloading based only on different parameter lists is not Dart's normal method model.
Mistake 4: Overusing Inheritance
Not every "is-a" or code-reuse problem should automatically be solved with inheritance. Composition can sometimes provide a cleaner design.
32. Inheritance vs Composition
Inheritance represents an is-a relationship, while composition generally represents a has-a relationship.
Inheritance Example
class Vehicle {
}
class Car extends Vehicle {
}
A car is a vehicle.
Composition Example
class Engine {
void start() {
print("Engine started");
}
}
class Car {
final Engine engine;
Car(this.engine);
}
A car has an engine.
Choosing between inheritance and composition depends on the actual relationship and design requirements.
33. Best Practices
- Use
@override whenever intentionally overriding an inherited member.
- Use meaningful parent abstractions.
- Keep parent classes focused.
- Do not create inheritance relationships only to reuse a small amount of code.
- Use abstract classes when defining common contracts is appropriate.
- Use polymorphic functions to reduce repetitive type-specific logic.
- Prefer composition when the relationship is "has-a" rather than "is-a".
- Keep overridden methods consistent with the expectations of the parent abstraction.
- Use
super when parent behavior should be preserved and extended.
34. Quick Revision Table
| Concept |
Description |
Example |
| Inheritance |
Child class inherits from parent class |
class Dog extends Animal |
| Overriding |
Child provides its own implementation |
@override void sound() |
| Polymorphism |
One parent type works with different child objects |
Animal animal = Dog(); |
super |
Accesses parent implementation |
super.sound() |
| Abstract class |
Provides a common abstraction/contract |
abstract class Payment |
@override |
Marks an inherited member as intentionally overridden |
@override |
35. Practice Exercises
- Create an
Animal parent class with a sound() method.
- Create
Dog, Cat, and Cow child classes.
- Override
sound() in every child class.
- Create a
Payment abstract class and implement UPI, Card, and Cash payments.
- Create an
Employee parent class and Developer, Designer, and Manager child classes.
- Store different employee objects in a
List.
- Create a
Shape abstraction with Circle and Rectangle implementations.
- Create a Flutter widget that extends
StatelessWidget and overrides build().
- Create a notification system using Email, SMS, and Push notification classes.
- Create a delivery system using Bike, Truck, and Drone delivery classes.
36. Key Takeaways
- Polymorphism means one common interface or parent type can represent different forms of behavior.
- Method overriding allows a child class to provide its own implementation of inherited behavior.
@override is used to clearly indicate an overridden member.
super can be used to call the parent implementation.
- A parent-type variable can refer to a child object.
- Different child objects can respond differently to the same method call.
- Lists containing a parent type can contain objects of multiple child classes.
- Abstract classes are useful for creating common contracts for polymorphic implementations.
- Polymorphism helps make applications flexible, reusable, and easier to extend.
- Flutter development uses inheritance and overriding extensively, including widget classes and their methods.
37. JustAcademy Flutter Resources
JustAcademy's Flutter Training curriculum includes Dart OOP concepts such as classes, objects, constructors, inheritance, polymorphism, and abstraction. :contentReference[oaicite:0]{index=0}
Flutter Training Course: https://www.justacademy.co/course-detail/flutter-training
Register for Course Demo: https://www.justacademy.co/register-for-course-demo