Abstract Classes and Interfaces
When you start building bigger programs, you often want to say: "these things all share an ability, but each one does it differently." This chapter shows how Dart handles that promise with two tools: abstract classes and interfaces.
The Problem: Promises Without Details
Imagine a drawing program. It has circles, squares, and triangles. Every shape can compute its area — the space it covers — but the math is different for each one. The program itself doesn't care how a shape computes its area; it only needs to know that it can.
This is where an abstract class helps. An abstract class is a class that is never created directly. Instead, it exists to describe what its children must be able to do. It can include:
- Abstract methods — a method name and its input/output types, but no body. A child class must fill in the body.
- Concrete methods — normal methods with full bodies, shared by all children.
An abstract class is like a restaurant menu: it lists what must be available, but someone else (each child class) must actually cook the dishes.
Your First Abstract Class
The abstract keyword marks the class. Note that an abstract method has no braces — just a semicolon.
abstract class Shape {
double area(); // abstract: no body, children must provide one
// concrete: shared by all children
String describe() => 'A shape with area ${area().toStringAsFixed(2)}';
}
class Circle implements Shape {
final double radius;
Circle(this.radius);
@override
double area() => 3.14159 * radius * radius;
}
class Square implements Shape {
final double side;
Square(this.side);
@override
double area() => side * side;
}
void main() {
final shapes = [Circle(2), Square(3)];
for (final s in shapes) {
print(s.describe()); // works on any Shape
}
}Running this prints:
A shape with area 12.57
A shape with area 9.00The list holds both kinds of shapes side by side. The loop treats them the same, because both keep the promise of area().
Interfaces: Just a Contract
Here is a surprise about Dart: every class is also an interface. There is no special interface keyword needed. When you write implements Shape, you are saying "I will follow the Shape contract" — the list of method names and types that Shape publishes.
The implements keyword means: I promise to provide my own version of every member, including fields. Nothing is inherited from the parent — not even working code. You get only the contract.
Compare that with extends, which you met in the inheritance chapter. extends means "I am a child, and I inherit working code from my parent." You then choose which methods to override.
extends: inherit code, override what you like.implements: inherit nothing, promise everything.
Choosing Between extends and implements
This is the main decision you make here. Use the table to pick.
| You want... | Use |
|---|---|
| To reuse a parent's working code | `extends` |
| To keep a parent's state (fields) | `extends` |
| Only a shared contract, fully your own code | `implements` |
| To promise behavior across unrelated classes | `implements` |
| A mix: some code shared, some required | `extends` an abstract class |
abstract class Logger {
void log(String message); // must be filled in
// shared convenience, built on the abstract method
void warn(String message) => log('WARN: $message');
}
class ConsoleLogger extends Logger {
@override
void log(String message) => print(message);
}
void main() {
final logger = ConsoleLogger();
logger.warn('disk almost full'); // prints: WARN: disk almost full
}Because ConsoleLogger extends Logger, it only needs to write log; it inherits warn for free. That is the sweet spot of abstract classes: required methods plus free shared code.
Runtime Type Checks
Sometimes you receive a general value — say a Shape — and need to know what it really is. Dart lets you check at runtime with is, and type promotion (the compiler narrowing the type afterward) gives you access to the more specific members.
void printShape(Shape s) {
if (s is Circle) {
// promoted: the compiler now knows s is a Circle
print('circle with radius ${s.radius}');
}
}This check is real and safe. If s is not a Circle, the block simply does not run — no crash, no guessing.
Show diagram source
classDiagram
class Shape {
<<abstract>>
+area() double
+describe() String
}
class Circle {
+radius double
+area() double
}
class Square {
+side double
+area() double
}
Shape <|.. Circle : implements
Shape <|.. Square : implementsWhy Not Just Use Plain Classes?
You could make Shape a normal class and have children inherit. But then nothing stops someone from creating Shape() directly — a shape with no actual math behind it, whose area() does something meaningless. Declaring the class abstract turns that mistake into an error the compiler catches before the program ever runs. Catching mistakes early is the whole point.
The same reasoning applies to interfaces. If a function accepts a Shape, the compiler guarantees whatever you pass can compute an area. Without the contract, you would find out about the missing ability at the worst time — while the program is running.
Common Errors and How to Fix Them
Error: Concrete classes must implement...— you usedimplementsbut forgot to write a method from the contract. Add it, ideally with@overrideabove it so the checker confirms the signature matches.- Trying to create an abstract class —
Shape()fails. Create a child class instead. - Expecting inherited code with
implements—implementsgives you the contract only. If you want the parent's code, useextends. - Signature mismatch — if your override has different parameter types, the compiler complains. Copy the parent's signature exactly, or let your editor insert the override for you.
Where This Fits in Your Dart Toolkit
Abstract classes and interfaces are the foundation for everything that shares behavior. In the next chapters you will meet mixins, which share behavior without any parent-child relationship at all, and generics, which make contracts like Shape reusable for many types at once. Together these tools let you describe what your code promises and keep how it does it open to change — the heart of flexible programs.
Exercise
What is the key difference between extends and implements when relating to a parent class?
Show answer
extends inherits the parent's working code and fields, and lets you override only what you choose. implements inherits nothing; it makes you promise to provide your own version of every member of the parent's interface, including fields.
Exercise
Why would you mark the Shape class abstract instead of leaving it a normal class?
Show answer
To prevent creating useless instances directly and to force every child class to fill in the abstract methods like area(). The compiler then catches incomplete children before the program runs.
Exercise
In the code if (s is Circle) print(s.radius); why can the compiler allow access to s.radius inside the block?
Show answer
Because of type promotion. The is check confirms s is a Circle, so inside the block the compiler treats s as a Circle and allows access to its members.
Spotted a mistake, or code that does not work?