Overview
Dart does not have a separate interface keyword — every class can serve as both a superclass and an interface. Understanding when to use extends (inherit from an abstract class) vs implements (adopt a class as a pure interface contract) is a critical design decision in Dart OOP.
Abstract Classes (extends)
Use abstract class + extends when:
- You want to share code (concrete methods) with subclasses
- You have a true "is-a" relationship (Dog IS AN Animal)
- You want to enforce a template while providing defaults
abstract class Vehicle {
String make;
int year;
Vehicle(this.make, this.year);
// Abstract — must override
double fuelEfficiency();
// Concrete — shared by all subclasses
int age() => DateTime.now().year - year;
String describe() => ''$make ($year), efficiency: \$${fuelEfficiency()}mpg, age: \$${age()}yr'';
}
class GasCar extends Vehicle {
final double mpg;
GasCar(String make, int year, this.mpg) : super(make, year);
@override double fuelEfficiency() => mpg;
}
class ElectricCar extends Vehicle {
final int rangeMiles;
final double kwhPer100Miles;
ElectricCar(String make, int year, this.rangeMiles, this.kwhPer100Miles) : super(make, year);
@override double fuelEfficiency() => rangeMiles / kwhPer100Miles;
}
void main() {
List<Vehicle> fleet = [
GasCar(''Toyota'', 2020, 32.5),
ElectricCar(''Tesla'', 2023, 358, 25.0),
];
for (var v in fleet) print(v.describe());
}
Interfaces (implements)
Use implements when:
- You want to enforce a contract without sharing code
- The relationship is "can-do" rather than "is-a"
- You need a class to fulfill multiple contracts
- You want to mock/stub in tests
abstract class Cacheable {
String get cacheKey;
Duration get ttl;
}
abstract class Serializable {
Map<String, dynamic> toJson();
String toJsonString();
}
abstract class Validatable {
List<String> validate();
bool get isValid => validate().isEmpty;
}
class UserProfile implements Cacheable, Serializable, Validatable {
final int id;
final String name;
final String email;
UserProfile(this.id, this.name, this.email);
@override String get cacheKey => ''user:$id'';
@override Duration get ttl => const Duration(hours: 1);
@override
Map<String, dynamic> toJson() => {''id'': id, ''name'': name, ''email'': email};
@override
String toJsonString() => toJson().toString();
@override
List<String> validate() {
var errors = <String>[];
if (name.trim().isEmpty) errors.add(''Name is required'');
if (!email.contains(''@'')) errors.add(''Invalid email'');
return errors;
}
}
void main() {
var user = UserProfile(1, ''Alice'', ''alice@example.com'');
print(user.cacheKey); // user:1
print(user.isValid); // true
print(user.toJsonString()); // {id: 1, name: Alice, email: alice@example.com}
}
Side-by-Side Comparison
| Feature | Abstract Class (extends) | Interface (implements) |
| Keyword | abstract class + extends | Any class + implements |
| Code sharing | Yes — concrete methods inherited | No — must implement everything |
| Multiple | One parent only | Many interfaces allowed |
| Relationship | "is-a" | "can-do" |
| Constructor | Inherited via super() | Not inherited |
| Typical use | Template Method pattern | Dependency injection, testing |
When to Use Which
Use abstract class + extends when:
- Subclasses share significant code (DRY principle)
- The hierarchy represents a true taxonomy (Animal → Dog)
- You use the Template Method pattern
Use implements when:
- You need multiple inheritance of behavior contracts
- You want to decouple callers from implementations
- You need to mock/test with fake implementations
- Different classes share a capability, not an identity
Full Example: Data Layer Design
// Interface — contract only
abstract class DataSource {
Future<Map<String, dynamic>?> get(String key);
Future<void> set(String key, Map<String, dynamic> value);
Future<void> delete(String key);
}
// Abstract class — shared caching logic
abstract class CachingRepository {
final Map<String, dynamic> _cache = {};
dynamic getFromCache(String key) => _cache[key];
void putInCache(String key, dynamic value) => _cache[key] = value;
void invalidateCache(String key) => _cache.remove(key);
void clearCache() => _cache.clear();
}
// Combines both: inherits cache code, fulfills DataSource contract
class InMemoryDataSource extends CachingRepository implements DataSource {
@override
Future<Map<String, dynamic>?> get(String key) async {
return getFromCache(key) as Map<String, dynamic>?;
}
@override
Future<void> set(String key, Map<String, dynamic> value) async {
putInCache(key, value);
}
@override
Future<void> delete(String key) async {
invalidateCache(key);
}
}
// A function that works with any DataSource (polymorphic)
Future<void> saveUser(DataSource ds, Map<String, dynamic> user) async {
await ds.set(''user:\$${user[''id'']}'', user);
print(''Saved user \$${user[''name'']}'');
}
void main() async {
DataSource ds = InMemoryDataSource();
await saveUser(ds, {''id'': 1, ''name'': ''Alice''});
var result = await ds.get(''user:1'');
print(result);
}
Output:Saved user Alice
{id: 1, name: Alice}
Exercise
- Design a media player: abstract class
MediaPlayer with shared volume management. Interfaces Streamable and Downloadable. Create PremiumPlayer that extends MediaPlayer and implements both interfaces.
- Create an abstract
BaseController with shared request parsing logic. Define interfaces Authenticatable and Rateable. Implement UserController.
Quiz
1. What is the key difference between extends and implements in Dart?
- A) extends is for classes; implements is for interfaces only
- B) extends inherits code; implements adopts the contract without inheriting code ✅
- C) implements is slower
- D) There is no difference
2. How many classes can a Dart class extend?
- A) Unlimited
- B) Two
- C) One ✅
- D) Zero — Dart doesn''t use extends
3. When is implements preferred over extends?
- A) When you need code reuse
- B) When you need multiple contracts, dependency injection, or test mocking ✅
- C) When the parent has a constructor
- D) Always
Summary
Use abstract class + extends when subclasses share significant code and have a true "is-a" relationship. Use implements when you need to enforce a pure contract (can-do relationship), support multiple interfaces, or enable easy mocking in tests. Dart lets you combine both: a class can extends one abstract class while also implements multiple interfaces. This flexibility makes Dart's OOP system expressive and practical for real-world architecture.