What is the difference between Abstract Class vs Interface Class vs Final Class vs Sealed Class?
Answer
Quick Answer
Dart provides four class modifiers to control inheritance and implementation:
| Type | Can Extend? | Can Implement? | Can Instantiate? | Dart Version |
|---|---|---|---|---|
| Abstract | ✅ Yes | ✅ Yes | ❌ No | All versions |
| Interface | ❌ No | ✅ Yes | ❌ No | Dart 3.0+ |
| Final | ❌ No | ❌ No | ✅ Yes | Dart 3.0+ |
| Sealed | ✅ Yes (same library) | ❌ No | ❌ No | Dart 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
dartabstract 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
dartinterface 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
dartfinal 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
dartfinal 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
dartString 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
dartsealed 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
| Feature | Abstract | Interface | Final | Sealed |
|---|---|---|---|---|
| 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 | ❌ No (use text | ✅ Yes (implicitly abstract) |
| Concrete methods | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| Inheritance allowed | ✅ Yes | ❌ No | ❌ No | ⚠️ Same lib |
| Exhaustive matching | ❌ No | ❌ No | ❌ No | ✅ Yes |
| Dart version | All | 3.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
dartabstract class Example { void abstractMethod(); // Abstract — no body void concreteMethod() { // Concrete — has body print('Default implementation'); } }
Method Rules Per Class Modifier
| Class Modifier | Abstract Methods | Concrete Methods | Why? |
|---|---|---|---|
text | ✅ Yes | ✅ Yes | Cannot be instantiated — subclasses provide implementations |
text | ❌ No | ✅ Yes | Must be instantiable — all methods need bodies |
text | ❌ No | ✅ Yes | Must be instantiable — no subclasses to implement abstract methods |
text | ✅ Yes | ✅ Yes | Implicitly abstract — subclasses (same library) implement them |
Key rule: Only classes that are abstract (explicitly or implicitly) can have abstract methods. Concrete classes (
,textinterface class) must provide implementations for all methods because they can be instantiated.textfinal class
interface class — Concrete Methods Only
interface classdart// ❌ 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
instead.textabstract interface class
final class — Concrete Methods Only
final classdart// ❌ 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
(same-library only) instead.textabstract final class
sealed class — Supports Both Abstract AND Concrete Methods
Unlike
interfacefinalsealed classdart// ✅ 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
abstract| Combined Modifier | Abstract Methods | Concrete Methods | Instantiable | Extend | Implement |
|---|---|---|---|---|---|
text | ✅ Yes | ✅ Yes | ❌ No | ❌ No | ✅ Yes |
text | ✅ Yes | ✅ Yes | ❌ No | ❌ No (outside lib) | ❌ No (outside lib) |
text | ✅ 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
sealedabstract sealed classdart// 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)
dartsealed 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)
dartabstract 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)
dartinterface 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 Case | Best Choice |
|---|---|
| Fixed set of subtypes | text |
| Prevent all inheritance | text |
| Share implementation | text |
| Pure interface contract | text |
| Value objects | text |
| State machines | text |
| Repository pattern | text |
| Base classes with defaults | text |
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
,textinterface, andtextfinalmodifiers for better control over class inheritance and implementation.textsealed