ZetCode

Abstract Classes in Dart

last modified May 25, 2025

This tutorial explores abstract classes in Dart, a key feature for creating class hierarchies and defining interfaces. Abstract classes enable polymorphism and code reuse while enforcing implementation contracts.

Abstract Classes Overview

Abstract classes in Dart serve as incomplete blueprints that cannot be instantiated directly. They define a contract that concrete subclasses must implement. Abstract classes can contain both implemented methods and abstract method declarations.

Unlike regular classes, abstract classes may have unimplemented methods (abstract methods). These methods must be implemented by any concrete (non-abstract) subclass. Abstract classes are declared using the abstract modifier.

Feature Abstract Class Regular Class
Instantiation Cannot be instantiated Can be instantiated
Methods Can have abstract methods All methods implemented
Purpose Define interfaces/behavior Complete implementation
Inheritance Must be extended Can be used directly

Abstract classes are particularly useful when you want to share code among several related classes while enforcing certain methods to be implemented. They sit between interfaces (pure contracts) and concrete classes (full implementations).

Basic Abstract Class

This example demonstrates a simple abstract class with both concrete and abstract methods. We'll create an abstract Shape class that defines common behavior for all shapes.

basic_abstract.dart
abstract class Shape {
  // Abstract method (no implementation)
  double area();
  
  // Concrete method
  void describe() {
    print('This shape has an area of ${area()}');
  }
}

class Circle extends Shape {
  final double radius;
  
  Circle(this.radius);
  
  @override
  double area() => 3.14159 * radius * radius;
}

class Square extends Shape {
  final double side;
  
  Square(this.side);
  
  @override
  double area() => side * side;
}

void main() {
  // var shape = Shape(); // Error: Can't instantiate abstract class
  var circle = Circle(5.0);
  var square = Square(4.0);
  
  circle.describe();
  square.describe();
}

The Shape abstract class declares an abstract area method that subclasses must implement. It also provides a concrete describe method that uses the abstract method. This demonstrates how abstract classes can mix implemented and unimplemented functionality.

The Circle and Square classes extend Shape and provide their specific implementations of area. The example shows that we cannot instantiate Shape directly - we must create instances of the concrete subclasses.

$ dart run basic_abstract.dart
This shape has an area of 78.53975
This shape has an area of 16.0

Abstract Classes as Interfaces

Dart doesn't have a separate interface keyword - abstract classes often serve as interfaces. This example shows how to use abstract classes to define contracts that multiple classes can implement differently.

interface_abstract.dart
abstract class PaymentProcessor {
  void processPayment(double amount);
  void refundPayment(double amount);
}

class CreditCardProcessor implements PaymentProcessor {
  @override
  void processPayment(double amount) {
    print('Processing credit card payment: \$$amount');
  }
  
  @override
  void refundPayment(double amount) {
    print('Refunding credit card payment: \$$amount');
  }
}

class PayPalProcessor implements PaymentProcessor {
  @override
  void processPayment(double amount) {
    print('Processing PayPal payment: \$$amount');
  }
  
  @override
  void refundPayment(double amount) {
    print('Refunding PayPal payment: \$$amount');
  }
}

void processOrder(PaymentProcessor processor, double amount) {
  processor.processPayment(amount);
}

void main() {
  var creditCard = CreditCardProcessor();
  var paypal = PayPalProcessor();
  
  processOrder(creditCard, 100.0);
  processOrder(paypal, 50.0);
}

The PaymentProcessor abstract class defines an interface that payment processors must implement. Unlike with extends, using implements requires the subclass to provide all method implementations, including concrete ones from the abstract class.

The example demonstrates polymorphism - the processOrder function accepts any PaymentProcessor implementation. This pattern is useful for creating pluggable architectures where implementations can be swapped easily.

$ dart run interface_abstract.dart
Processing credit card payment: $100.0
Processing PayPal payment: $50.0

Abstract Classes with Properties

Abstract classes can define abstract properties that subclasses must implement. This example shows an abstract class with both abstract and concrete properties.

properties_abstract.dart
abstract class Vehicle {
  // Abstract property
  String get name;
  
  // Concrete property
  int wheels = 4;
  
  // Abstract method
  void move();
  
  // Concrete method using abstract members
  void describe() {
    print('$name with $wheels wheels is moving');
    move();
  }
}

class Car extends Vehicle {
  @override
  final String name;
  
  Car(this.name);
  
  @override
  void move() {
    print('Vroom vroom!');
  }
}

class Bicycle extends Vehicle {
  @override
  final String name = 'Bicycle';
  
  @override
  int wheels = 2;
  
  @override
  void move() {
    print('Pedaling...');
  }
}

void main() {
  var car = Car('Tesla');
  var bike = Bicycle();
  
  car.describe();
  bike.describe();
}

The Vehicle abstract class declares an abstract name property and move method, while providing a default wheels value and concrete describe method. The Car and Bicycle classes implement these differently.

This demonstrates how abstract classes can define both required and optional aspects of an interface. The wheels property shows how concrete subclasses can override default values from the abstract class.

$ dart run properties_abstract.dart
Tesla with 4 wheels is moving
Vroom vroom!
Bicycle with 2 wheels is moving
Pedaling...

Multiple Inheritance with Mixins

Dart's mixins can be used with abstract classes to achieve multiple inheritance- like behavior. This example combines an abstract class with mixins to create flexible class hierarchies.

mixin_abstract.dart
abstract class Animal {
  String get name;
  void makeSound();
}

mixin Walker {
  void walk() {
    print('Walking...');
  }
}

mixin Swimmer {
  void swim() {
    print('Swimming...');
  }
}

class Dog extends Animal with Walker {
  @override
  final String name;
  
  Dog(this.name);
  
  @override
  void makeSound() => print('Woof!');
}

class Duck extends Animal with Walker, Swimmer {
  @override
  final String name;
  
  Duck(this.name);
  
  @override
  void makeSound() => print('Quack!');
}

void main() {
  var dog = Dog('Buddy');
  var duck = Duck('Donald');
  
  dog.makeSound();
  dog.walk();
  
  duck.makeSound();
  duck.walk();
  duck.swim();
  
  // Can't instantiate abstract class
  // var animal = Animal();
}

The Animal abstract class defines core animal behavior, while the Walker and Swimmer mixins add specific capabilities. The Dog and Duck classes combine these features differently.

This pattern allows for flexible code reuse while maintaining a clear class hierarchy. The abstract class ensures certain contracts are fulfilled, while mixins provide modular behavior that can be mixed in as needed.

$ dart run mixin_abstract.dart
Woof!
Walking...
Quack!
Walking...
Swimming...

Factory Constructors in Abstract Classes

Abstract classes can have factory constructors that return instances of concrete subclasses. This is useful for creating objects without exposing the concrete implementations.

factory_abstract.dart
abstract class Logger {
  void log(String message);
  
  factory Logger(String type) {
    switch (type.toLowerCase()) {
      case 'console':
        return ConsoleLogger();
      case 'file':
        return FileLogger();
      default:
        throw ArgumentError('Unknown logger type: $type');
    }
  }
}

class ConsoleLogger implements Logger {
  @override
  void log(String message) {
    print('CONSOLE: $message');
  }
}

class FileLogger implements Logger {
  @override
  void log(String message) {
    print('FILE: $message (written to log file)');
  }
}

void main() {
  var consoleLogger = Logger('console');
  var fileLogger = Logger('file');
  
  consoleLogger.log('This is a console message');
  fileLogger.log('This should go to a file');
  
  try {
    var unknownLogger = Logger('database');
  } catch (e) {
    print('Error: $e');
  }
}

The Logger abstract class defines a factory constructor that returns different logger implementations based on the input parameter. This hides the concrete classes from consumers, who only interact with the abstract interface.

This pattern is useful for creating objects where the exact implementation needs to be determined at runtime. It provides a clean way to encapsulate object creation logic while maintaining a simple interface for clients.

$ dart run factory_abstract.dart
CONSOLE: This is a console message
FILE: This should go to a file (written to log file)
Error: ArgumentError: Unknown logger type: database

Source

Dart Abstract Classes
Dart Mixins
Dart Factory Constructors

Abstract classes are a powerful tool in Dart for creating flexible, maintainable class hierarchies. They enable polymorphism, code reuse, and clear interface definitions. By combining abstract classes with mixins and factory constructors, you can build sophisticated yet clean object-oriented designs.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.