Ankit Ranjan
Back to Deep Dives

Class Modifiers in Dart — abstract, base, final, interface, sealed, and mixin

Dart 3 introduced powerful class modifiers that control how your classes can be extended, implemented, and instantiated. Learn when to use each one.

May 27, 2026 9 topics 7 quiz questions
Share:
1

Why class modifiers exist

Before Dart 3, every class was wide open. Anyone could extend it, implement it, or instantiate it. That sounds flexible, but it creates problems.

Consider a library author who writes a DatabaseConnection class. Internally, this class relies on certain methods being called in a specific order. If someone extends it and overrides those methods incorrectly, the whole thing breaks. The library author has no way to prevent this.

// Your library
class DatabaseConnection {
  void connect() { /* sets up state */ }
  void query(String sql) { /* uses that state */ }
}

// Someone else's code
class MyConnection extends DatabaseConnection {
  @override
  void connect() {
    // Oops, forgot to call super.connect()
    // Now query() will crash
  }
}
Class modifiers give you control. They let you say: "This class is for implementing, not extending." Or: "This class must stay within my library." Or: "This class cannot be subtyped at all."

Think of modifiers as API contracts. They communicate intent and the compiler enforces it.

2

The modifier cheat sheet

Dart 3 has six class modifiers. Each one controls different aspects of what code outside your library can do with your class.

Dart 3 Class Modifiers — What Each One ControlsModifierCan Extend?Can Implement?Can Construct?(no modifier)YesYesYesabstractYesYesNobaseYesNo*YesinterfaceNo*YesYesfinalNo*No*YessealedSame fileSame fileNo* Restrictions apply outside the library where the class is defined

The key insight: these restrictions only apply outside the library where the class is defined. Within your own library, you have full access. The modifiers protect your API from external misuse.

3

abstract — cannot instantiate

The abstract modifier prevents direct instantiation. You cannot call the constructor from outside the class hierarchy.

abstract class Shape {
  double get area;   // No implementation
  void draw();       // Must be overridden
}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);

  @override
  double get area => 3.14159 * radius * radius;

  @override
  void draw() => print('Drawing circle');
}

// var s = Shape();     // Error: abstract class
var c = Circle(5);      // OK
Abstract classes can have both abstract members (no implementation) and concrete members (with implementation). Subclasses must implement all abstract members.

Use abstract when you want to define a template. The class describes what something should do, but subclasses decide how.

4

base — extend only, no implementing

The base modifier allows extending but blocks implementing. This forces subclasses to inherit your implementation, not just your interface.

// In your library
base class Vehicle {
  void startEngine() {
    _checkFuel();      // Private method
    _initSystems();    // Must run in order
    print('Engine started');
  }

  void _checkFuel() { /* ... */ }
  void _initSystems() { /* ... */ }
}

// Outside your library
class Car extends Vehicle {    // OK
  @override
  void startEngine() {
    super.startEngine();       // Gets the full sequence
    print('Car ready');
  }
}

// class FakeVehicle implements Vehicle { }  // Error!
Why block implements? Because implementing creates a completely new class that just happens to have the same method signatures. It doesn't get your actual code. If your class relies on internal state or method call sequences, an implementing class will break those assumptions.

base forces the entire inheritance chain to stay in your libraryyour_library.dartbase class Abase class Bextends Aexternal_package.dartclass Cextends Bclass Dimplements Avar obj = B();Can instantiate and use objectsSubclasses of base must also be base, final, or sealed — the chain is unbroken

Important: subclasses of a base class must themselves be base, final, or sealed. The protection is transitive.

5

interface — implement only, no extending

The interface modifier is the opposite of base. It allows implementing but blocks extending.

// In your library
interface class Comparable {
  int compareTo(Object other) => 0;  // Default impl
}

// Outside your library
class Person implements Comparable {  // OK
  final String name;
  Person(this.name);

  @override
  int compareTo(Object other) {
    if (other is Person) {
      return name.compareTo(other.name);
    }
    return -1;
  }
}

// class Employee extends Comparable { }  // Error!
Use interface when you want to define a contract without forcing users to inherit your implementation. This is useful when:

1. Your class has implementation details that shouldn't be inherited
2. You want to evolve the implementation without breaking subclasses
3. The class is meant purely as a type/contract

interface class vs abstract class — different constraintsinterface class LoggerCan implement (use as contract):class FileLogger implements LoggerCannot extend (outside library):class MyLogger extends LoggerCan construct:Logger()abstract class LoggerCan implement:class FileLogger implements LoggerCan also extend:class MyLogger extends LoggerCannot construct:Logger()interface = contract only | abstract = template for extension

6

final — complete lockdown

The final modifier is the strictest. It blocks both extending and implementing outside the library. The type is frozen.

// In your library
final class ApiResponse {
  final int statusCode;
  final String body;
  final Map<String, String> headers;

  ApiResponse(this.statusCode, this.body, this.headers);

  bool get isSuccess => statusCode >= 200 && statusCode < 300;
}
Outside the library, users can create instances and use them normally. But they cannot extend or implement the class.

final class — complete lockdown outside your libraryfinal class Stringclass MyStringextends StringNo extendingclass FakeStringimplements StringNo implementingvar s = 'hello';String.fromCharCodes([...])Can use normallyThe type is frozen — no subtyping allowed outside the defining library

Use final when:
1. The class represents a complete, self-contained value (like DateTime or Uri)
2. You want total control over all instances of the type
3. Subclassing would break invariants or security guarantees

7

sealed — exhaustive subtypes in one file

The sealed modifier restricts subclasses to the same file. This enables exhaustive pattern matching — the compiler knows all possible subtypes.

// All in one file: result.dart
sealed class Result<T> {}

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

class Failure<T> extends Result<T> {
  final String error;
  Failure(this.error);
}

// Usage: compiler knows the switch is exhaustive
String describe<T>(Result<T> result) {
  return switch (result) {
    Success(:var value) => 'Got: $value',
    Failure(:var error) => 'Failed: $error',
    // No default needed — compiler knows these are all cases
  };
}
Sealed classes are implicitly abstract. You cannot instantiate a sealed class directly — only its subtypes.

Use sealed for algebraic data types: a fixed set of variants that represent all possible states. Network results, UI states, command types, AST nodes.

8

mixin class — dual-use declarations

The mixin class modifier creates something that can be both extended (like a class) and mixed in (like a mixin).

mixin class — can be both extended AND mixed inmixin class Loggingvoid log(String msg) {...}Pattern 1: extendsclass FileLoggerextends LoggingSingle inheritance, gets all membersPattern 2: with (mixin)class Widget extends Basewith LoggingAdd behaviour without using up extendsmixin LoggingOnly mixable, cannot extendmixin class LoggingBoth mixable AND extendable

mixin class Identifiable {
  late final String id = _generateId();
  String _generateId() => DateTime.now().toString();
}

// Use as superclass
class Document extends Identifiable {
  final String content;
  Document(this.content);
}

// Use as mixin
class User extends Person with Identifiable {
  final String name;
  User(this.name);
}
A mixin class has restrictions: it cannot have an extends clause (must extend Object), and it cannot have a on clause. These are the same restrictions that would apply to using a regular class as a mixin.

9

Combining modifiers — valid combinations

Modifiers can be combined, but not all combinations are valid. Here are the useful ones:

// Abstract + base: extend only, no instantiation
abstract base class Widget { }

// Abstract + interface: implement only, no instantiation
abstract interface class Serializable { }

// Abstract + final: can only be used within the library
abstract final class InternalBase { }

// Base + mixin: extend or mix in, no implementing
base mixin class Disposable { }

// Interface + mixin: NOT VALID — conflicting constraints
The logic: abstract removes instantiation. base removes implementing. interface removes extending. final removes both. sealed restricts subtypes to the same file.

When choosing, ask yourself:

Should users extend this? If no, use interface or final.
Should users implement this? If no, use base or final.
Should users instantiate this? If no, use abstract.
Are all subtypes known? If yes, use sealed.

Class Modifiers in Dart

Dart 3 introduced class modifiers that give you fine-grained control over how your classes can be used. This episode covers all six modifiers and when to use each one.

Class Modifiers Quiz

7 questions

Test your understanding of Dart 3 class modifiers

Search

Loading search...