What is Method Overriding?
Method overriding lets a subclass provide its own implementation of a method that is already defined in the parent class. The subclass version replaces the parent version for objects of that subclass type. This is the mechanism that makes runtime polymorphism work in Dart.
@override Annotation
The @override annotation tells the Dart compiler that you intentionally override a parent member. It is technically optional, but strongly recommended — the compiler will warn you if the method doesn't actually exist in the parent class:
class Vehicle {
String make;
Vehicle(this.make);
String fuelType() => ''Petrol'';
String describe() => ''$make runs on \$${fuelType()}'';
}
class ElectricCar extends Vehicle {
int batteryCapacityKwh;
ElectricCar(String make, this.batteryCapacityKwh) : super(make);
@override
String fuelType() => ''Electricity''; // overrides parent
@override
String describe() =>
''$make is electric (\$${batteryCapacityKwh}kWh battery)'';
}
class HybridCar extends Vehicle {
HybridCar(String make) : super(make);
@override
String fuelType() => ''Petrol + Electricity'';
// describe() is NOT overridden — inherited from Vehicle
}
void main() {
var vehicles = [
Vehicle(''Toyota''),
ElectricCar(''Tesla'', 100),
HybridCar(''Prius''),
];
for (var v in vehicles) {
print(v.describe());
}
}
Output:Toyota runs on Petrol
Tesla is electric (100kWh battery)
Prius runs on Petrol + Electricity
Calling super
An overriding method can call the parent version using super.methodName(). This is useful for extending (not replacing) parent behavior:
class Logger {
void log(String message) {
print(''[LOG] $message'');
}
}
class TimestampLogger extends Logger {
@override
void log(String message) {
// Add timestamp, then call parent
final timestamp = DateTime.now().toIso8601String();
super.log(''[$timestamp] $message'');
}
}
class PrefixLogger extends Logger {
final String prefix;
PrefixLogger(this.prefix);
@override
void log(String message) {
super.log(''[$prefix] $message'');
}
}
void main() {
var tl = TimestampLogger();
tl.log(''Server started'');
// [LOG] [2024-01-15T10:30:00.000] Server started
var pl = PrefixLogger(''AUTH'');
pl.log(''User logged in'');
// [LOG] [AUTH] User logged in
}
Overriding Rules
- Return type: The overriding method can return a subtype of the parent return type (covariant return)
- Parameters: Must match the parent signature (same name, count, and types)
- Visibility: Cannot reduce visibility (cannot make a public method private)
- final methods: Cannot be overridden — Dart does not have
final for methods, but you can document intent
class Animal {
Animal create() => Animal();
}
class Dog extends Animal {
// Covariant return: Dog is a subtype of Animal — allowed
@override
Dog create() => Dog();
}
covariant Keyword
Normally, overriding parameter types must be the same or broader (contravariant). The covariant keyword allows a narrower type — useful for type-safe collections:
class Animal {
void eat(covariant Animal food) {
print(''$runtimeType eats \$${food.runtimeType}'');
}
}
class Dog extends Animal {
@override
void eat(Dog food) { // narrowed to Dog — allowed via covariant
print(''Dog eats dog food'');
}
}
Full Example: Employee Bonus System
class Employee {
final String name;
final double baseSalary;
Employee(this.name, this.baseSalary);
double calculateBonus() => baseSalary * 0.10;
void printPayslip() {
final bonus = calculateBonus();
print(''--- Payslip for $name ---'');
print(''Base Salary: \$\$${baseSalary.toStringAsFixed(2)}'');
print(''Bonus: \$\$${bonus.toStringAsFixed(2)}'');
print(''Total: \$\$${(baseSalary + bonus).toStringAsFixed(2)}'');
}
}
class Manager extends Employee {
final int teamSize;
Manager(String name, double salary, this.teamSize) : super(name, salary);
@override
double calculateBonus() => super.calculateBonus() + teamSize * 500;
}
class SalesRep extends Employee {
final double salesRevenue;
SalesRep(String name, double salary, this.salesRevenue) : super(name, salary);
@override
double calculateBonus() => salesRevenue * 0.05;
}
class Intern extends Employee {
Intern(String name, double salary) : super(name, salary);
@override
double calculateBonus() => 0; // no bonus for interns
}
void main() {
List<Employee> staff = [
Employee(''Alice'', 60000),
Manager(''Bob'', 90000, 8),
SalesRep(''Carol'', 55000, 200000),
Intern(''Dave'', 25000),
];
for (var e in staff) {
e.printPayslip();
print('''');
}
}
Output:--- Payslip for Alice ---
Base Salary: $60000.00
Bonus: $6000.00
Total: $66000.00
--- Payslip for Bob ---
Base Salary: $90000.00
Bonus: $13000.00
Total: $103000.00
--- Payslip for Carol ---
Base Salary: $55000.00
Bonus: $10000.00
Total: $65000.00
--- Payslip for Dave ---
Base Salary: $25000.00
Bonus: $0.00
Total: $25000.00
Exercise
- Create an
Animal class with a move() method. Override it in Fish (swims), Bird (flies), Snake (slithers). Store them in a List<Animal> and call move() on each.
- Create a logging hierarchy:
BaseLogger with log(message), FileLogger that calls super and also writes to a list, NetworkLogger that calls super and adds an HTTP prefix.
Quiz
1. What does the @override annotation do?
- A) Forces the method to be called at compile time
- B) Signals intentional replacement of a parent method and enables compiler checking ✅
- C) Prevents further overriding
- D) Calls the parent method automatically
2. How do you call the parent''s version of an overridden method?
- A) parent.method()
- B) base.method()
- C) super.method() ✅
- D) this.super.method()
3. What does covariant allow in a method parameter?
- A) A broader (parent) type
- B) A narrower (subtype) — normally disallowed in overrides ✅
- C) Any type
- D) Only nullable types
Summary
Method overriding lets subclasses replace parent methods with customized implementations. Always use @override to signal intent and enable compiler checking. Call super.method() from an override when you want to extend (not replace) parent behavior. Overriding return types can be narrowed (covariant return), and the covariant keyword allows narrowing parameter types. Overriding is the runtime engine of polymorphism.