Inheritance in Dart

By Syncfusion®, Inc. · Last updated

How to build a child class on a parent with extends, override and use super, and when to build objects from parts instead, with aggregation and composition.

What Is Inheritance?

Inheritance = making a new class from an existing class. The new class gets everything the old one has, and adds more.

  • Parent class (superclass) = the class you start from.
  • Child class (subclass) = the new class built on top of it.
  • The child inherits the parent's fields and methods.
  • An "is-a" test tells you when to use it: a Student is a Person.

Simple lesson: inheritance lets you write shared code once, in the parent, and reuse it in every child.

A Real-World Example

Everyone in Green Valley School is a person with a name.

  • A student is a person who also has a roll number.
  • A teacher is a person who also has a subject.
  • A class monitor is a student who also has a duty.
  • Writing name and greet() again in every class would be a waste. The parent holds them once.

Click a class to see what it owns and what it inherits:

Every Run box on this page runs on the course's Dart runner in your browser.

In Dart, every class you write already has a parent: Object. That is where toString() and hashCode come from.

What You Will Learn

In this lesson, you will learn:

  1. How to create a child class with extends, and the kinds of inheritance.
  2. How to override methods and use super.
  3. What has-a relationships are.
  4. Aggregation and composition, and how they differ.
  5. When to choose composition over inheritance.

The extends Keyword

Creating a Child Class

class Child extends Parent { ... }

A Student is a Person

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

  void greet() => print('Hello, I am $name.');
}

class Student extends Person {
  int rollNo;
  Student(String name, this.rollNo) : super(name);

  void study() => print('$name (roll $rollNo) is studying.');
}

void main() {
  var s = Student('Asha', 12);
  s.greet();
  s.study();
  print(s.name);
}
  • Student never wrote greet() or name, but it has both.
  • : super(name) passes name up to the Person constructor. More on this below.

Inherited Fields and Methods

What a child gets from its parent:

GetsExample
Public fieldsname
Public methodsgreet()
Getters and settersget initials
Private _ members, if in the same file_secret

What a child does not get:

  • Constructors. Each class writes its own, and calls the parent's with super.
  • Static members. Use them with the parent's name: Person.count.

Simple lesson: extends copies the parent's abilities into the child, except constructors and statics.

Types of Inheritance

Single, Multi-Level and Hierarchical

TypeShapeExample
Singleone parent, one childPerson → Student
Multi-levela chainPerson → Student → Monitor
Hierarchicalone parent, many childrenPerson → Student and Person → Teacher

Multi-level and hierarchical together

class Person {
  String name;
  Person(this.name);
  void greet() => print('Hello, I am $name.');
}

class Student extends Person {
  Student(String name) : super(name);
  void study() => print('$name is studying.');
}

class Monitor extends Student {
  Monitor(String name) : super(name);
  void takeAttendance() => print('$name is taking attendance.');
}

class Teacher extends Person {
  Teacher(String name) : super(name);
  void teach() => print('$name is teaching.');
}

void main() {
  var m = Monitor('Kiran');
  m.greet();
  m.study();
  m.takeAttendance();
  Teacher('Mrs. Iyer').greet();
  print(m is Person);
  print(m is Teacher);
}

A Monitor has all three methods. And m is Person is true, because a monitor is a student, and a student is a person.

Multiple Inheritance: Not in Dart

Multiple inheritance = one class with two parents: class SportsCaptain extends Student, Athlete.

  • Dart does not allow this. A class has exactly one parent.
  • Two parents could both have a play() method. Which one should run? Dart avoids that confusion.
  • Instead, Dart gives you two tools:

    • interfaces with implements (see Polymorphism)
    • mixins with with (see Mixins)
class SportsCaptain extends Student, Athlete {}
// Error: Each class definition can have at most one extends clause.

Simple lesson: a Dart class has one parent, but that parent can have its own parent, and so on.

Overriding Methods

Using @override

Override = a child writes its own version of a method it inherited.

  • Same name, same parameters, a new body.
  • Put @override above it. It tells readers (and Dart) "this replaces a parent method".
  • If you spell the name wrong, @override makes Dart warn you.

Each animal speaks its own way

class Animal {
  String name;
  Animal(this.name);
  void speak() => print('$name makes a sound.');
}

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

  @override
  void speak() => print('$name says Woof!');
}

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

void main() {
  Dog('Tommy').speak();
  Cat('Billu').speak();
}

Dog overrides speak(). Cat does not, so it uses the parent's version.

Overriding toString()

Every class inherits toString() from Object. Override it to print something useful:

A friendly print for a student

class Student {
  String name;
  int marks;
  Student(this.name, this.marks);

  @override
  String toString() => 'Student($name, $marks)';
}

void main() {
  var s = Student('Ravi', 75);
  print(s);
  print([s, Student('Priya', 85)]);
}

@override is not required to make overriding work. But always write it.

It documents your intent, and it catches spelling mistakes like toSting().

Simple lesson: override when the child must do the job differently, and mark it with @override.

The super Keyword

Calling the Parent's Method

super = "the parent's version". Use it inside a child to reach what you replaced.

super.describe() runs the parent part first

  • Without super., calling describe() inside describe() would call itself forever.
  • super. lets the child add to the parent's work, instead of replacing it all.

Calling the Parent's Constructor

A child must build its parent part first. You pass the values with super(...):

super in the initializer list

class Person {
  String name;
  Person(this.name) {
    print('Person part built for $name');
  }
}

class Teacher extends Person {
  String subject;
  Teacher(String name, this.subject) : super(name) {
    print('Teacher part built: $subject');
  }
}

void main() {
  var t = Teacher('Mrs. Iyer', 'Science');
  print('${t.name} teaches ${t.subject}');
}

The parent's constructor body runs before the child's body.

Super Parameters

Super parameter = super.name in the child's parameter list. It passes the value straight to the parent. Shorter, same meaning.

super.name does the passing for you

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

class Student extends Person {
  final String house;
  Student(super.name, super.age, this.house);

  @override
  String toString() => '$name, $age, $house house';
}

void main() {
  print(Student('Meena', 13, 'Blue'));
}
Long wayShort way
Student(String name, int age, this.house) : super(name, age);Student(super.name, super.age, this.house);

Simple lesson: super.method() reuses the parent's method, and super(...) or super.x builds the parent part.

Designing Good Hierarchies

  • Put shared fields and methods in the parent. Put special ones in the child.
  • Keep chains short. Two or three levels is usually enough.
  • Use the "is-a" test. A Car is not an Engine, so Car extends Engine is wrong.
  • When the test is "has-a" (a car has an engine), use a field instead, as shown below.

Do not inherit just to borrow one method.

If Library extends Student only to reuse printName(), every library would also get rollNo and study(). That makes no sense.

Easy way to remember

  • extends = "is a kind of".
  • @override = "I do this my own way".
  • super.x() = "do it the parent's way too".
  • super.name in the constructor = "pass this up to the parent".

Simple lesson: inheritance is for real "is-a" families, kept short and simple.

What Are Aggregation and Composition?

Both are ways to build a big object out of smaller objects. They are "has-a" relationships.

  • Has-a relationship = one object keeps another object in a field. A school has classrooms.
  • Aggregation = the parts are shared. They were made outside, and they live on if the whole goes away.
  • Composition = the parts are owned. The whole makes them, and they go away with it.

Simple lesson: aggregation borrows parts, and composition owns its parts.

A Real-World Example

Look at Green Valley School.

  • The school has classrooms. Rooms 7A and 7B are built inside the school. Close the school, and the rooms are gone too. That is composition.
  • The school has teachers. Mrs. Iyer was a person before she joined, and she still is if the school closes. That is aggregation.

Close the school and watch which parts go with it:

Dart does not have special keywords for these ideas. You show them by where an object is made, and who keeps it.

Has-a Relationships

Is-a (from above) and has-a are different questions:

QuestionAnswerTool
Is a Student a Person?yesextends
Does a Student have a Bag?yesa field: Bag bag;
Is a Car an Engine?nodo not extend
Does a Car have an Engine?yesa field: Engine engine;

A student has a bag

class Bag {
  final String colour;
  final List<String> books = [];
  Bag(this.colour);
}

class Student {
  final String name;
  final Bag bag;
  Student(this.name, this.bag);

  void pack(String book) {
    bag.books.add(book);
    print('$name packed $book in the ${bag.colour} bag');
  }
}

void main() {
  var s = Student('Asha', Bag('blue'));
  s.pack('Maths');
  s.pack('Science');
  print(s.bag.books);
}

Simple lesson: if "X has a Y" sounds right, give X a field of type Y.

Aggregation

Aggregation = the whole holds a link to parts that exist on their own.

  • The parts are usually passed in from outside (as constructor or method arguments).
  • The same part can be shared by many wholes.
  • Removing the whole does not remove the parts.

Two clubs share the same student

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

class Club {
  final String title;
  final List<Student> members = [];
  Club(this.title);

  void join(Student s) => members.add(s);

  void show() => print('$title: ${members.map((m) => m.name).join(', ')}');
}

void main() {
  var asha = Student('Asha');
  var ravi = Student('Ravi');

  var chess = Club('Chess')..join(asha)..join(ravi);
  var music = Club('Music')..join(asha);

  chess.show();
  music.show();

  chess.members.clear();
  print('Chess club closed. Asha is still ${asha.name}.');
  music.show();
}
  • asha was made outside both clubs.
  • Clearing the chess club did not touch her. She is still in the music club.

Simple lesson: in aggregation, the whole only points to parts that someone else made.

Composition

Composition = the whole creates and owns its parts.

  • The parts are made inside the whole, often in its constructor.
  • Nobody else holds them. They are private to the whole.
  • When the whole is gone, the parts are gone too.

A timetable owns its periods

class Period {
  final String subject;
  final String time;
  Period(this.subject, this.time);
}

class Timetable {
  final String className;
  final List<Period> _periods = [];

  Timetable(this.className) {
    _periods.add(Period('Maths', '9:00'));
    _periods.add(Period('Science', '9:45'));
    _periods.add(Period('English', '10:30'));
  }

  void show() {
    print('Timetable for $className');
    for (var p in _periods) {
      print('  ${p.time}  ${p.subject}');
    }
  }
}

void main() {
  var t = Timetable('7A');
  t.show();
}
  • The Period objects are made inside Timetable.
  • _periods is private, so no outside code can take them away.
  • A period for 7A has no meaning without 7A's timetable.

A quick test: who called the constructor of the part?

  • The whole called it: composition.
  • Someone else called it, and passed the part in: aggregation.

Simple lesson: in composition, the whole builds its parts and keeps them to itself.

Comparing the Two

AggregationComposition
Ownershipshared, the whole only links to itowned by the whole
Who makes the part?outside codethe whole itself
Lifetimethe part lives on alonethe part ends with the whole
Shared by many wholes?yesno
School exampleschool has teachersschool has classrooms
Other examplea playlist has songsa house has rooms

Both in one program:

A playlist borrows songs, a house owns rooms

class Song {
  final String title;
  Song(this.title);
}

class Playlist {
  final String name;
  final List<Song> songs;
  Playlist(this.name, this.songs);
}

class Room {
  final String use;
  Room(this.use);
}

class House {
  final List<Room> _rooms = [Room('kitchen'), Room('bedroom')];
  int get roomCount => _rooms.length;
}

void main() {
  var anthem = Song('Jana Gana Mana');
  var morning = Playlist('Assembly', [anthem]);
  var sports = Playlist('Sports Day', [anthem]);
  print('${morning.name} and ${sports.name} share ${anthem.title}');

  var home = House();
  print('The house made its own ${home.roomCount} rooms');
}

Simple lesson: ask "can the part live alone?". Yes means aggregation. No means composition.

Inheritance or Composition?

Sometimes both could work. Which is better?

A phone built from parts, not from a parent

class Camera {
  void click() => print('Photo taken');
}

class Speaker {
  void play(String song) => print('Playing $song');
}

class Phone {
  final Camera _camera = Camera();
  final Speaker _speaker = Speaker();

  void takePhoto() => _camera.click();
  void playSong(String song) => _speaker.play(song);
}

void main() {
  var p = Phone();
  p.takePhoto();
  p.playSong('Jana Gana Mana');
}
  • A phone is not a camera. It has a camera. So composition fits.
  • With extends, a phone could only have one parent. With fields, it can have many parts.
  • Each part can be changed or tested on its own.
ChooseWhen
Inheritance (extends)a true "is-a", and the child really is a special kind of the parent
Composition (fields)a "has-a", or you want to mix many abilities

A common beginner mistake is class Phone extends Camera. It compiles, but now a phone is a camera, with every camera method on it. Later changes to Camera can break Phone in surprising ways.

Easy way to remember

  • is-a: inheritance.
  • has-a, borrowed: aggregation (passed in, shared).
  • has-a, owned: composition (made inside, private).
  • When in doubt, prefer composition.

Simple lesson: prefer building from parts, and use inheritance only for real "is-a" families.

TL;DR

  1. class Child extends Parent inherits the parent's fields and methods. One parent only.
  2. Override with @override. super.m() runs the parent's version. super.name passes values up.
  3. Has-a = one object keeps another in a field.
  4. Aggregation: parts made outside and shared. Composition: parts made inside and owned.
  5. Use inheritance for is-a, and prefer composition for has-a.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
class A extends B, CA class can have two parentsOne parent onlyA compile error.
Forgetting : super(name)The parent builds itselfPass the parent's required valuesA compile error: the parent needs arguments.
Calling describe() inside describe()It reaches the parentWrite super.describe()It calls itself forever: Stack Overflow.
Leaving out @overrideIt is only decorationIt catches spelling mistakesA typo makes a new method silently.
Car extends EngineInherit to reuse codeUse "is-a". A car has an engineA confusing class design.
Expecting constructors to be inheritedChildren get every memberEach class writes its own constructorA compile error when you call it.
class Car extends EngineInherit to reuseA car has an engine: use a fieldOdd methods appear on Car.
A public list of owned partsPublic is simplerKeep owned parts private: _periodsOutside code removes parts the whole needs.
Making shared parts inside the wholeEvery whole needs its own copyPass shared parts inTwo "Ashas" that should be one person.
Thinking Dart has aggregate or compose keywordsThey are language featuresThey are design ideas, built with fieldsConfusion when reading code.
Deep extends chains to collect abilitiesMore parents, more powerCombine parts with fieldsFragile, hard-to-change code.

Check Yourself

Exercises

What to Remember

  • extends makes a child class that reuses the parent's code.
  • Override with @override, and reach the parent with super.
  • A has-a relationship is a field that holds another object.
  • Aggregation shares parts, and composition owns them.
  • Keep inheritance for true is-a families, and build everything else from parts.

Next: Polymorphism. You will write plans that classes must follow, and see one call do different things for different objects.