Popular Searches
Popular Course Categories
Popular Courses

Polymorphism and Method Overriding

Polymorphism and Method Overriding

Object-Oriented Programming in Dart

Polymorphism and Method Overriding in Dart – Detailed Notes with Practical Examples

Polymorphism and method overriding are important concepts of Object-Oriented Programming (OOP) in Dart. Polymorphism allows the same interface or parent type to work with objects of different child classes, while method overriding allows a child class to provide its own implementation of an inherited method.

These concepts are especially useful when building flexible and maintainable applications. JustAcademy's Flutter curriculum includes Object-Oriented Programming in Dart along with classes, objects, constructors, inheritance, polymorphism, and abstraction as part of Dart Programming Fundamentals. :contentReference[oaicite:0]{index=0}

Explore JustAcademy's Flutter Training

1. What is Polymorphism?

The word polymorphism comes from two words:

  • Poly – many
  • Morph – forms

Therefore, polymorphism means "many forms".

In Dart, polymorphism allows objects of different child classes to be treated through a common parent type while still providing their own specialized behavior.

For example, a Dog, Cat, and Cow can all be treated as Animal objects, but each animal can provide a different implementation of the sound() method.

2. Basic Polymorphism 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

Both variables have the parent type Animal, but they contain different child objects. The appropriate overridden method is executed according to the actual object.

3. What is Method Overriding?

Method overriding occurs when a child class provides a new implementation of a method inherited from its parent class.

The child class uses the @override annotation to indicate that it is overriding an inherited member.

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 that method with its own behavior.

4. Why is Method Overriding Used?

Method overriding is useful when a child class needs behavior that is more specific than the behavior provided by its parent class.

Common reasons include:

  • Providing specialized behavior.
  • Changing inherited behavior.
  • Implementing polymorphism.
  • Creating flexible application designs.
  • Allowing different child classes to respond differently to the same method call.

5. Simple Method Overriding Example

class Vehicle {
  void start() {
    print('Vehicle is starting');
  }
}

class Car extends Vehicle {
  @override
  void start() {
    print('Car engine is starting');
  }
}

void main() {
  Car car = Car();

  car.start();
}

Output:

Car engine is starting

The Car class replaces the inherited implementation of start().

6. Understanding @override

The @override annotation tells the Dart analyzer that the method is intended to override an inherited member.

class Animal {
  void sound() {
    print('Animal sound');
  }
}

class Dog extends Animal {
  @override
  void sound() {
    print('Dog bark');
  }
}

Using @override makes the programmer's intention clear and helps identify certain mistakes during development.

7. Polymorphism with Multiple Child Classes

One of the most useful forms of polymorphism is when multiple child classes inherit from the same parent and override the same method.

class Animal {
  void sound() {
    print('Animal 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 the implementation that gets executed depends on the actual object stored in a variable.

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 animal;

  animal = Dog();
  animal.sound();

  animal = Cat();
  animal.sound();
}

Output:

Dog barks
Cat meows

The same variable animal refers to different objects at different times.

9. Polymorphism Using a List

Polymorphism becomes particularly useful when multiple child objects are stored in a collection using the 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 design allows the program to work with all three child classes through the common Animal type.

10. Method Overriding with super

Sometimes a child class needs to keep the parent behavior and add additional behavior. In this situation, super can be used to call the parent implementation.

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

The first statement executes the parent implementation, while the second statement adds the child-specific behavior.

11. Practical Example: Employee Salary

Suppose an application has different types of employees. The parent class can define a common salary calculation method, while child classes can provide specialized implementations.

class Employee {
  String name;

  Employee(this.name);

  double calculateSalary() {
    return 30000;
  }
}

class Developer extends Employee {
  Developer(String name) : super(name);

  @override
  double calculateSalary() {
    return 60000;
  }
}

class Designer extends Employee {
  Designer(String name) : super(name);

  @override
  double calculateSalary() {
    return 50000;
  }
}

void main() {
  Employee developer = Developer('Ravi');
  Employee designer = Designer('Priya');

  print('${developer.name}: ₹${developer.calculateSalary()}');
  print('${designer.name}: ₹${designer.calculateSalary()}');
}

Output:

Ravi: ₹60000.0
Priya: ₹50000.0

The parent type Employee can represent different employee objects, while each child class provides its own implementation of calculateSalary().

12. Practical Example: Payment System

Polymorphism is useful when an application supports multiple payment methods.

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 processPayment(Payment payment, double amount) {
  payment.pay(amount);
}

void main() {
  processPayment(
    CreditCardPayment(),
    1500,
  );

  processPayment(
    UpiPayment(),
    2500,
  );

  processPayment(
    CashPayment(),
    1000,
  );
}

Output:

Paid ₹1500.0 using Credit Card
Paid ₹2500.0 using UPI
Paid ₹1000.0 using Cash

The processPayment() function accepts the parent type Payment, but it can work with different payment implementations.

13. Practical Example: Notification System

class Notification {
  void send(String message) {
    print('Sending notification: $message');
  }
}

class EmailNotification extends Notification {
  @override
  void send(String message) {
    print('Sending Email: $message');
  }
}

class SmsNotification extends Notification {
  @override
  void send(String message) {
    print('Sending SMS: $message');
  }
}

class PushNotification extends Notification {
  @override
  void send(String message) {
    print('Sending Push Notification: $message');
  }
}

void main() {
  List notifications = [
    EmailNotification(),
    SmsNotification(),
    PushNotification(),
  ];

  for (Notification notification in notifications) {
    notification.send('Welcome to our application');
  }
}

Output:

Sending Email: Welcome to our application
Sending SMS: Welcome to our application
Sending Push Notification: Welcome to our application

14. Practical Example: E-Commerce Products

An e-commerce application may have different product types. Each product can calculate its final price differently.

class Product {
  String name;
  double price;

  Product(this.name, this.price);

  double finalPrice() {
    return price;
  }
}

class ElectronicsProduct extends Product {
  ElectronicsProduct(
    String name,
    double price,
  ) : super(name, price);

  @override
  double finalPrice() {
    return price * 1.18;
  }
}

class BookProduct extends Product {
  BookProduct(
    String name,
    double price,
  ) : super(name, price);

  @override
  double finalPrice() {
    return price * 1.05;
  }
}

void main() {
  List products = [
    ElectronicsProduct('Laptop', 50000),
    BookProduct('Dart Book', 1000),
  ];

  for (Product product in products) {
    print(
      '${product.name}: ₹${product.finalPrice()}',
    );
  }
}

Output:

Laptop: ₹59000.0
Dart Book: ₹1050.0

15. Practical Example: Shape Calculation

Different shapes can implement the same area() method differently.

class Shape {
  double area() {
    return 0;
  }
}

class Circle extends Shape {
  double radius;

  Circle(this.radius);

  @override
  double area() {
    return 3.14 * radius * radius;
  }
}

class Rectangle extends Shape {
  double width;
  double height;

  Rectangle(this.width, this.height);

  @override
  double area() {
    return width * height;
  }
}

void main() {
  List shapes = [
    Circle(5),
    Rectangle(10, 5),
  ];

  for (Shape shape in shapes) {
    print('Area: ${shape.area()}');
  }
}

Output:

Area: 78.5
Area: 50.0

16. Polymorphism with a Function

A function can accept a parent type and therefore work with multiple child classes.

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());
}

Output:

Dog barks
Cat meows

This approach reduces the need to write separate functions for every child class.

17. Static Type and Runtime Object

Consider the following example:

Animal animal = Dog();

Here:

  • The static type of the variable is Animal.
  • The runtime object is a Dog.
  • The available members through the variable are determined by its static type.
  • For an overridden instance method, the implementation associated with the actual object is selected at runtime.

This is a key idea behind polymorphic behavior.

18. Polymorphism with Abstract Classes

Abstract classes are commonly used to define a common contract that child classes must implement.

abstract class Animal {
  void sound();
}

class Dog extends Animal {
  @override
  void sound() {
    print('Dog barks');
  }
}

class Cat extends Animal {
  @override
  void sound() {
    print('Cat meows');
  }
}

void main() {
  Animal dog = Dog();
  Animal cat = Cat();

  dog.sound();
  cat.sound();
}

Output:

Dog barks
Cat meows

The abstract parent class defines the method contract, while the child classes provide the concrete implementations.

19. Method Overriding with Getters

In Dart, inherited getters can also be overridden.

class Animal {
  String get type {
    return 'Animal';
  }
}

class Dog extends Animal {
  @override
  String get type {
    return 'Dog';
  }
}

void main() {
  Animal animal = Dog();

  print(animal.type);
}

Output:

Dog

20. Practical Example: Delivery System

Consider an application that supports different delivery methods.

class Delivery {
  void deliver(String order) {
    print('Delivering $order');
  }
}

class BikeDelivery extends Delivery {
  @override
  void deliver(String order) {
    print('Delivering $order by bike');
  }
}

class CarDelivery extends Delivery {
  @override
  void deliver(String order) {
    print('Delivering $order by car');
  }
}

class DroneDelivery extends Delivery {
  @override
  void deliver(String order) {
    print('Delivering $order by drone');
  }
}

void processDelivery(
  Delivery delivery,
  String order,
) {
  delivery.deliver(order);
}

void main() {
  processDelivery(
    BikeDelivery(),
    'Food Order',
  );

  processDelivery(
    CarDelivery(),
    'Grocery Order',
  );

  processDelivery(
    DroneDelivery(),
    'Express Order',
  );
}

Output:

Delivering Food Order by bike
Delivering Grocery Order by car
Delivering Express Order by drone

21. Method Overriding vs Method Overloading

Method overriding and method overloading are different concepts.

Feature Method Overriding Method Overloading
Meaning Child class provides a new implementation of an inherited method. Using the same method name with different parameter lists.
Inheritance Requires an inherited member. Does not inherently require inheritance.
Dart Support Supported. Traditional method overloading by parameter list is not supported.
Common Alternative in Dart Use @override. Use optional/named parameters or different method names.

22. Overriding vs Inheritance

Concept Purpose
Inheritance Allows a child class to inherit members from a parent class.
Method Overriding Allows a child class to provide its own implementation of an inherited member.
Polymorphism Allows common parent types to work with different child objects and their specialized behavior.

23. Polymorphism in Flutter

Flutter development makes extensive use of Dart's object-oriented features. The Flutter training curriculum at JustAcademy introduces Dart OOP concepts such as inheritance and polymorphism before moving into Flutter widgets and UI development. :contentReference[oaicite:1]{index=1}

Example: StatelessWidget

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 overrides the build() method.

The general idea is:

StatelessWidget
       |
       | extends
       v
WelcomeScreen
       |
       | overrides
       v
build()

24. Practical Flutter Example: Custom Buttons

Polymorphic thinking can also be useful when designing reusable Flutter components with common behavior.

import 'package:flutter/material.dart';

abstract class AppButton {
  Widget build();
}

class PrimaryButton extends AppButton {
  final String text;

  PrimaryButton(this.text);

  @override
  Widget build() {
    return ElevatedButton(
      onPressed: () {},
      child: Text(text),
    );
  }
}

class TextButtonWidget extends AppButton {
  final String text;

  TextButtonWidget(this.text);

  @override
  Widget build() {
    return TextButton(
      onPressed: () {},
      child: Text(text),
    );
  }
}

Both child classes provide their own implementation of build() while following the common AppButton abstraction.

25. Practical Example: User Roles

class User {
  String name;

  User(this.name);

  void showDashboard() {
    print('$name sees the user dashboard');
  }
}

class Admin extends User {
  Admin(String name) : super(name);

  @override
  void showDashboard() {
    print('$name sees the admin dashboard');
  }
}

class Customer extends User {
  Customer(String name) : super(name);

  @override
  void showDashboard() {
    print('$name sees the customer dashboard');
  }
}

void main() {
  List users = [
    Admin('Admin User'),
    Customer('Customer User'),
  ];

  for (User user in users) {
    user.showDashboard();
  }
}

Output:

Admin User sees the admin dashboard
Customer User sees the customer dashboard

26. Advantages of Polymorphism

  • Flexibility: The same parent type can work with different child objects.
  • Code Reusability: Common operations can be written once.
  • Loose Coupling: Code can depend on a common parent abstraction rather than specific implementations.
  • Extensibility: New child classes can often be introduced without changing code that works with the parent type.
  • Maintainability: Specialized behavior remains inside the appropriate child class.
  • Cleaner Architecture: Common contracts can be separated from specialized implementations.

27. Advantages of Method Overriding

  • Allows child classes to specialize inherited behavior.
  • Supports runtime polymorphic behavior.
  • Helps create flexible class hierarchies.
  • Allows a child class to modify parent behavior where appropriate.
  • Works naturally with abstract classes and interfaces.
  • Improves separation of responsibilities between parent and child classes.

28. Common Mistakes

Mistake 1: Forgetting @override

The annotation is recommended because it makes the intention explicit and helps the analyzer detect certain mistakes.

Mistake 2: Changing the Method Contract Incorrectly

When overriding a method, the child implementation must be compatible with the inherited member's contract. Changing parameters or return types incompatibly can cause errors.

Mistake 3: Confusing Overriding with Overloading

Overriding happens across an inheritance relationship. Traditional method overloading based only on different parameter lists is not supported in Dart.

Mistake 4: Using the Wrong Parent Type

class Animal {
  void sound() {
    print('Animal sound');
  }
}

class Dog extends Animal {
  @override
  void sound() {
    print('Dog bark');
  }
}

void main() {
  Animal animal = Dog();

  animal.sound();
}

The parent type must provide access to the member being called. Here, sound() is declared in Animal, so it can be called through the Animal reference.

Mistake 5: Overusing Inheritance

Polymorphism does not mean that every application relationship should be implemented through inheritance. Composition and other design techniques may be more appropriate depending on the problem.

29. Best Practices

  • Use @override whenever overriding an inherited member.
  • Keep overridden methods focused on the specialized behavior of the child class.
  • Use parent types when multiple child implementations share a meaningful common contract.
  • Use abstract classes when you need to define a common contract for related classes.
  • Prefer clear and meaningful class hierarchies.
  • Avoid unnecessarily deep inheritance structures.
  • Use composition when inheritance does not represent a genuine "is-a" relationship.
  • Keep common functionality in the parent class and specialized functionality in child classes.

30. Complete Practical Example

The following example combines inheritance, method overriding, polymorphism, and a parent-type collection.

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');
  }
}

class Tester extends Employee {
  Tester(String name) : super(name);

  @override
  void work() {
    print('$name is testing the application');
  }
}

void main() {
  List employees = [
    Developer('Ravi'),
    Designer('Priya'),
    Tester('Amit'),
  ];

  for (Employee employee in employees) {
    employee.work();
  }
}

Output:

Ravi is writing code
Priya is designing UI
Amit is testing the application

This is a practical example of polymorphism because one List contains objects of different child classes, and the overridden work() method behaves differently for each object.

31. Difference Between Parent Class, Child Class, Overriding, and Polymorphism

Concept Meaning Example
Parent Class Provides common properties and methods. Animal
Child Class Inherits and can extend parent functionality. Dog extends Animal
Method Overriding Child class provides a specialized implementation of an inherited method. @override void sound()
Polymorphism Common parent types can work with different child objects. Animal animal = Dog()

32. 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 main() {
  List animals = [
    Dog(),
    Cat(),
  ];

  for (Animal animal in animals) {
    animal.sound();
  }
}

Output:

Dog barks
Cat meows

33. Important Keywords and Concepts

Keyword / Concept Purpose Example
extends Creates a class inheritance relationship. class Dog extends Animal
@override Indicates that an inherited member is being overridden. @override void sound()
super References the superclass from a child class. super.sound()
Polymorphism Allows a common type to work with different object implementations. Animal animal = Dog()
Method Overriding Provides a specialized implementation in a child class. void sound()
Abstract Class Can define a common contract for subclasses. abstract class Animal

34. Practice Exercises

  1. Create an Animal parent class and override sound() in Dog.
  2. Create Dog, Cat, and Cow classes and demonstrate polymorphism.
  3. Create an Employee parent class with Developer and Designer child classes.
  4. Override a calculateSalary() method in each employee class.
  5. Create a Payment class with CreditCardPayment, UpiPayment, and CashPayment.
  6. Create a Notification parent class with Email, SMS, and Push notification classes.
  7. Create a Shape class with Circle and Rectangle subclasses.
  8. Store multiple child objects inside a List.
  9. Write a function that accepts a parent class and works with different child objects.
  10. Create an abstract class and implement its methods in multiple child classes.
  11. Create a Flutter widget that extends StatelessWidget and identify the overridden build() method.

35. Interview Questions

  1. What is polymorphism in Dart?
  2. What does the word polymorphism mean?
  3. What is method overriding?
  4. Why is @override used in Dart?
  5. What is the difference between inheritance and polymorphism?
  6. What is runtime polymorphism?
  7. Can a parent reference hold a child object?
  8. What happens when an overridden method is called through a parent reference?
  9. What is the purpose of super?
  10. Does Dart support traditional method overloading?
  11. How can polymorphism be implemented using a List?
  12. How are abstract classes related to polymorphism?
  13. How is method overriding used in Flutter widgets?
  14. What is the difference between method overriding and method overloading?
  15. What are the advantages of polymorphism?

36. Key Takeaways

  • Polymorphism means allowing objects to take different forms through a common type.
  • A parent type can refer to an object of a child class.
  • Method overriding allows a child class to provide its own implementation of an inherited method.
  • @override is used to indicate an overridden member.
  • super can be used to call the parent implementation.
  • Polymorphism is commonly demonstrated using parent references and child objects.
  • A List can contain objects of different child classes.
  • Different child classes can implement the same method differently.
  • Abstract classes can define common contracts for polymorphic implementations.
  • Traditional method overloading by parameter list is not supported in Dart.
  • Polymorphism improves flexibility and extensibility when used with appropriate class designs.
  • Understanding polymorphism and overriding is important for Dart OOP and Flutter development.

37. Learn Flutter with JustAcademy

JustAcademy's Flutter curriculum includes Dart Programming Fundamentals with Object-Oriented Programming, classes, objects, constructors, inheritance, polymorphism, and abstraction. The curriculum then moves into Flutter Widgets and UI Design. :contentReference[oaicite:2]{index=2}

View JustAcademy Flutter Training

Register for JustAcademy Course Demo

Previous Next
whatsapp