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 their own
implementations of the same method while keeping code flexible, reusable, and maintainable.
These concepts are particularly useful when developing Flutter applications because Flutter
and Dart make extensive use of classes, inheritance, abstract classes, interfaces, and
overridden methods. JustAcademy's Flutter curriculum also includes Object-Oriented Programming,
inheritance, polymorphism, and abstraction as part of its Dart programming fundamentals.
:contentReference[oaicite:0]{index=0}
Course:
JustAcademy Flutter Training
Demo Registration:
Register for Flutter Course Demo
1. What is Polymorphism?
The word polymorphism comes from two Greek words:
- Poly = Many
- Morph = Forms
Therefore, polymorphism means "many forms."
In programming, polymorphism allows the same method, operation, or interface to behave
differently depending on the object that is using it.
For example, different animals can have a method called sound(), 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');
}
}
Here, sound() has the same name, but its behavior changes depending on whether the
object is a Dog or a Cat.
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 inherits the method from the parent class, but it can redefine the method
according to its own requirements.
class Animal {
void sound() {
print('Animal makes a sound');
}
}
class Dog extends Animal {
@override
void sound() {
print('Dog barks');
}
}
In this example:
Animal is the parent class.
Dog is the child class.
sound() is defined in the parent class.
Dog overrides sound().
@override indicates that the inherited method is being overridden.
3. Relationship Between Polymorphism and Method Overriding
Polymorphism and method overriding are closely related.
Method overriding allows a child class to provide a different implementation
of an inherited method, while polymorphism allows code to work with the
parent type while the actual behavior comes from the object's runtime 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 main() {
Animal animal1 = Dog();
Animal animal2 = Cat();
animal1.sound();
animal2.sound();
}
Output:
Dog barks
Cat meows
Although the variables are declared using the Animal type, the actual objects are
Dog and Cat. Therefore, the overridden methods are executed.
4. The @override Annotation
Dart provides the @override annotation to indicate that a method, getter, or
setter is overriding an inherited member.
class Vehicle {
void start() {
print('Vehicle starts');
}
}
class Car extends Vehicle {
@override
void start() {
print('Car starts with a key');
}
}
Using @override is recommended because it makes the developer's intention clear
and allows Dart's analyzer to detect incorrect overriding.
Important
The @override annotation does not create overriding by itself. The method must
actually correspond to an inherited member.
5. Basic Method Overriding Example
class Employee {
void work() {
print('Employee is working');
}
}
class Developer extends Employee {
@override
void work() {
print('Developer is writing code');
}
}
void main() {
Developer developer = Developer();
developer.work();
}
Output:
Developer is writing code
The Developer class inherits work() from Employee, but
provides its own implementation.
6. Why Do We Need Method Overriding?
Method overriding is useful when child classes need specialized behavior.
- To customize inherited behavior
- To implement polymorphism
- To create flexible application architecture
- To reuse common parent-class functionality
- To provide different implementations for different objects
- To make code easier to extend
7. Polymorphism with Multiple Child Classes
One of the most useful applications of polymorphism is working with multiple child classes
through a common parent type.
class Payment {
void pay() {
print('Processing payment');
}
}
class CreditCardPayment extends Payment {
@override
void pay() {
print('Payment using credit card');
}
}
class UpiPayment extends Payment {
@override
void pay() {
print('Payment using UPI');
}
}
class CashPayment extends Payment {
@override
void pay() {
print('Payment using cash');
}
}
void main() {
Payment payment1 = CreditCardPayment();
Payment payment2 = UpiPayment();
Payment payment3 = CashPayment();
payment1.pay();
payment2.pay();
payment3.pay();
}
Output:
Payment using credit card
Payment using UPI
Payment using cash
The same pay() method is used, but its implementation changes according to the
actual object.
8. Polymorphism with a List
Polymorphism becomes especially useful when multiple objects are stored in a collection using
their 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 is a powerful example of polymorphism because the loop does not need to know the exact
child class of every object.
9. Runtime Polymorphism
Runtime polymorphism occurs when the implementation that executes depends on the actual object
at runtime.
class Shape {
void draw() {
print('Drawing shape');
}
}
class Circle extends Shape {
@override
void draw() {
print('Drawing circle');
}
}
class Rectangle extends Shape {
@override
void draw() {
print('Drawing rectangle');
}
void main() {
Shape shape;
shape = Circle();
shape.draw();
shape = Rectangle();
shape.draw();
}
Here, the variable shape can refer to different objects, and the corresponding
overridden draw() method executes.
10. Using super with Method Overriding
Sometimes a child class wants to execute the parent's implementation and then add additional
behavior.
The super keyword can be used to access the parent class implementation.
class Employee {
void work() {
print('Employee is working');
}
}
class Developer extends Employee {
@override
void work() {
super.work();
print('Developer is writing Dart code');
}
}
void main() {
Developer developer = Developer();
developer.work();
}
Output:
Employee is working
Developer is writing Dart code
In this example, super.work() calls the implementation from the parent class.
11. Overriding Getters
Dart also allows inherited getters to be overridden.
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
12. Practical Example: Employee Salary
Suppose an application has different types of employees. Each employee can calculate salary
differently.
class Employee {
double calculateSalary() {
return 0;
}
}
class FullTimeEmployee extends Employee {
@override
double calculateSalary() {
return 50000;
}
}
class PartTimeEmployee extends Employee {
@override
double calculateSalary() {
return 25000;
}
}
void main() {
Employee employee1 = FullTimeEmployee();
Employee employee2 = PartTimeEmployee();
print(employee1.calculateSalary());
print(employee2.calculateSalary());
}
The application can work with the common Employee type while each employee type
calculates its salary differently.
13. Practical Example: E-Commerce Products
An e-commerce application may have different product types. Each product can calculate its
final price differently.
class Product {
double finalPrice() {
return 0;
}
}
class Electronics extends Product {
@override
double finalPrice() {
return 45000;
}
}
class Clothing extends Product {
@override
double finalPrice() {
return 2500;
}
}
class Grocery extends Product {
@override
double finalPrice() {
return 800;
}
}
void main() {
List<Product> products = [
Electronics(),
Clothing(),
Grocery(),
];
for (Product product in products) {
print(product.finalPrice());
}
}
Polymorphism makes it possible to process all products using the same
Product interface.
14. Practical Example: Notification System
class Notification {
void send() {
print('Sending notification');
}
}
class EmailNotification extends Notification {
@override
void send() {
print('Sending email notification');
}
}
class SmsNotification extends Notification {
@override
void send() {
print('Sending SMS notification');
}
}
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 one common type,
Notification, but can work with many different notification implementations.
15. Polymorphism with Abstract Classes
Polymorphism is commonly used with abstract classes.
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<Shape> shapes = [
Circle(5),
Rectangle(10, 5),
];
for (Shape shape in shapes) {
print(shape.calculateArea());
}
}
The abstract class defines the common behavior, while child classes provide their own
implementations.
16. Polymorphism in Flutter
Polymorphism and method overriding are important in Flutter because Flutter applications are
built around classes and widgets. For example, custom widgets commonly extend Flutter widget
classes and override methods such as build().
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'),
),
);
}
}
Here, WelcomeScreen extends StatelessWidget and provides its own
implementation of the inherited build() method.
This demonstrates how inheritance and overriding are used in everyday Flutter development.
17. Method Overriding Rules in Dart
- The child class must inherit from the parent class or another applicable supertype.
- The overridden member should correspond to an inherited member.
- The method signature must be compatible with the inherited method.
- The return type must be compatible with the inherited member.
- Use
@override to clearly indicate an override.
- Access restrictions and type compatibility must be respected.
- Constructors themselves are not overridden.
18. Method Overriding vs Method Overloading
Method Overriding |
Method Overloading |
|---|
Child class provides a new implementation of an inherited method. |
Traditionally means multiple methods with the same name but different parameter lists. |
Related to inheritance. |
Usually associated with compile-time method selection in languages that support it. |
Commonly used for polymorphism. |
Dart does not support traditional method overloading based only on different parameter lists. |
Uses inherited members. |
In Dart, optional parameters, named parameters, or different method names can be used to achieve similar API flexibility. |
Dart Example Using Optional Parameters
void greet(String name, [String? message]) {
if (message != null) {
print('$message, $name');
} else {
print('Hello, $name');
}
}
void main() {
greet('Rahul');
greet('Rahul', 'Welcome');
}
19. Polymorphism vs Inheritance
Inheritance |
Polymorphism |
|---|
Allows a class to inherit behavior from another class. |
Allows one common interface/type to represent different behaviors. |
Creates a parent-child relationship. |
Allows different implementations of common behavior. |
Uses mechanisms such as extends. |
Often works through overriding and common supertypes. |
Focuses on code reuse and relationships. |
Focuses on flexible behavior. |
20. Static Type vs Runtime Object
One important concept in polymorphism is understanding the difference between the variable's
declared type and the actual object assigned to it.
class Animal {
void sound() {
print('Animal sound');
}
}
class Dog extends Animal {
@override
void sound() {
print('Dog barks');
}
}
void main() {
Animal animal = Dog();
animal.sound();
}
The variable is declared as Animal, but the actual object is a Dog.
Therefore, the overridden Dog.sound() implementation is executed.
21. Polymorphic Function
A function can accept a parent type and work with objects of different child classes.
class Vehicle {
void drive() {
print('Vehicle is driving');
}
}
class Car extends Vehicle {
@override
void drive() {
print('Car is driving');
}
}
class Bike extends Vehicle {
@override
void drive() {
print('Bike is driving');
}
}
void startVehicle(Vehicle vehicle) {
vehicle.drive();
}
void main() {
startVehicle(Car());
startVehicle(Bike());
}
This approach reduces the need for separate functions for every individual vehicle type.
22. Practical Delivery System Example
abstract class Delivery {
void deliver();
}
class BikeDelivery extends Delivery {
@override
void deliver() {
print('Delivery by bike');
}
}
class TruckDelivery extends Delivery {
@override
void deliver() {
print('Delivery by truck');
}
}
class DroneDelivery extends Delivery {
@override
void deliver() {
print('Delivery by drone');
}
}
void processDelivery(Delivery delivery) {
delivery.deliver();
}
void main() {
processDelivery(BikeDelivery());
processDelivery(TruckDelivery());
processDelivery(DroneDelivery());
}
The delivery-processing function does not need to know the specific delivery implementation.
It only needs an object that follows the Delivery contract.
23. Advantages of Polymorphism
- Flexibility: The same interface can work with different implementations.
- Code Reusability: Common logic can work with multiple child classes.
- Maintainability: New implementations can often be added without changing existing client code.
- Scalability: Applications can support additional object types more easily.
- Loose Coupling: Code can depend on a common abstraction rather than a specific implementation.
- Cleaner Architecture: Large systems can be divided into well-defined responsibilities.
24. Common Mistakes
Mistake 1: Forgetting @override
Although Dart can recognize a valid override without the annotation, using
@override makes the intention clear and helps tooling detect mistakes.
Mistake 2: Incorrect Method Signature
class Parent {
void display(String name) {
print(name);
}
}
class Child extends Parent {
// Avoid changing the inherited contract incorrectly.
}
When overriding a method, make sure the new declaration remains compatible with the inherited
member.
Mistake 3: Confusing Overriding with Overloading
Overriding changes inherited behavior in a subtype. Dart does not provide traditional
same-name/different-parameter-list method overloading.
Mistake 4: Using Too Many Inheritance Layers
Deep inheritance hierarchies can make code harder to understand. Prefer simple inheritance
relationships and composition where appropriate.
25. Best Practices
- Use
@override when overriding inherited members.
- Keep overridden methods focused on the responsibility of the child class.
- Use meaningful parent abstractions.
- Prefer abstract classes or common interfaces when several classes share a contract.
- Avoid unnecessary inheritance.
- Use polymorphism to reduce repetitive conditional logic.
- Keep parent classes general and child classes specialized.
- Use composition instead of inheritance when there is no genuine "is-a" relationship.
- Keep method contracts clear and predictable.
26. Step-by-Step Example
Consider an application containing different types of users.
class User {
void login() {
print('User login');
}
}
class Admin extends User {
@override
void login() {
print('Admin login with additional security');
}
}
class Customer extends User {
@override
void login() {
print('Customer login');
}
}
void main() {
List<User> users = [
Admin(),
Customer(),
];
for (User user in users) {
user.login();
}
}
Output:
Admin login with additional security
Customer login
Step 1: User defines common behavior.
Step 2: Admin and Customer inherit from
User.
Step 3: Both child classes override login().
Step 4: A List<User> stores both objects.
Step 5: When login() is called, the appropriate implementation
executes for each object.
This is a practical example of polymorphism through method overriding.
27. Polymorphism in Real Flutter Architecture
The same concept can be useful when designing Flutter applications. For example, an application
can define a common repository or service contract and provide different implementations.
abstract class UserRepository {
void getUsers();
}
class ApiUserRepository extends UserRepository {
@override
void getUsers() {
print('Getting users from API');
}
}
class LocalUserRepository extends UserRepository {
@override
void getUsers() {
print('Getting users from local database');
}
}
void loadUsers(UserRepository repository) {
repository.getUsers();
}
void main() {
loadUsers(ApiUserRepository());
loadUsers(LocalUserRepository());
}
The application can work with UserRepository without tightly coupling the
calling code to one specific implementation.
28. Revision Table
Concept |
Meaning |
Dart Example |
|---|
Polymorphism |
One common type can represent objects with different behavior. |
Animal animal = Dog(); |
Method Overriding |
A child class provides its own implementation of an inherited member. |
@override void sound() |
Inheritance |
A child class derives behavior and members from a parent class. |
class Dog extends Animal |
super |
Accesses members of the superclass. |
super.work() |
@override |
Indicates that an inherited member is being overridden. |
@override |
Abstract Class |
Can define a common contract for subclasses. |
abstract class Shape |
29. Practice Exercises
- Create a parent class
Animal and child classes Dog, Cat, and Cow.
- Create and override a
sound() method.
- Store the objects in a
List<Animal>.
- Create a
Payment parent class and implement UPI, Card, and Cash payments.
- Create a
Shape abstract class and override calculateArea() for Circle and Rectangle.
- Create an employee system where different employee types calculate salary differently.
- Create a Flutter widget that extends
StatelessWidget and overrides build().
- Create a notification system using an abstract parent class and multiple implementations.
30. Key Takeaways
- Polymorphism means "many forms."
- It allows the same common type or interface to work with different object implementations.
- Method overriding allows a child class to redefine inherited behavior.
- The
@override annotation clearly identifies an overridden member.
- The
super keyword can be used to call the parent implementation.
- Polymorphism is especially useful when multiple child classes share a common parent or abstraction.
- Abstract classes can provide contracts that child classes implement.
- Polymorphism helps create flexible, reusable, and maintainable Dart and Flutter applications.
- Dart does not support traditional method overloading based solely on different parameter lists.
- Flutter itself makes extensive use of inheritance and overridden methods such as
build().
31. JustAcademy Flutter Learning Resources
For structured Flutter and Dart learning, explore the JustAcademy Flutter Training course.
Its published curriculum includes Dart OOP topics such as classes, objects, constructors,
inheritance, polymorphism, and abstraction. :contentReference[oaicite:1]{index=1}
Explore JustAcademy's Flutter Training Course
Register for JustAcademy's Course Demo