Dart Interfaces — Implicit Interfaces and Abstract Contracts
In this tutorial, you will learn about Dart Interfaces. We cover key concepts, practical examples, and best practices to help you master this topic.
Dart interfaces are defined implicitly by classes, where every class automatically defines an interface containing all its public method signatures that other classes can implement.
What You Will Learn
- How Dart's implicit interface system works
- Using the
implementskeyword to fulfill interfaces - Abstract classes as explicit interface definitions
- Implementing multiple interfaces
- Interface segregation and single-responsibility design
- Comparing
extends,implements, andwith
Why It Matters
Unlike Java and C# which have a separate interface keyword, Dart uses classes as interfaces. This simplifies the language while retaining full interface capability. When you define a class, you automatically create an interface that other classes can implement. This design encourages programming to interfaces rather than implementations, which is a cornerstone of testable, maintainable code. In Flutter development, every widget implements the Widget interface through inheritance or direct implementation.
Real-World Use
The DodaTech app defines a StorageService abstract class that specifies save, load, and delete methods. Platform-specific implementations (LocalStorageService, CloudStorageService, TestStorageService) all implement this interface. The rest of the app depends on StorageService (the interface), not on any specific implementation.
Learning Path
flowchart LR A[Dart Inheritance] --> B[Dart Interfaces\nYou are here] B --> C[Dart Mixins] style B fill:#f90,color:#fff
Implicit Interfaces
Every Dart class defines an implicit interface. Any class can implement that interface by providing bodies for all its methods:
class Printer {
void print(String message) {
print('Printing: $message');
}
void scan(String document) {
print('Scanning: $document');
}
}
// This class implements the Printer interface
class MockPrinter implements Printer {
@override
void print(String message) {
// Mock implementation, no actual printing
print('Mock print: $message');
}
@override
void scan(String document) {
print('Mock scan: $document');
}
}
void main() {
Printer realPrinter = Printer();
Printer mockPrinter = MockPrinter();
realPrinter.print('Annual Report');
mockPrinter.print('Annual Report');
}
Output:
Printing: Annual Report
Mock print: Annual Report
The MockPrinter class does not extend Printer. It implements the Printer interface by providing bodies for all of Printer's public methods. The compiler checks that MockPrinter satisfies the interface.
Implementing Multiple Interfaces
A class can implement multiple interfaces, combining contracts from different sources:
abstract class Readable {
String read(String key);
}
abstract class Writable {
void write(String key, String value);
}
abstract class Deletable {
void delete(String key);
}
// Implements all three interfaces
class FileStorage implements Readable, Writable, Deletable {
@override
String read(String key) {
print('Reading $key from file');
return 'value';
}
@override
void write(String key, String value) {
print('Writing $value to $key in file');
}
@override
void delete(String key) {
print('Deleting $key from file');
}
}
void main() {
var storage = FileStorage();
storage.write('theme', 'dark');
print(storage.read('theme'));
storage.delete('theme');
}
Output:
Writing dark to theme in file
Reading theme from file
value
Deleting theme from file
Implementing multiple interfaces enables the Interface Segregation Principle: keep interfaces small and focused, then combine them as needed.
Abstract Classes as Explicit Interfaces
Abstract classes with only abstract methods serve as pure interface definitions:
// Pure interface using abstract class
abstract class AuthenticationService {
bool login(String username, String password);
void logout();
bool get isLoggedIn;
String? get currentUser;
}
class FirebaseAuthService implements AuthenticationService {
@override
bool login(String username, String password) {
print('Logging into Firebase as $username');
return true;
}
@override
void logout() {
print('Logging out of Firebase');
}
@override
bool get isLoggedIn => true;
@override
String? get currentUser => 'alice@example.com';
}
class MockAuthService implements AuthenticationService {
@override
bool login(String username, String password) => true;
@override
void logout() {}
@override
bool get isLoggedIn => false;
@override
String? get currentUser => null;
}
AuthService getAuthService() {
// Return different implementations based on environment
return MockAuthService();
}
void main() {
var auth = getAuthService();
print('Logged in: ${auth.login('alice', 'pass123')}');
print('Current user: ${auth.currentUser}');
}
Output:
Logged in: true
Current user: null
Abstract classes with only abstract methods are the closest thing to Java-style interfaces in Dart. They define a contract without any implementation.
Extends vs Implements vs With
Understanding the differences between these three keywords is critical:
class Animal {
void eat() => print('Eating');
void breathe() => print('Breathing');
}
mixin Swimmer {
void swim() => print('Swimming');
}
class Dog extends Animal {
// Inherits eat() and breathe() implementations
}
class Fish implements Animal {
// Must implement both eat() and breathe()
@override
void eat() => print('Fish eating');
@override
void breathe() => print('Fish breathing');
}
class Duck extends Animal with Swimmer {
// Inherits eat(), breathe() from Animal
// Gets swim() from Swimmer mixin
}
void main() {
print('Dog:');
var dog = Dog();
dog.eat();
dog.breathe();
print('\nFish:');
var fish = Fish();
fish.eat();
fish.breathe();
print('\nDuck:');
var duck = Duck();
duck.eat();
duck.swim();
}
Output:
Dog:
Eating
Breathing
Fish:
Fish eating
Fish breathing
Duck:
Eating
Swimming
extends inherits both interface and implementation. implements only requires the interface. with mixes in implementation without inheritance.
Interface Segregation Principle
Keep interfaces focused on specific capabilities:
// Bad: Fat interface
abstract class AllInOneMachine {
void print(String doc);
void scan(String doc);
void fax(String doc);
void staple(String doc);
void shred(String doc);
}
// Good: Segregated interfaces
abstract class Printer2 {
void print(String doc);
}
abstract class Scanner {
String scan(String doc);
}
abstract class Fax {
void send(String doc, String number);
}
class SimplePrinter implements Printer2 {
@override
void print(String doc) => print('Printing: $doc');
}
class MultiFunctionMachine implements Printer2, Scanner, Fax {
@override
void print(String doc) => print('Printing: $doc');
@override
String scan(String doc) {
print('Scanning: $doc');
return 'scanned_$doc';
}
@override
void send(String doc, String number) {
print('Faxing $doc to $number');
}
}
The interface segregation principle states that no client should be forced to depend on methods it does not use. Small, focused interfaces are preferred over large, monolithic ones.
Dependency Inversion with Interfaces
High-level modules should depend on abstractions, not concrete implementations:
abstract class Logger {
void log(String message);
void error(String message);
}
class ConsoleLogger implements Logger {
@override
void log(String message) => print('[LOG]: $message');
@override
void error(String message) => print('[ERROR]: $message');
}
class FileLogger implements Logger {
@override
void log(String message) {
// Write to file
print('Writing to log file: $message');
}
@override
void error(String message) {
// Write to error file
print('Writing to error file: $message');
}
}
class UserService {
final Logger logger;
UserService(this.logger);
void createUser(String name) {
logger.log('Creating user: $name');
// Business logic...
logger.log('User created: $name');
}
}
void main() {
var service = UserService(ConsoleLogger());
service.createUser('Alice');
}
Output:
[LOG]: Creating user: Alice
[LOG]: User created: Alice
UserService depends on the Logger interface, not on ConsoleLogger or FileLogger. You can swap implementations without changing UserService.
Default Implementations in Interfaces
Abstract classes can provide default method implementations that implementing classes can override or inherit:
abstract class Cache {
String get(String key);
void set(String key, String value);
// Default implementation
bool has(String key) {
var value = get(key);
return value != null && value.isNotEmpty;
}
}
class MemoryCache implements Cache {
final _store = <String, String>{};
@override
String get(String key) => _store[key] ?? '';
@override
void set(String key, String value) {
_store[key] = value;
}
// has() is inherited from the interface's default
}
void main() {
var cache = MemoryCache();
cache.set('theme', 'dark');
print('Has theme: ${cache.has('theme')}');
print('Has user: ${cache.has('user')}');
}
Output:
Has theme: true
Has user: false
The implementing class gets the has method automatically. It can also override it if a different implementation is needed.
Common Mistakes
Using
implementswhenextendsis appropriate: If you want to reuse implementation, useextends. Useimplementsonly when you want to fulfill a contract from scratch.Forgetting to implement all interface members: The compiler enforces that every method and getter declared in the interface must have a corresponding
@overridein the implementing class.Making fat interfaces that violate ISP: A single interface with dozens of methods forces implementers to provide empty or throwing bodies for methods they do not need.
Returning concrete types from Factory functions: Functions that return instances should return the interface type, not the concrete type. This allows swapping implementations without changing callers.
Not using abstract classes for shared state: Use abstract classes (not interfaces) when the contract includes shared state or constructor logic that concrete classes should inherit.
Practice Questions
- How does Dart's implicit interface system differ from explicit interfaces in Java?
- What is the difference between
extendsandimplements? - How does the interface segregation principle improve code maintainability?
- Why should high-level modules depend on abstractions rather than concrete implementations?
- Challenge: Design a
NotificationServiceinterface withsendandgetStatusmethods. ImplementEmailNotificationService,SMSNotificationService, andPushNotificationService. Then build aNotificationManagerthat accepts anyNotificationServiceand sends bulk notifications.
Mini Project
Build a data access layer using interfaces:
- Define
Repository<T>interface withgetAll,getById,create,update,delete - Implement
InMemoryRepository<T>that stores data in a Map - Implement
UserRepositorythat extendsRepository<User> - Implement
ProductRepositorysimilarly - Write a service class that depends on
Repository<T>and works with any type - Write unit tests with mock repositories
FAQ
What is Next
Now that you understand interfaces, learn about mixins for reusable behavior. Proceed to Dart Mixins for composing behavior without inheritance. Then explore Dart Generics for type-safe parameterized classes.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro