Skip to content

Dart Interfaces — Implicit Interfaces and Abstract Contracts

DodaTech Updated 2026-06-28 8 min read

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 implements keyword to fulfill interfaces
  • Abstract classes as explicit interface definitions
  • Implementing multiple interfaces
  • Interface segregation and single-responsibility design
  • Comparing extends, implements, and with

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

  1. Using implements when extends is appropriate: If you want to reuse implementation, use extends. Use implements only when you want to fulfill a contract from scratch.

  2. Forgetting to implement all interface members: The compiler enforces that every method and getter declared in the interface must have a corresponding @override in the implementing class.

  3. 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.

  4. 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.

  5. 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

  1. How does Dart's implicit interface system differ from explicit interfaces in Java?
  2. What is the difference between extends and implements?
  3. How does the interface segregation principle improve code maintainability?
  4. Why should high-level modules depend on abstractions rather than concrete implementations?
  5. Challenge: Design a NotificationService interface with send and getStatus methods. Implement EmailNotificationService, SMSNotificationService, and PushNotificationService. Then build a NotificationManager that accepts any NotificationService and sends bulk notifications.

Mini Project

Build a data access layer using interfaces:

  • Define Repository<T> interface with getAll, getById, create, update, delete
  • Implement InMemoryRepository<T> that stores data in a Map
  • Implement UserRepository that extends Repository<User>
  • Implement ProductRepository similarly
  • Write a service class that depends on Repository<T> and works with any type
  • Write unit tests with mock repositories

FAQ

Does Dart have an `interface` keyword?

No. Every class doubles as an interface. If you want a pure interface, define an abstract class with only abstract methods.

Can I implement a class from another library?

Yes. Dart's implicit interfaces work across library boundaries. Any public method of any class can be implemented by any other class in any file.

What happens if I implement a class with private members?

Private members are library-scoped. You cannot implement a private member from another library because you cannot see it. Within the same library, private members are part of the interface.

Can I implement multiple classes that have the same method name?

Yes, but the implementing method must satisfy all interfaces simultaneously. If two interfaces declare void save() with the same signature, one implementation suffices.

Should I always use `implements` instead of `extends`?

No. Use extends when there is an is-a relationship and you want to reuse implementation. Use implements when you only need to satisfy a contract.

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