| |

Dart 23 🎯 Inheritance, Method Overriding, and Super Initializer Lists

Inheritance is the mechanism by which a class acquires the members of another class. It is the foundation of polymorphism in Dart, the way a Circle can be treated as a Shape, the way a Manager can be treated as an Employee. But inheritance in Dart has rules that differ from other object-oriented languages. There is no multiple inheritance. There is no extends keyword in interface declarations. Method overriding requires an annotation in some codebases and is enforced by the analyzer in others. And the superclass constructor must be called explicitly, either through a super parameter or a super initializer.

This chapter covers inheritance, method overriding, and the super constructor chain in Dart. You will learn how extends creates a subtype, how @override documents and validates method replacement, how the superclass constructor is invoked from the subclass, and how the super keyword accesses the superclass from within an instance. You will also see the rules that govern abstract classes, the covariant keyword, and the interaction between inheritance and constructors.

By the end, you will understand not just the syntax but the design principles: when inheritance is the right tool, when composition is better, and how to structure a class hierarchy that stays maintainable.

Key point: A subclass must call its superclass constructor. If the superclass has a default constructor, the call is implicit. If it has only named or parameterized constructors, the subclass must invoke one explicitly using : super(...) in its initializer list or super.param in its parameter list.


Why inheritance works this way

The subtype problem. Inheritance exists to express “is a” relationships. A Circle is a Shape. A Manager is an Employee. The subtype can be used wherever the supertype is expected, and it inherits the supertype’s interface. This is what makes polymorphism possible: a function that accepts a Shape can accept a Circle, call shape.area, and get the circle’s implementation.

The code reuse problem. Inheritance also provides code reuse. A subclass inherits fields and methods from its superclass without redefining them. A Manager inherits the name, email, and salary fields of Employee. This reduces duplication, though it can also create tight coupling if overused.

The constructor problem. Dart’s constructors are not inherited. A subclass does not automatically get its superclass’s constructors. This is deliberate: the subclass may have different initialization requirements. But it means the subclass must explicitly call a superclass constructor to initialize the inherited fields. The initializer list syntax : super(...) is where this happens.

The override problem. Method overriding replaces a superclass method with a subclass implementation. Dart requires the @override annotation in many style guides, and the analyzer warns if the annotated method does not actually override anything. This catches typos and signature mismatches that would otherwise produce silent bugs.

The multiple inheritance problem. Dart does not support multiple inheritance. A class can extend only one class. But it can implement multiple interfaces and use multiple mixins. This is a deliberate limitation that avoids the diamond problem and keeps the class hierarchy simple.


a. Extending a class

The extends keyword creates a subclass. The subclass inherits all non-private members of the superclass.

class Animal {
  String name;

  Animal(this.name);

  void speak() {
    print('$name makes a sound');
  }
}

class Dog extends Animal {
  Dog(super.name);

  void fetch() {
    print('$name fetches the ball');
  }
}

The Dog class extends Animal. It inherits the name field and the speak method. It adds a fetch method. The super.name parameter in the constructor forwards the argument to Animal‘s constructor.

A Dog instance can be used where an Animal is expected:

Animal pet = Dog('Rex');
pet.speak();  // OK: uses inherited method

But the fetch method is not available on the Animal type because it is specific to Dog. To call it, you need a Dog reference.

Private members are not inherited in the sense of accessibility. A private field _name in Animal is accessible in Dog only if Dog is in the same library. If Dog is in a different library, the private member is invisible.


b. Method overriding and the @override annotation

A subclass can replace a superclass method by declaring a method with the same name and signature. The @override annotation documents the intent and lets the analyzer verify it.

class Animal {
  String name;
  Animal(this.name);

  String describe() => 'Animal named $name';
}

class Dog extends Animal {
  Dog(super.name);

  @override
  String describe() => 'Dog named $name';
}

The describe method in Dog overrides the one in Animal. The @override annotation is not required, but it is strongly recommended. Without it, the analyzer does not warn if the method accidentally fails to override—for example, if the parameter types or return type are slightly different.

The override must be compatible with the superclass method. The parameter types can be the same or broader (contravariance), and the return type can be the same or narrower (covariance). Dart enforces this with its type system.

class Base {
  num get value => 0;
}

class Derived extends Base {
  @override
  int get value => 42;  // OK: int is a subtype of num
}

The covariant keyword allows a subclass to narrow a parameter type in an override. It is used sparingly and with care because it can break the Liskov substitution principle if misused.

A subclass can call the superclass implementation of an overridden method using super:

class Dog extends Animal {
  Dog(super.name);

  @override
  String describe() => '${super.describe()} (a dog)';
}

The super.describe() call invokes Animal‘s implementation. This is useful when the subclass wants to extend rather than replace the behavior.


c. Super initializer lists and super parameters

A subclass constructor must call a superclass constructor. If the superclass has a default constructor—one with no required parameters—the call is implicit. If the superclass has only named or parameterized constructors, the subclass must call one explicitly.

The super parameter syntax, introduced in Dart 2.17, is the modern way to forward constructor parameters to the superclass:

class Animal {
  final String name;
  Animal(this.name);
}

class Dog extends Animal {
  Dog(super.name);
}

The super.name parameter declares a parameter that is forwarded to the superclass constructor. It is concise and clear. This is the preferred form in modern Dart.

The older syntax uses an initializer list:

class Dog extends Animal {
  Dog(String name) : super(name);
}

Both forms are equivalent. The super parameter is shorter; the initializer list is more explicit and allows additional initialization.

When the subclass has its own fields to initialize, the initializer list handles both:

class Dog extends Animal {
  final String breed;

  Dog(String name, this.breed) : super(name);
}

The initializer list runs before the constructor body. The super call must come first if it is present, followed by field initializers. The order is: super initializer, then field initializers, then the constructor body.

Named super constructors are called the same way:

class Animal {
  final String name;
  Animal.named({required this.name});
}

class Dog extends Animal {
  Dog({required super.name});
}

The super.name parameter forwards to the named constructor. If the superclass constructor is named, the super parameter syntax still works as long as the parameter name matches.

Multiple super parameters can be forwarded:

class Animal {
  final String name;
  final int age;
  Animal(this.name, this.age);
}

class Dog extends Animal {
  Dog(super.name, super.age);
}

The order of super parameters matches the order of the superclass constructor’s parameters.


Complete Example Session

// ============================================
// PART 1: BASIC INHERITANCE
// ============================================
class Animal {
  final String name;
  Animal(this.name);

  void speak() => print('$name makes a sound');
}

class Dog extends Animal {
  Dog(super.name);

  void fetch() => print('$name fetches');
}
// ============================================
// PART 2: METHOD OVERRIDING
// ============================================
class Cat extends Animal {
  Cat(super.name);

  @override
  void speak() => print('$name meows');
}
// ============================================
// PART 3: CALLING SUPER METHOD
// ============================================
class Kitten extends Cat {
  Kitten(super.name);

  @override
  void speak() {
    super.speak();
    print('...softly');
  }
}
// ============================================
// PART 4: SUPER INITIALIZER LIST
// ============================================
class Vehicle {
  final String make;
  final String model;

  Vehicle(this.make, this.model);
}

class Car extends Vehicle {
  final int doors;

  Car(String make, String model, this.doors) : super(make, model);
}
// ============================================
// PART 5: SUPER PARAMETERS
// ============================================
class Truck extends Vehicle {
  final int payload;

  Truck(super.make, super.model, this.payload);
}
// ============================================
// PART 6: NAMED SUPER CONSTRUCTOR
// ============================================
class Engine {
  final int horsepower;

  Engine.named({required this.horsepower});
}

class SportsCar extends Engine {
  SportsCar({required super.horsepower});
}
// ============================================
// PART 7: ABSTRACT CLASS
// ============================================
abstract class Shape {
  double get area;
  double get perimeter;
}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);

  @override
  double get area => 3.14159 * radius * radius;

  @override
  double get perimeter => 2 * 3.14159 * radius;
}
// ============================================
// PART 8: OVERRIDE WITH COVARIANT
// ============================================
class AnimalShelter {
  void adopt(Animal animal) => print('Adopted ${animal.name}');
}

class DogShelter extends AnimalShelter {
  @override
  void adopt(covariant Dog dog) => print('Adopted dog ${dog.name}');
}
// ============================================
// PART 9: POLYMORPHISM
// ============================================
void describeShape(Shape shape) {
  print('Area: ${shape.area}, Perimeter: ${shape.perimeter}');
}

void main() {
  final shapes = <Shape>[
    Circle(1),
    Circle(2),
  ];
  shapes.forEach(describeShape);
}
// ============================================
// PART 10: SUPER IN OVERRIDDEN GETTER
// ============================================
class Base {
  String get description => 'Base';
}

class Derived extends Base {
  @override
  String get description => '${super.description} + Derived';
}

The ten parts covered basic inheritance, method overriding, calling super methods, super initializer lists, super parameters, named super constructors, abstract classes, the covariant keyword, polymorphism, and super in overridden getters.


Quick Reference

Inheritance Syntax

FeatureSyntax
Extend a classclass B extends A { }
Call super constructorB() : super();
Super parameterB(super.param);
Named super parameterB({required super.name});
Access super methodsuper.method();
Access super gettersuper.getter
Override annotation@override
Abstract classabstract class A { }
Abstract methodType method();

Constructor Rules

Superclass ConstructorSubclass Must
Default (no params)Nothing (implicit)
Positional paramsCall super(...) explicitly
Named paramsCall super.name(...)
Named constructorCall super.name(...)
No default constructorCall some super constructor

Override Rules

RuleRequirement
Method nameMust match
ParametersSame or broader
Return typeSame or narrower
@overrideRecommended
covariantNarrow parameter types
super accessAllowed

When to Use Inheritance

Use InheritanceUse Composition
True “is a” relationship“Has a” relationship
Shared interfaceShared implementation
Polymorphism neededDelegation needed
Subclass extends behaviorSubclass uses behavior

Best Practices

✅ Do This:

// Use @override for clarity and analyzer checks
@override
String toString() => 'Dog($name)';                            // ✅

// Use super parameters for concise forwarding
Dog(super.name, super.age);                                   // ✅

// Call super method to extend behavior
@override
void speak() {
  super.speak();
  print('...');
}                                                              // ✅

// Use abstract classes for interfaces with partial implementation
abstract class Shape {
  double get area;
}                                                              // ✅

// Prefer composition over inheritance when possible
class Car {
  final Engine engine;  // has-a
}                                                              // ✅

// Use covariant only when necessary
void adopt(covariant Dog dog) { }                              // ⚠️ use sparingly

❌ Don’t Do This:

// Don't forget to call super constructor
class Dog extends Animal {
  Dog(String name);  // ❌ if Animal has no default constructor
}                                                              // ❌

// Don't override without @override
class Cat extends Animal {
  void speak() => print('meow');  // ⚠️ no annotation
}                                                              // ⚠️

// Don't use inheritance for code reuse alone
class Stack extends List { }                                   // ❌ (is-a violation)

// Don't narrow return types incorrectly
@override
num get value => 0;  // ⚠️ if super returns int
}                                                              // ⚠️

// Don't use covariant to bypass type safety
void add(covariant Object x) { }                               // ❌

Common Pitfalls

PitfallWhy It HappensFix
Super constructor not calledSuper has no defaultAdd : super(...) or super.param
Override not recognizedSignature mismatchCheck parameter and return types
super undefinedNo superclass or wrong contextVerify extends clause
Abstract method not implementedSubclass is not abstractImplement or make subclass abstract
covariant breaks LSPNarrowed parameter typeUse only when sound
Private members not inheritedDifferent libraryKeep subclass in same library
Override changes behavior unexpectedlySuper not calledCall super.method() if extending

Real-World Examples

1. Basic Extension

class Manager extends Employee {
  Manager(super.name, super.salary);
}

2. Override toString

@override
String toString() => 'User($name, $email)';

3. Extend with Super Call

class PremiumUser extends User {
  PremiumUser(String name) : super(name, tier: 'premium');
}

4. Abstract Shape

abstract class Shape {
  double get area;
}

5. Covariant Parameter

@override
void compareTo(covariant Person other) { }

6. Super Getter

@override
String get label => '${super.label} (updated)';

7. Multi-Level Hierarchy

class A { }
class B extends A { }
class C extends B { }

8. Named Super Constructor

class Config extends BaseConfig {
  Config() : super.defaults();
}

9. Polymorphic Collection

List<Shape> shapes = [Circle(1), Square(2)];

10. Sealed Class Hierarchy

sealed class Result { }
class Success extends Result { }
class Failure extends Result { }

Visual

Inheritance Hierarchy

┌─────────────────────────────────────────────────────────────┐
│  CLASS HIERARCHY                                            │
│                                                             │
│                    ┌──────────┐                             │
│                    │  Animal  │                             │
│                    │  name    │                             │
│                    │  speak() │                             │
│                    └────┬─────┘                             │
│                         │                                   │
│            ┌────────────┼────────────┐                      │
│            │            │            │                      │
│            ▼            ▼            ▼                      │
│      ┌──────────┐ ┌──────────┐ ┌──────────┐                 │
│      │   Dog    │ │   Cat    │ │  Bird    │                 │
│      │ fetch()  │ │ speak()  │ │ fly()    │                 │
│      │          │ │ override │ │          │                 │
│      └──────────┘ └────┬─────┘ └──────────┘                 │
│                        │                                    │
│                        ▼                                    │
│                  ┌──────────┐                               │
│                  │  Kitten  │                               │
│                  │ speak()  │                               │
│                  │ override │                               │
│                  └──────────┘                               │
│                                                             │
│  Subclass inherits all non-private members.                 │
│  @override replaces behavior.                               │
│  super calls the parent implementation.                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Super Constructor Chain

┌─────────────────────────────────────────────────────────────┐
│  CONSTRUCTOR EXECUTION ORDER                                │
│                                                             │
│  class Animal {                                             │
│    final String name;                                       │
│    Animal(this.name) { print('Animal'); }                   │
│  }                                                          │
│                                                             │
│  class Dog extends Animal {                                 │
│    final String breed;                                      │
│    Dog(super.name, this.breed) { print('Dog'); }            │
│  }                                                          │
│                                                             │
│  Dog('Rex', 'Lab')                                          │
│    │                                                        │
│    ▼                                                        │
│  1. Animal constructor runs                                 │
│     - name initialized                                      │
│     - prints "Animal"                                       │
│    │                                                        │
│    ▼                                                        │
│  2. Dog constructor body runs                               │
│     - breed initialized                                     │
│     - prints "Dog"                                          │
│                                                             │
│  Superclass constructor always runs first.                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Method Override and Super

┌─────────────────────────────────────────────────────────────┐
│  OVERRIDE WITHOUT SUPER                                     │
│                                                             │
│  Animal.speak() → "Rex makes a sound"                       │
│  Dog.speak()    → "Rex barks"                               │
│                                                             │
│  Animal implementation is replaced entirely.                │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  OVERRIDE WITH SUPER                                        │
│                                                             │
│  Dog.speak():                                               │
│    super.speak();                                           │
│    print('...loudly');                                      │
│                                                             │
│  Output:                                                    │
│    "Rex makes a sound"                                      │
│    "...loudly"                                              │
│                                                             │
│  Animal implementation runs, then extension.                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Inheritance vs Composition

┌─────────────────────────────────────────────────────────────┐
│  INHERITANCE ("IS A")                                       │
│                                                             │
│  class Dog extends Animal { }                               │
│                                                             │
│  Dog IS AN Animal.                                          │
│  Dog inherits Animal's interface and implementation.        │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  COMPOSITION ("HAS A")                                      │
│                                                             │
│  class Car {                                                │
│    final Engine engine;                                     │
│  }                                                          │
│                                                             │
│  Car HAS AN Engine.                                         │
│  Car delegates to engine, does not inherit from it.         │
│                                                             │
│  Composition is more flexible and avoids tight coupling.    │
│  Prefer composition unless the "is a" relationship is true. │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Extend a classclass B extends A
Inherit membersAll non-private members
Override methodSame signature + @override
Call super methodsuper.method()
Call super gettersuper.getter
Super constructor: super(...) or super.param
Named super constructorsuper.name(...)
Abstract classCannot be instantiated
Abstract methodNo body; must be overridden
covariantNarrow parameter type
Multiple inheritanceNot supported
Prefer compositionWhen “has a” not “is a”

Key takeaways:

  • Inheritance expresses “is a” relationships. A Dog is an Animal. The subclass can be used wherever the superclass is expected, and it inherits the superclass’s interface and implementation.
  • Dart supports single inheritance only. A class can extend one class. It can implement multiple interfaces and use multiple mixins. This avoids the diamond problem and keeps hierarchies simple.
  • The superclass constructor must be called. If the superclass has a default constructor, the call is implicit. Otherwise, the subclass must call a super constructor explicitly with : super(...) or super.param.
  • Super parameters are the modern syntax. Dog(super.name) forwards name to the superclass constructor. It is concise and equivalent to Dog(String name) : super(name).
  • @override is recommended. It documents the intent and lets the analyzer verify that the method actually overrides something. Without it, typos and signature mismatches produce silent bugs.
  • super accesses the superclass implementation. Inside an overridden method, super.method() calls the parent’s version. This is how a subclass extends rather than replaces behavior.
  • Private members are not inherited across libraries. A private field in one library is invisible to a subclass in another library. Keep related classes in the same library if they need to share private state.
  • Prefer composition over inheritance. Inheritance creates tight coupling. If the relationship is not genuinely “is a,” composition—holding a reference to another object—is more flexible and easier to maintain.

Remember: Inheritance is a powerful tool for expressing type hierarchies and enabling polymorphism. But it is also a commitment: once a class extends another, it is coupled to the parent’s interface and implementation. Use it when the “is a” relationship is real and the hierarchy is stable. Use composition when the relationship is “has a” or when the hierarchy is likely to change. In Dart, the mechanics are straightforward: extends to inherit, @override to replace, super to access the parent, and : super(...) or super.param to initialize. The design decision—whether to use inheritance at all—is the part that matters most.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!