Skip to content

Interface Classes

Understand how to use interface classes to define strict contracts in Dart.


What is it?

Interface classes are a feature introduced in Dart 3 that enforce strict interface contracts. A class marked with the interface modifier can be implemented (not extended), ensuring that the contract is followed without inheritance.


Why does it exist?

Interface classes exist to:

  • Define pure contracts without implementation
  • Enforce interface implementation
  • Prevent accidental extension
  • Enable better abstraction
  • Support multiple implementations
  • Maintain clean separation

Basic Interface Classes

Creating an Interface

// Interface class - can only be implemented, not extended
interface class Logger {
  // No implementation needed
  void log(String message);
  void error(String message);
  void warning(String message);
  void info(String message);

  // Can have static methods
  static Logger getConsole() => ConsoleLogger();
  static Logger getFile() => FileLogger();
}

// Correct: Implementing interface
class ConsoleLogger implements Logger {
  @override
  void log(String message) {
    print('LOG: $message');
  }

  @override
  void error(String message) {
    print('ERROR: $message');
  }

  @override
  void warning(String message) {
    print('WARNING: $message');
  }

  @override
  void info(String message) {
    print('INFO: $message');
  }
}

// Another implementation
class FileLogger implements Logger {
  @override
  void log(String message) {
    print('Writing to file: $message');
  }

  @override
  void error(String message) {
    print('Writing error to file: $message');
  }

  @override
  void warning(String message) {
    print('Writing warning to file: $message');
  }

  @override
  void info(String message) {
    print('Writing info to file: $message');
  }
}

// Usage
void processLogger(Logger logger) {
  logger.info('Application started');
  logger.log('Processing data');
  logger.warning('Low memory');
  logger.error('Something went wrong');
}

var consoleLogger = Logger.getConsole();
var fileLogger = Logger.getFile();

processLogger(consoleLogger);
processLogger(fileLogger);

Interface with Properties

interface class Repository {
  // Properties
  String get name;
  bool get isConnected;

  // Methods
  Future<void> connect();
  Future<void> disconnect();
  Future<List<Map<String, dynamic>>> findAll();
  Future<Map<String, dynamic>?> findById(String id);
  Future<void> save(Map<String, dynamic> data);
  Future<void> delete(String id);
}

// Implementation
class MySQLRepository implements Repository {
  final String _name = 'MySQL';
  bool _isConnected = false;

  @override
  String get name => _name;

  @override
  bool get isConnected => _isConnected;

  @override
  Future<void> connect() async {
    print('Connecting to MySQL...');
    _isConnected = true;
  }

  @override
  Future<void> disconnect() async {
    print('Disconnecting from MySQL...');
    _isConnected = false;
  }

  @override
  Future<List<Map<String, dynamic>>> findAll() async {
    print('Finding all from MySQL');
    return [];
  }

  @override
  Future<Map<String, dynamic>?> findById(String id) async {
    print('Finding by id $id from MySQL');
    return {'id': id, 'data': 'sample'};
  }

  @override
  Future<void> save(Map<String, dynamic> data) async {
    print('Saving to MySQL: $data');
  }

  @override
  Future<void> delete(String id) async {
    print('Deleting from MySQL: $id');
  }
}

// Another implementation
class InMemoryRepository implements Repository {
  final Map<String, Map<String, dynamic>> _data = {};
  bool _isConnected = true;

  @override
  String get name => 'InMemory';

  @override
  bool get isConnected => _isConnected;

  @override
  Future<void> connect() async {
    print('Connecting to InMemory...');
    _isConnected = true;
  }

  @override
  Future<void> disconnect() async {
    print('Disconnecting from InMemory...');
    _isConnected = false;
  }

  @override
  Future<List<Map<String, dynamic>>> findAll() async {
    print('Finding all from InMemory');
    return _data.values.toList();
  }

  @override
  Future<Map<String, dynamic>?> findById(String id) async {
    print('Finding by id $id from InMemory');
    return _data[id];
  }

  @override
  Future<void> save(Map<String, dynamic> data) async {
    print('Saving to InMemory: $data');
    _data[data['id']] = data;
  }

  @override
  Future<void> delete(String id) async {
    print('Deleting from InMemory: $id');
    _data.remove(id);
  }
}

// Usage
void processRepository(Repository repo) async {
  print('Repository: ${repo.name}');
  print('Connected: ${repo.isConnected}');

  await repo.connect();
  await repo.save({'id': '1', 'name': 'Alice'});
  var items = await repo.findAll();
  print('Items: $items');
  await repo.disconnect();
}

Interface Classes with Default Implementation

Providing Defaults

interface class Validator {
  // Interface with default implementation
  bool isValid(String value) {
    return value.isNotEmpty;
  }

  // Other validation methods
  bool isValidEmail(String email) {
    return email.contains('@') && email.contains('.');
  }

  bool isValidPhone(String phone) {
    return RegExp(r'^\d{10}$').hasMatch(phone);
  }

  bool isValidPassword(String password) {
    return password.length >= 8;
  }
}

// Implementation using defaults
class UserValidator implements Validator {
  @override
  bool isValid(String value) {
    // Override default behavior
    return value.isNotEmpty && value.length >= 3;
  }

  // Can use default implementations for other methods
  // Or override them
  @override
  bool isValidPassword(String password) {
    // Custom password validation
    return password.length >= 10 && 
           RegExp(r'[A-Z]').hasMatch(password) &&
           RegExp(r'\d').hasMatch(password);
  }
}

// Usage
var validator = UserValidator();
print(validator.isValid('Al')); // false (too short)
print(validator.isValid('Alice')); // true
print(validator.isValidEmail('test@example.com')); // true (from interface)
print(validator.isValidPhone('1234567890')); // true (from interface)
print(validator.isValidPassword('Password123')); // true (custom)

Interface Classes vs Abstract Classes

Comparison

// Interface (pure contract)
interface class Drawable {
  void draw();
  void erase();
  void resize(double factor);

  // Can have default implementation
  String get description => 'Drawable object';
}

// Abstract class (with implementation)
abstract class Shape {
  // Abstract method
  double get area;

  // Concrete method
  void printArea() {
    print('Area: $area');
  }

  // Abstract method
  void draw();
}

// Implementing interface and extending abstract class
class Circle implements Drawable, Shape {
  double radius;

  Circle(this.radius);

  // From Drawable
  @override
  void draw() {
    print('Drawing circle');
  }

  @override
  void erase() {
    print('Erasing circle');
  }

  @override
  void resize(double factor) {
    radius *= factor;
    print('Resized to $radius');
  }

  // From Shape
  @override
  double get area => math.pi * radius * radius;

  @override
  String get description => 'Circle with radius $radius';
}

// Usage
var circle = Circle(5);
circle.draw(); // Drawing circle
circle.printArea(); // Area: 78.53981633974483
circle.resize(2); // Resized to 10.0
circle.erase(); // Erasing circle

Best Practices

Use for API Contracts

// Good: Interface for API contract
interface class PaymentGateway {
  Future<bool> processPayment(double amount, String currency);
  Future<bool> refund(String transactionId);
  String get gatewayName;
  bool get isAvailable;
}

class StripeGateway implements PaymentGateway {
  @override
  String get gatewayName => 'Stripe';

  @override
  bool get isAvailable => true;

  @override
  Future<bool> processPayment(double amount, String currency) async {
    print('Processing Stripe payment: $amount $currency');
    return true;
  }

  @override
  Future<bool> refund(String transactionId) async {
    print('Refunding Stripe transaction: $transactionId');
    return true;
  }
}

class PayPalGateway implements PaymentGateway {
  @override
  String get gatewayName => 'PayPal';

  @override
  bool get isAvailable => true;

  @override
  Future<bool> processPayment(double amount, String currency) async {
    print('Processing PayPal payment: $amount $currency');
    return true;
  }

  @override
  Future<bool> refund(String transactionId) async {
    print('Refunding PayPal transaction: $transactionId');
    return true;
  }
}

Keep Interfaces Focused

// Good: Focused interfaces
interface class Logger {
  void log(String message);
  void error(String message);
  void warning(String message);
}

interface class Metrics {
  void track(String metric, double value);
  void increment(String counter);
}

// Bad: Monolithic interface
interface class Everything {
  void log(String message);
  void track(String metric, double value);
  void save(String data);
  void validate(String input);
  // Too many responsibilities
}

Common Mistakes

Extending Interface Class

Wrong:

interface class Logger {
  void log(String message);
}

class CustomLogger extends Logger {
  // Error: Cannot extend interface class
}

Correct:

interface class Logger {
  void log(String message);
}

class CustomLogger implements Logger {
  @override
  void log(String message) {
    // Implementation
  }
}

Missing Implementation

Wrong:

interface class Validator {
  bool isValid(String value);
}

class UserValidator implements Validator {
  // Missing isValid implementation
}
// Error: Missing implementation

Correct:

interface class Validator {
  bool isValid(String value);
}

class UserValidator implements Validator {
  @override
  bool isValid(String value) {
    return value.isNotEmpty;
  }
}

Summary

Interface classes provide pure contracts that enforce implementation without inheritance. They're ideal for defining APIs, creating service contracts, and enabling polymorphism through implementation.


Next Steps

Now that you understand interface classes, continue to:


Did You Know?

  • Interface classes were introduced in Dart 3
  • They can only be implemented, not extended
  • Interface classes can have default implementations
  • They support multiple interface implementation
  • Interface classes are pure contracts
  • They're useful for dependency injection
  • Interface classes enable better testing through mocks