What is the difference between Abstract Class vs Interface Class vs Final Class vs Sealed Class?

#dart#oop#abstract-class#interface#final-class#sealed-class#dart-3.0#class-modifiers#inheritance#pattern-matching

Answer

Quick Answer

Dart provides four class modifiers to control inheritance and implementation:

TypeCan Extend?Can Implement?Can Instantiate?Dart Version
Abstract✅ Yes✅ Yes❌ NoAll versions
Interface❌ No✅ Yes❌ NoDart 3.0+
Final❌ No❌ No✅ YesDart 3.0+
Sealed✅ Yes (same library)❌ No❌ NoDart 3.0+

1. Abstract Class

Overview

Abstract classes cannot be instantiated but can be extended or implemented. They can contain both abstract methods (no implementation) and concrete methods (with implementation).

Syntax

dart
abstract class Animal {
  // Abstract method - no implementation
  void makeSound();
  
  // Concrete method - has implementation
  void sleep() {
    print('Sleeping...');
  }
  
  // Fields allowed
  String name = 'Unknown';
}

// Extend - inherit implementation
class Dog extends Animal {
  
  void makeSound() {
    print('Woof!');
  }
  // sleep() is inherited automatically
}

// Implement - must reimplement everything
class Cat implements Animal {
  
  String name = 'Cat';
  
  
  void makeSound() {
    print('Meow!');
  }
  
  
  void sleep() {
    print('Cat is sleeping');
  }
}

void main() {
  // var animal = Animal(); // ❌ Error: Cannot instantiate abstract class
  
  var dog = Dog();
  dog.makeSound(); // Woof!
  dog.sleep();     // Sleeping... (inherited)
  
  var cat = Cat();
  cat.makeSound(); // Meow!
  cat.sleep();     // Cat is sleeping (reimplemented)
}

When to Use

Use abstract class when:

  • You want to provide default implementations for some methods
  • You want to define a common base for a family of classes
  • You need to share fields across subclasses
  • You want both inheritance (extends) and interface (implements) options

2. Interface Class (Dart 3.0+)

Overview

Interface classes can only be implemented, not extended. They enforce a pure interface contract without allowing code reuse through inheritance.

Syntax

dart
interface class Vehicle {
  void start() {
    print('Starting vehicle');
  }
  
  void stop() {
    print('Stopping vehicle');
  }
}

// ✅ Can implement
class Car implements Vehicle {
  
  void start() {
    print('Car starting');
  }
  
  
  void stop() {
    print('Car stopping');
  }
}

// ❌ Cannot extend
// class Bike extends Vehicle { } // Error in Dart 3.0+

void main() {
  var car = Car();
  car.start(); // Car starting
}

Key Points

  • Available since: Dart 3.0
  • Purpose: Force implementation-only contracts
  • Use case: Library design where you don't want users inheriting your implementation

When to Use

Use interface class when:

  • You're designing a library API and want to prevent inheritance
  • You want to ensure users don't rely on implementation details
  • You want to force re-implementation of all methods
  • You're defining a pure contract without shared behavior

3. Final Class (Dart 3.0+)

Overview

Final classes cannot be extended or implemented. They're completely sealed from inheritance. Users can only instantiate and use them directly.

Syntax

dart
final class Database {
  final String connectionString;
  
  Database(this.connectionString);
  
  void connect() {
    print('Connecting to: $connectionString');
  }
  
  void query(String sql) {
    print('Executing: $sql');
  }
}

// ❌ Cannot extend
// class CustomDatabase extends Database { } // Error

// ❌ Cannot implement  
// class MockDatabase implements Database { } // Error

// ✅ Can instantiate and use
void main() {
  var db = Database('localhost:5432');
  db.connect(); // Connecting to: localhost:5432
  db.query('SELECT * FROM users'); // Executing: SELECT * FROM users
}

Real-World Example: Immutable Value Object

dart
final class Money {
  final double amount;
  final String currency;
  
  const Money(this.amount, this.currency);
  
  Money add(Money other) {
    if (currency != other.currency) {
      throw ArgumentError('Currency mismatch');
    }
    return Money(amount + other.amount, currency);
  }
  
  
  String toString() => '$amount $currency';
}

void main() {
  var price = Money(99.99, 'USD');
  var tax = Money(7.99, 'USD');
  var total = price.add(tax);
  print(total); // 107.98 USD
  
  // Cannot extend or implement to bypass validation logic
}

When to Use

Use final class when:

  • You want to prevent all inheritance
  • You're implementing security-sensitive logic that shouldn't be overridden
  • You're creating value objects or data classes
  • You want to ensure implementation integrity (no subclass can break your invariants)
  • You're designing library APIs where inheritance would break encapsulation

4. Sealed Class (Dart 3.0+)

Overview

Sealed classes can only be extended or implemented within the same library. They enable exhaustive pattern matching and are perfect for representing a fixed set of subtypes.

Syntax

dart
// result.dart (same library)
sealed class Result<T> {}

class Success<T> extends Result<T> {
  final T data;
  Success(this.data);
}

class Error<T> extends Result<T> {
  final String message;
  Error(this.message);
}

class Loading<T> extends Result<T> {}

// Outside this library:
// class Pending extends Result { } // ❌ Error: Cannot extend sealed class

Exhaustive Pattern Matching

dart
String handleResult(Result<String> result) {
  // Compiler ENFORCES exhaustive matching
  return switch (result) {
    Success(data: var d) => 'Success: $d',
    Error(message: var m) => 'Error: $m',
    Loading() => 'Loading...',
    // If you forget a case, compile error!
  };
}

void main() {
  var result1 = Success('User data loaded');
  print(handleResult(result1)); // Success: User data loaded
  
  var result2 = Error('Network timeout');
  print(handleResult(result2)); // Error: Network timeout
  
  var result3 = Loading<String>();
  print(handleResult(result3)); // Loading...
}

Real-World Example: Navigation State

dart
sealed class NavigationState {}

class HomeScreen extends NavigationState {}

class ProfileScreen extends NavigationState {
  final String userId;
  ProfileScreen(this.userId);
}

class SettingsScreen extends NavigationState {
  final bool darkMode;
  SettingsScreen({this.darkMode = false});
}

class DetailScreen extends NavigationState {
  final int itemId;
  DetailScreen(this.itemId);
}

// Router can exhaustively handle all cases
Widget buildScreen(NavigationState state) {
  return switch (state) {
    HomeScreen() => HomeWidget(),
    ProfileScreen(:var userId) => ProfileWidget(userId),
    SettingsScreen(:var darkMode) => SettingsWidget(darkMode),
    DetailScreen(:var itemId) => DetailWidget(itemId),
  };
}

When to Use

Use sealed class when:

  • You have a fixed set of subtypes (like a discriminated union)
  • You want exhaustive pattern matching with compile-time safety
  • You're modeling state machines or algebraic data types
  • You're implementing the Result/Either pattern
  • You want to prevent external subclasses while allowing internal ones

Comparison Table

Feature Comparison

FeatureAbstractInterfaceFinalSealed
Can instantiate❌ No❌ No✅ Yes❌ No
Can extend✅ Yes❌ No❌ No✅ Same library
Can implement✅ Yes✅ Yes❌ No✅ Same library
Abstract methods✅ Yes❌ No (use
text
abstract interface
)
❌ No (use
text
abstract final
)
✅ Yes (implicitly abstract)
Concrete methods✅ Yes✅ Yes✅ Yes✅ Yes
Inheritance allowed✅ Yes❌ No❌ No⚠️ Same lib
Exhaustive matching❌ No❌ No❌ No✅ Yes
Dart versionAll3.0+3.0+3.0+

Usage Decision Tree

dart
// Do you want to prevent all inheritance?
//   ├─ Yes → final class
//   └─ No ↓

// Do you have a fixed set of known subtypes?
//   ├─ Yes → sealed class
//   └─ No ↓

// Do you want to provide default implementations?
//   ├─ Yes → abstract class
//   └─ No → interface class

Abstract Method vs Concrete Method Support

What Are They?

  • Abstract method: Has no body — subclasses must override and implement it
  • Concrete method: Has a body — provides default implementation that subclasses can inherit or override
dart
abstract class Example {
  void abstractMethod();           // Abstract — no body
  void concreteMethod() {          // Concrete — has body
    print('Default implementation');
  }
}

Method Rules Per Class Modifier

Class ModifierAbstract MethodsConcrete MethodsWhy?
text
abstract class
✅ Yes✅ YesCannot be instantiated — subclasses provide implementations
text
interface class
❌ No✅ YesMust be instantiable — all methods need bodies
text
final class
❌ No✅ YesMust be instantiable — no subclasses to implement abstract methods
text
sealed class
✅ Yes✅ YesImplicitly abstract — subclasses (same library) implement them

Key rule: Only classes that are abstract (explicitly or implicitly) can have abstract methods. Concrete classes (

text
interface class
,
text
final class
) must provide implementations for all methods because they can be instantiated.

interface class — Concrete Methods Only

text
interface class
cannot have abstract methods. All methods must have a body because the class is instantiable.

dart
// ❌ ERROR: interface class cannot have abstract methods
interface class Printable {
  void printInfo();  // ❌ Compile error — no body!
}

// ✅ CORRECT: All methods must have implementations
interface class Printable {
  void printInfo() {
    print('Printable object');
  }
}

// When implemented — all methods must be overridden
class User implements Printable {
  
  void printInfo() {
    print('User: Alice');
  }
}

Want abstract methods in an interface? Use

text
abstract interface class
instead.

final class — Concrete Methods Only

text
final class
cannot have abstract methods. Since no class can extend or implement it, there would be no subclass to provide the implementation.

dart
// ❌ ERROR: final class cannot have abstract methods
final class Logger {
  void log(String msg);  // ❌ Compile error — no body!
}

// ✅ CORRECT: All methods must have implementations
final class Logger {
  void log(String msg) {
    print('[LOG] $msg');
  }
  
  void error(String msg) {
    print('[ERROR] $msg');
  }
}

// ❌ Cannot extend or implement — no subclassing possible
// class CustomLogger extends Logger { }  // Error
// class MockLogger implements Logger { }  // Error

Want abstract methods with restricted inheritance? Use

text
abstract final class
(same-library only) instead.

sealed class — Supports Both Abstract AND Concrete Methods

Unlike

text
interface
and
text
final
,
text
sealed class
does support abstract methods because it is implicitly abstract — it cannot be instantiated directly.

dart
// ✅ sealed supports BOTH abstract and concrete methods
sealed class Result {
  String describe();              // ✅ Abstract method — no body

  void log() {                    // ✅ Concrete method — has body
    print('Result: ${describe()}');
  }
}

class Success extends Result {
  final String data;
  Success(this.data);

  
  String describe() => 'Success: $data';  // Must implement abstract method
  // log() is inherited automatically
}

class Failure extends Result {
  final String error;
  Failure(this.error);

  
  String describe() => 'Error: $error';   // Must implement abstract method
  // log() is inherited automatically
}

void main() {
  var result = Success('Data loaded');
  result.log();  // Result: Success: Data loaded
}

Combined Modifiers (Dart 3.0+)

Dart allows combining

text
abstract
with other modifiers to unlock abstract methods:

Combined ModifierAbstract MethodsConcrete MethodsInstantiableExtendImplement
text
abstract interface class
✅ Yes✅ Yes❌ No❌ No✅ Yes
text
abstract final class
✅ Yes✅ Yes❌ No❌ No (outside lib)❌ No (outside lib)
text
abstract class
✅ Yes✅ Yes❌ No✅ Yes✅ Yes
dart
// Pure interface — abstract methods only, implement-only contract
abstract interface class Serializable {
  Map<String, dynamic> toJson();    // ✅ Abstract method allowed
  void fromJson(Map<String, dynamic> json);
}

// Cannot extend — must implement all methods
class User implements Serializable {
  
  Map<String, dynamic> toJson() => {'name': name};
  
  
  void fromJson(Map<String, dynamic> json) {
    name = json['name'];
  }
  
  String name = '';
}
dart
// Abstract final — only same-library subclasses allowed
abstract final class BaseRepository {
  Future<void> connect();          // ✅ Abstract method
  
  void log(String msg) {           // ✅ Concrete method
    print('[Repository] $msg');
  }
}

// Only works within the SAME library file
class UserRepository extends BaseRepository {
  
  Future<void> connect() async {
    log('Connecting to user DB...');
  }
}

// ❌ Outside library — cannot extend or implement

Sealed Class — Implicitly Abstract

text
sealed
is the only modifier that is implicitly abstract — you don't need to write
text
abstract sealed class
:

dart
// sealed is implicitly abstract — abstract methods are allowed
sealed class NetworkState {
  String get message;              // ✅ Abstract getter
  void log();                      // ✅ Abstract method
  
  DateTime get timestamp =>        // ✅ Concrete method
      DateTime.now();
}

class Loading extends NetworkState {
  
  String get message => 'Loading...';
  
  
  void log() => print('State: Loading');
}

class Success extends NetworkState {
  final String data;
  Success(this.data);
  
  
  String get message => 'Success: $data';
  
  
  void log() => print('State: Success');
}

class Error extends NetworkState {
  final String error;
  Error(this.error);
  
  
  String get message => 'Error: $error';
  
  
  void log() => print('State: Error - $error');
}

Quick Decision Guide for Methods

dart
// Need abstract methods + anyone can extend?
//   → abstract class

// Need abstract methods + implement-only contract?
//   → abstract interface class

// Need abstract methods + same-library only?
//   → sealed class (with exhaustive matching)
//   → abstract final class (without exhaustive matching)

// All methods must have implementations?
//   → final class (no inheritance at all)
//   → interface class (implement-only, no extension)

Real-World Examples

Example 1: API Response Handling (Sealed)

dart
sealed class ApiResponse<T> {}

class Success<T> extends ApiResponse<T> {
  final T data;
  final int statusCode;
  Success(this.data, this.statusCode);
}

class Failure<T> extends ApiResponse<T> {
  final String error;
  final int? statusCode;
  Failure(this.error, [this.statusCode]);
}

class NetworkError<T> extends ApiResponse<T> {
  final String message;
  NetworkError(this.message);
}

// Exhaustive handling
Future<void> handleResponse(ApiResponse<User> response) async {
  switch (response) {
    case Success(:var data, :var statusCode):
      print('User loaded: ${data.name} (Status: $statusCode)');
      // Navigate to user screen
      
    case Failure(:var error, :var statusCode):
      print('API Error: $error (${statusCode ?? 'unknown'})');
      // Show error dialog
      
    case NetworkError(:var message):
      print('Network Error: $message');
      // Show retry button
  }
}

Example 2: Database Connection (Final)

dart
// Prevent users from extending to bypass connection pooling
final class DatabaseConnection {
  static final _instance = DatabaseConnection._internal();
  factory DatabaseConnection() => _instance;
  
  DatabaseConnection._internal();
  
  final _connectionPool = <Connection>[];
  
  Future<T> transaction<T>(Future<T> Function(Connection) action) async {
    final conn = _getConnection();
    try {
      return await action(conn);
    } finally {
      _releaseConnection(conn);
    }
  }
  
  Connection _getConnection() { /* ... */ }
  void _releaseConnection(Connection c) { /* ... */ }
}

Example 3: Shape Hierarchy (Abstract)

dart
abstract class Shape {
  // Abstract methods
  double calculateArea();
  double calculatePerimeter();
  
  // Concrete method - shared implementation
  void display() {
    print('${runtimeType}:');
    print('  Area: ${calculateArea()}');
    print('  Perimeter: ${calculatePerimeter()}');
  }
}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);
  
  
  double calculateArea() => 3.14159 * radius * radius;
  
  
  double calculatePerimeter() => 2 * 3.14159 * radius;
}

class Rectangle extends Shape {
  final double width;
  final double height;
  
  Rectangle(this.width, this.height);
  
  
  double calculateArea() => width * height;
  
  
  double calculatePerimeter() => 2 * (width + height);
}

Example 4: Repository Pattern (Interface)

dart
interface class IUserRepository {
  Future<User?> getUserById(String id) async {
    throw UnimplementedError();
  }
  
  Future<List<User>> getAllUsers() async {
    throw UnimplementedError();
  }
  
  Future<void> saveUser(User user) async {
    throw UnimplementedError();
  }
}

// Firebase implementation
class FirebaseUserRepository implements IUserRepository {
  final FirebaseFirestore _firestore;
  
  FirebaseUserRepository(this._firestore);
  
  
  Future<User?> getUserById(String id) async {
    final doc = await _firestore.collection('users').doc(id).get();
    return doc.exists ? User.fromJson(doc.data()!) : null;
  }
  
  
  Future<List<User>> getAllUsers() async {
    final snapshot = await _firestore.collection('users').get();
    return snapshot.docs.map((d) => User.fromJson(d.data())).toList();
  }
  
  
  Future<void> saveUser(User user) async {
    await _firestore.collection('users').doc(user.id).set(user.toJson());
  }
}

// SQLite implementation
class SQLiteUserRepository implements IUserRepository {
  final Database _db;
  
  SQLiteUserRepository(this._db);
  
  
  Future<User?> getUserById(String id) async {
    final result = await _db.query('users', where: 'id = ?', whereArgs: [id]);
    return result.isNotEmpty ? User.fromJson(result.first) : null;
  }
  
  
  Future<List<User>> getAllUsers() async {
    final result = await _db.query('users');
    return result.map((row) => User.fromJson(row)).toList();
  }
  
  
  Future<void> saveUser(User user) async {
    await _db.insert('users', user.toJson());
  }
}

Dart 2.x vs Dart 3.0+

Dart 2.x (Before 3.0)

dart
// Only abstract class available
abstract class Vehicle {
  void start();
}

// No way to prevent extension (only implementation)
class Car extends Vehicle {
  
  void start() => print('Car started');
}

// No sealed classes - no exhaustive matching
// No interface class - can't prevent extension
// No final class - can't prevent all inheritance

Dart 3.0+

dart
// Full modifier support
abstract class Vehicle { }
interface class Drivable { }
final class Car { }
sealed class Result { }

// Exhaustive matching with sealed
sealed class State {}
class Loading extends State {}
class Success extends State {}
class Error extends State {}

String handle(State s) => switch (s) {
  Loading() => 'Loading',
  Success() => 'Success',
  Error() => 'Error',
  // Compiler ensures all cases covered!
};

Best Practices

1. Choose the Right Modifier

dart
// ✅ Good: Use sealed for fixed type hierarchies
sealed class PaymentMethod {}
class CreditCard extends PaymentMethod { }
class PayPal extends PaymentMethod { }
class BankTransfer extends PaymentMethod { }

// ✅ Good: Use final for value objects
final class Email {
  final String value;
  Email(this.value) {
    if (!value.contains('@')) throw ArgumentError('Invalid email');
  }
}

// ✅ Good: Use abstract for shared behavior
abstract class NetworkClient {
  Future<Response> request(String url);
  
  Future<T> get<T>(String url) async {
    final response = await request(url);
    return parse<T>(response);
  }
}

// ✅ Good: Use interface for pure contracts
interface class Serializable {
  Map<String, dynamic> toJson();
  void fromJson(Map<String, dynamic> json);
}

2. Don't Mix Inheritance Styles

dart
// ❌ Bad: Confusing - implementing an abstract class
abstract class Animal {
  void eat() => print('Eating');
}

class Dog implements Animal {
  
  void eat() => print('Dog eating'); // Must reimplement
}

// ✅ Good: Clear intent - extend for code reuse
class Dog extends Animal {
  // eat() inherited automatically
}

3. Use Sealed for State Management

dart
// ✅ Perfect use case for sealed
sealed class AuthState {}
class Authenticated extends AuthState {
  final User user;
  Authenticated(this.user);
}
class Unauthenticated extends AuthState {}
class AuthLoading extends AuthState {}
class AuthError extends AuthState {
  final String message;
  AuthError(this.message);
}

// Widget can exhaustively handle all states
Widget build(BuildContext context, AuthState state) {
  return switch (state) {
    Authenticated(:var user) => HomeScreen(user),
    Unauthenticated() => LoginScreen(),
    AuthLoading() => LoadingScreen(),
    AuthError(:var message) => ErrorScreen(message),
  };
}

4. Document Your Intent

dart
/// Represents a database connection.
/// 
/// This class is [final] to prevent subclassing that could bypass
/// connection pooling and lead to resource leaks.
final class DatabaseConnection { }

/// Base class for all shapes.
/// 
/// This class is [abstract] because shapes cannot exist without
/// specific implementations. Subclasses must implement area and
/// perimeter calculations.
abstract class Shape { }

/// Represents the result of an async operation.
/// 
/// This class is [sealed] to enable exhaustive pattern matching
/// and ensure all result states are handled.
sealed class Result<T> { }

Common Mistakes

Mistake 1: Using Abstract When Sealed Is Better

dart
// ❌ Wrong: Abstract allows external subtypes
abstract class NetworkState {}
class Loading extends NetworkState {}
class Success extends NetworkState {}
class Error extends NetworkState {}
// Problem: Can't use exhaustive matching

// ✅ Correct: Sealed prevents external subtypes
sealed class NetworkState {}
class Loading extends NetworkState {}
class Success extends NetworkState {}
class Error extends NetworkState {}
// Benefit: Compiler enforces exhaustive matching

Mistake 2: Using Final Too Liberally

dart
// ❌ Wrong: Prevents useful extension
final class Button {
  final String label;
  Button(this.label);
}
// Problem: Users can't create CustomButton extending Button

// ✅ Correct: Allow extension for UI components
class Button {
  final String label;
  Button(this.label);
}

class IconButton extends Button {
  final IconData icon;
  IconButton(String label, this.icon) : super(label);
}

Mistake 3: Not Using Interface for APIs

dart
// ❌ Wrong: Users can extend and break your implementation
class ApiClient {
  Future<Response> request(String url) {
    // Complex implementation
  }
}

// ✅ Correct: Force implementation, prevent extension
interface class IApiClient {
  Future<Response> request(String url);
}

class ApiClient implements IApiClient {
  
  Future<Response> request(String url) {
    // Implementation
  }
}

Summary

Use CaseBest Choice
Fixed set of subtypes
text
sealed class
Prevent all inheritance
text
final class
Share implementation
text
abstract class
Pure interface contract
text
interface class
Value objects
text
final class
State machines
text
sealed class
Repository pattern
text
interface class
Base classes with defaults
text
abstract class

Key Takeaways

Abstract: Can extend or implement. Use for shared behavior.

Interface (3.0+): Can only implement. Use for pure contracts.

Final (3.0+): Cannot extend or implement. Use for sealed implementations.

Sealed (3.0+): Can extend/implement in same library. Use for fixed type hierarchies with exhaustive matching.

Dart 3.0+ introduced

text
interface
,
text
final
, and
text
sealed
modifiers for better control over class inheritance and implementation.


Resources