Interfaces
Understand how to define and implement interfaces in Dart.
What is it?
Interfaces define a contract that classes must follow. In Dart, every class implicitly defines an interface, meaning you can implement any class as an interface. Interfaces specify which methods and properties a class must have, enabling polymorphism and loose coupling.
Why does it exist?
Interfaces exist to:
- Define contracts for classes to implement
- Enable polymorphism without inheritance
- Support multiple interface implementation
- Create loose coupling between components
- Facilitate testing through mocking
- Design clean and maintainable APIs
Basic Interfaces
Implementing an Interface
// Interface (defined as a class)
class Flyable {
void fly() {
// No implementation needed, but can have default
}
String get wings => '';
}
// Class implementing the interface
class Bird implements Flyable {
@override
void fly() {
print('Bird flying...');
}
@override
String get wings => 'Feathered wings';
}
// Another implementation
class Airplane implements Flyable {
@override
void fly() {
print('Airplane flying...');
}
@override
String get wings => 'Metal wings';
}
// Usage
void makeItFly(Flyable flyable) {
flyable.fly();
print('Wings: ${flyable.wings}');
}
var bird = Bird();
var plane = Airplane();
makeItFly(bird); // Bird flying... Wings: Feathered wings
makeItFly(plane); // Airplane flying... Wings: Metal wings
Interface with Multiple Methods
// Interface with multiple methods
class Drawable {
void draw() {}
void erase() {}
void resize(double factor) {}
String get description => '';
}
class Rectangle implements Drawable {
double width;
double height;
Rectangle(this.width, this.height);
@override
void draw() {
print('Drawing rectangle $width x $height');
}
@override
void erase() {
print('Erasing rectangle');
}
@override
void resize(double factor) {
width *= factor;
height *= factor;
print('Resized to $width x $height');
}
@override
String get description => 'Rectangle($width x $height)';
}
class Circle implements Drawable {
double radius;
Circle(this.radius);
@override
void draw() {
print('Drawing circle with radius $radius');
}
@override
void erase() {
print('Erasing circle');
}
@override
void resize(double factor) {
radius *= factor;
print('Resized to radius $radius');
}
@override
String get description => 'Circle(radius $radius)';
}
// Usage
void processDrawable(Drawable drawable) {
print(drawable.description);
drawable.draw();
drawable.resize(2);
drawable.erase();
print('---');
}
var rect = Rectangle(10, 20);
var circle = Circle(5);
processDrawable(rect);
processDrawable(circle);
Multiple Interfaces
Implementing Multiple Interfaces
// Multiple interfaces
class JsonSerializable {
Map<String, dynamic> toJson();
}
class Validatable {
bool isValid();
}
class Renderable {
void render();
}
// Class implementing multiple interfaces
class User implements JsonSerializable, Validatable, Renderable {
String name;
int age;
String email;
User(this.name, this.age, this.email);
// From JsonSerializable
@override
Map<String, dynamic> toJson() {
return {
'name': name,
'age': age,
'email': email,
};
}
// From Validatable
@override
bool isValid() {
return name.isNotEmpty && age > 0 && email.contains('@');
}
// From Renderable
@override
void render() {
print('Rendering user: $name ($age)');
}
}
// Usage
var user = User('Alice', 25, 'alice@example.com');
// Use as JsonSerializable
Map<String, dynamic> json = user.toJson();
print(json); // {name: Alice, age: 25, email: alice@example.com}
// Use as Validatable
print(user.isValid()); // true
// Use as Renderable
user.render(); // Rendering user: Alice (25)
// Polymorphic usage
void processJson(JsonSerializable item) {
print(item.toJson());
}
void validate(Validatable item) {
print('Valid: ${item.isValid()}');
}
processJson(user); // {name: Alice, age: 25, email: alice@example.com}
validate(user); // Valid: true
Interface Inheritance
Extending Interfaces
// Base interface
class Animal {
void eat() {}
void sleep() {}
String get name => '';
}
// Extended interface
class Mammal implements Animal {
@override
void eat() => print('Mammal eating');
@override
void sleep() => print('Mammal sleeping');
@override
String get name => 'Mammal';
void nurse() => print('Nursing young');
}
// Extended interface with additional methods
class Bird implements Animal {
@override
void eat() => print('Bird eating');
@override
void sleep() => print('Bird sleeping');
@override
String get name => 'Bird';
void fly() => print('Bird flying');
}
// Usage
void processAnimal(Animal animal) {
print(animal.name);
animal.eat();
animal.sleep();
// Check for specific interface
if (animal is Mammal) {
animal.nurse();
}
if (animal is Bird) {
animal.fly();
}
print('---');
}
var mammal = Mammal();
var bird = Bird();
processAnimal(mammal);
// Mammal
// Mammal eating
// Mammal sleeping
// Nursing young
// ---
processAnimal(bird);
// Bird
// Bird eating
// Bird sleeping
// Bird flying
// ---
Abstract Class vs Interface
Comparison
// Abstract class (can have implementation)
abstract class Database {
// Abstract method
Future<void> connect();
// Concrete method
Future<void> disconnect() async {
print('Disconnecting...');
}
}
// Interface (implicit, no implementation)
class Repository {
Future<void> save(String data);
Future<String> load(String id);
}
// Class extending abstract class and implementing interface
class MySQLDatabase extends Database implements Repository {
@override
Future<void> connect() async {
print('Connecting to MySQL...');
}
@override
Future<void> save(String data) async {
print('Saving to MySQL: $data');
}
@override
Future<String> load(String id) async {
print('Loading from MySQL: $id');
return 'Data for $id';
}
// Can override concrete method from abstract class
@override
Future<void> disconnect() async {
print('Disconnecting from MySQL...');
}
}
// Usage
var db = MySQLDatabase();
await db.connect(); // Connecting to MySQL...
await db.save('user_data'); // Saving to MySQL: user_data
await db.disconnect(); // Disconnecting from MySQL...
Interface with Default Implementation
Providing Default Behavior
// Interface with default implementation
class Logger {
// Default implementation
void log(String message) {
print('${DateTime.now()}: $message');
}
void logError(String message) {
log('ERROR: $message');
}
void logWarning(String message) {
log('WARNING: $message');
}
}
// Class using default implementation
class ConsoleLogger implements Logger {
// Uses default implementation from Logger
// Can override if needed
}
// Class with custom implementation
class FileLogger implements Logger {
@override
void log(String message) {
// Write to file instead of console
print('Writing to file: $message');
}
@override
void logError(String message) {
log('ERROR: $message');
// Additional error handling
}
}
// Usage
var consoleLogger = ConsoleLogger();
consoleLogger.log('App started'); // 2024-01-01 00:00:00.000: App started
var fileLogger = FileLogger();
fileLogger.log('App started'); // Writing to file: App started
Best Practices
Use Interfaces for Loose Coupling
// Good: Using interface for dependency injection
class UserService {
final Repository _repository;
UserService(this._repository);
Future<void> saveUser(User user) async {
await _repository.save(user.toJson());
}
}
// Any class implementing Repository can be used
class MySQLRepository implements Repository {
@override
Future<void> save(Map<String, dynamic> data) async {
print('Saving to MySQL: $data');
}
}
class InMemoryRepository implements Repository {
@override
Future<void> save(Map<String, dynamic> data) async {
print('Saving in memory: $data');
}
}
var service = UserService(MySQLRepository());
await service.saveUser(user);
Keep Interfaces Focused
// Good: Focused interfaces
class Logger {
void log(String message) {}
}
class Configurable {
void configure(Map<String, dynamic> config) {}
}
class Database {
Future<void> query(String sql) {}
}
// Bad: Monolithic interface
class Everything {
void log(String message) {}
void configure(Map<String, dynamic> config) {}
Future<void> query(String sql) {}
void render() {}
void validate() {}
// Too many responsibilities
}
Common Mistakes
Missing @override Annotations
Wrong:
class Dog implements Animal {
void speak() {
print('Woof!');
}
// Missing @override
}
Correct:
class Dog implements Animal {
@override
void speak() {
print('Woof!');
}
}
Incomplete Implementation
Wrong:
class User implements JsonSerializable, Validatable {
String name;
User(this.name);
@override
Map<String, dynamic> toJson() {
return {'name': name};
}
// Missing isValid() from Validatable
}
Correct:
class User implements JsonSerializable, Validatable {
String name;
User(this.name);
@override
Map<String, dynamic> toJson() {
return {'name': name};
}
@override
bool isValid() {
return name.isNotEmpty;
}
}
Summary
Interfaces provide contracts that enable polymorphism and loose coupling. In Dart, every class implicitly defines an interface, allowing you to implement multiple interfaces and create flexible, maintainable code.
Next Steps
Now that you understand interfaces, continue to:
Did You Know?
- Every class in Dart is also an interface
- A class can implement multiple interfaces
- Interfaces can have default implementations
@overrideis recommended for clarity- Interface methods must be implemented
- Abstract classes can implement interfaces
- Dart doesn't have a separate
interfacekeyword