Watch the video (15 min)
On this page24 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. Four Things Code Can Do With a Class
  5. 2. The Rule: Only Other Libraries Are Restricted
  6. 3. abstract (Recap): No Construction
  7. 4. interface class: Implement, Don't Extend
  8. 5. base class: Extend, Don't Implement
  9. 6. The Subtype Rule: base and final Spread
  10. 7. final class: Use Only
  11. 8. sealed and mixin class (Recap)
  12. 9. Combining Modifiers
  13. 10. The Capability Table
  14. 11. Choosing a Modifier as an API Author
  15. 12. A Real-World Example
  16. Code Examples
  17. Common Beginner Mistakes
  18. Best Practices
  19. Mini Challenge
  20. Practice Questions
  21. Practice Answers
  22. Quick Revision
  23. Key Takeaways
  24. What's Next?

Lecture 130 — Class Modifiers

Category22 — Modern Dart Features
Lecture130 of 140
Estimated video length15 min
TargetDart 3.12.2
LevelAdvanced

A plain class can be created, extended and implemented by any file that imports it. Class modifiers let the author decide which of those doors stay open to other libraries.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.


Prerequisites

Students should already understand, from Lectures 059–102:

  • extends, abstract class and implements (Lectures 059, 062–063)
  • mixin, with and mixin class (Lecture 064)
  • sealed class and its same-file rule (Lecture 082)
  • custom exceptions with implements Exception (Lecture 085)
  • splitting code into library files with import and _ private names (Lectures 101–102)

Connection With Previous Lectures

Say Sara writes bank.dart with an Account class whose private _balance is changed only by deposit. Ali imports it and writes:

dart
class FakeAccount implements Account {
  @override
  int get balance => 1000000;
}

It compiles. implements copies only the shape of Account (Lecture 063), so Ali's object skips Sara's checks completely. Sara has no way to say "you may use my class, but not impersonate it". We met two exceptions already — Lecture 085 hit an Exception that "can't be extended outside of its library", and Lecture 082's sealed classes refused outside subtypes. Today we learn the whole set of modifiers behind those errors.


Learning Objectives

By the end of this lecture you will be able to:

  • Name the four things another library can do with a class: construct, extend, implement, mix in
  • Explain why class modifiers restrict only other libraries
  • Use interface class, base class and final class, and read their errors
  • Recall what abstract, sealed and mixin class add
  • Apply the rule that subtypes of base and final classes must be base, final or sealed
  • Combine modifiers into abstract interface class, abstract final class and others
  • Read a capability table for every modifier
  • Choose the right modifier when designing a library for others

1. Four Things Code Can Do With a Class

Another file can construct a class (Student()), extend it, implement it, or mix it in with with. A plain class allows the first three from anywhere (only a mixin or mixin class can be mixed in — Lecture 064).

Analogy — a class is a building with four doors. Modifiers lock some of them from the outside. People inside the building — code in the same library — still have every key.

school.dart

dart
class Student {
  String get name => 'Ali';
}

main.dart

dart
import 'school.dart';

class Monitor extends Student {}

class Guest implements Student {
  @override
  String get name => 'Sara';
}

void main() {
  print(Student().name);
  print(Monitor().name);
  print(Guest().name);
}
output
Ali
Ali
Sara

2. The Rule: Only Other Libraries Are Restricted

Every modifier in this lecture is a promise about other libraries. Inside the file that declares the class (Lecture 101: one file, one library), the author may still extend and implement it. That is what lets the author build their own subtypes while keeping outsiders out. Two modifiers are exceptions: abstract and sealed forbid construction everywhere.


3. abstract (Recap): No Construction

From Lecture 062: an abstract class cannot be created, not even in its own file. It can still be extended and implemented from anywhere.

dart
abstract class Shape {}

void main() {
  Shape();
}
output
Error: The class 'Shape' is abstract and can't be instantiated.
  Shape();
  ^^^^^

4. interface class: Implement, Don't Extend

An interface class can be constructed and implemented by anyone, but only its own library may extend it. Outsiders get the contract, never the code.

alerts.dart

dart
interface class Notifier {
  void send(String message) => print('Sent: $message');
}

class LoudNotifier extends Notifier {} // ✓ same library

main.dart

dart
import 'alerts.dart';

// class SmsNotifier extends Notifier {} // ✗
class SmsNotifier implements Notifier {
  @override
  void send(String message) => print('SMS: $message');
}

void main() {
  Notifier().send('Hi');
  LoudNotifier().send('Hello');
  SmsNotifier().send('Salam');
}
output
Sent: Hi
Sent: Hello
SMS: Salam

With the extends line uncommented:

output
Error: The class 'Notifier' can't be extended outside of its library because it's an interface class.

This is exactly Exception from dart:core — an interface class, which is why Lecture 085 wrote implements Exception. The author can change send's body later without breaking any subclass, because no outside subclass exists.


5. base class: Extend, Don't Implement

A base class is the opposite: other libraries may extend it, but not implement it. Every object of the type then really runs the base class's constructor and owns its private members — precisely what Sara needed.

bank.dart

dart
base class Account {
  int _balance = 0;
  int get balance => _balance;

  void deposit(int amount) {
    if (amount > 0) _balance += amount;
  }
}

main.dart

dart
import 'bank.dart';

// class FakeAccount implements Account { ... } // ✗
base class Savings extends Account {}

void main() {
  var savings = Savings()..deposit(500);
  print(savings.balance);
}
output
500

Trying the fake account from the connection section:

output
Error: The type 'FakeAccount' must be 'base', 'final' or 'sealed' because the supertype 'Account' is 'base'.
Error: The class 'Account' can't be implemented outside of its library because it's a base class.

Dart also reports the missing deposit. Note the first error: Savings must itself be marked base.


6. The Subtype Rule: base and final Spread

A subtype of a base or final class must be base, final or sealed — even inside the same library. Otherwise a plain subclass would reopen the door that was just locked.

dart
base class Account {}

class Current extends Account {}

void main() {}
output
Error: The type 'Current' must be 'base', 'final' or 'sealed' because the supertype 'Account' is 'base'.
Try adding 'base', 'final', or 'sealed' to the type.
class Current extends Account {}
      ^

Writing base class Current (or final class Current) fixes it.


7. final class: Use Only

A final class can be constructed by anyone, but other libraries can neither extend nor implement it. The family is closed; the author can change anything without breaking outside code.

shop.dart

dart
final class Receipt {
  Receipt(this.total);
  final int total;
}

final class GiftReceipt extends Receipt {
  GiftReceipt(super.total); // ✓ same library, marked final
}

main.dart

dart
import 'shop.dart';

// class MyReceipt extends Receipt { ... } // ✗
// class FakeReceipt implements Receipt { ... } // ✗

void main() {
  print(Receipt(1200).total);
  print(GiftReceipt(300).total);
}
output
1200
300
output
Error: The class 'Receipt' can't be extended outside of its library because it's a final class.
Error: The class 'Receipt' can't be implemented outside of its library because it's a final class.

8. sealed and mixin class (Recap)

sealed (Lecture 082) is final plus abstract plus a known list of subtypes, which is what makes exhaustive switch possible. mixin class (Lecture 064) is a class that can also follow with — the only modifier that opens a door. It combines only with abstract and base:

dart
interface mixin class Logger {}

void main() {}
output
Error: A mixin class can't be declared 'interface'.
Try removing the 'interface' keyword.
interface mixin class Logger {}
^^^^^^^^^

final and sealed give the same error.


9. Combining Modifiers

abstract stacks with base, interface and final:

  • abstract interface class — a pure contract: nobody constructs it, outsiders only implement it. This is the closest Dart has to an "interface" keyword.
  • abstract base class — a template that outsiders must extend.
  • abstract final class — nobody constructs, extends or implements it. Ideal for a holder of static constants.
dart
abstract interface class Payable {
  int get amount;
}

abstract final class Tax {
  static const rate = 0.17;
}

class Bill implements Payable {
  @override
  int get amount => 1000;
}

void main() {
  var bill = Bill();
  print(bill.amount * (1 + Tax.rate));
}
output
1170.0

10. The Capability Table

What another library may do (✓) — inside the declaring library, every class can be extended and implemented, subject to the subtype rule:

DeclarationConstructExtendImplementMix in
class✓✓✓✗
abstract class✗✓✓✗
base class✓✓ ¹✗✗
interface class✓✗✓✗
final class✓✗✗✗
sealed class✗✗✗✗
abstract base class✗✓ ¹✗✗
abstract interface class✗✗✓✗
abstract final class✗✗✗✗
mixin class✓✓✓✓
base mixin class✓✓ ¹✗✓ ¹

¹ The new subtype must be base, final or sealed. The ✗ for abstract and sealed construction also applies inside the library.


11. Choosing a Modifier as an API Author

  • Writing an app, not a library? A plain class is fine — you own both sides.
  • Outsiders should provide their own version (a notifier, a storage) → abstract interface class.
  • Outsiders should reuse your code and you rely on private state or constructor checks → base class.
  • Outsiders should only use it, and you want freedom to change it → final class.
  • You list every subtype and want exhaustive switch → sealed class.
  • Constants or static helpers only → abstract final class.

Loosening later (final → base) never breaks users; tightening later does. Start strict.


12. A Real-World Example

bank.dart

dart
abstract interface class Notifier {
  void send(String message);
}

base class Account {
  Account(this.owner, this.notifier);
  final String owner;
  final Notifier notifier;
  int _balance = 0;
  int get balance => _balance;

  void deposit(int amount) {
    if (amount <= 0) return;
    _balance += amount;
    notifier.send('$owner: +$amount, balance $_balance');
  }
}

final class Receipt {
  Receipt(this.account) : total = account.balance;
  final Account account;
  final int total;
  String get text => 'Receipt for ${account.owner}: $total';
}

main.dart

dart
import 'bank.dart';

class PrintNotifier implements Notifier {
  @override
  void send(String message) => print('[SMS] $message');
}

base class Savings extends Account {
  Savings(String owner) : super(owner, PrintNotifier());
  void addProfit() => deposit(balance ~/ 10);
}

void main() {
  var savings = Savings('Sara')
    ..deposit(1000)
    ..addProfit();
  print(Receipt(savings).text);
}
output
[SMS] Sara: +1000, balance 1000
[SMS] Sara: +100, balance 1100
Receipt for Sara: 1100

main.dart supplies its own Notifier, reuses Account through extends, and only uses Receipt. It cannot fake an Account or a Receipt.


Code Examples

Examples 1–6 are single files, so everything runs inside the declaring library.

Example 1 — Extending an interface class in Its Own Library

dart
interface class Greeter {
  String greet(String name) => 'Hello, $name';
}

class PoliteGreeter extends Greeter {
  @override
  String greet(String name) => '${super.greet(name)}, welcome!';
}

void main() {
  print(PoliteGreeter().greet('Zayan'));
}
output
Hello, Zayan, welcome!

Example 2 — Implementing a final class in Its Own Library

dart
final class Price {
  int get value => 100;
}

final class FreePrice implements Price {
  @override
  int get value => 0;
}

void main() {
  print(Price().value);
  print(FreePrice().value);
}
output
100
0

FreePrice must be final (or base/sealed) because of the subtype rule.

Example 3 — A base Chain

dart
base class Vehicle {
  int get wheels => 4;
}

base class Bike extends Vehicle {
  @override
  int get wheels => 2;
}

final class RaceBike extends Bike {}

void main() {
  print(Vehicle().wheels);
  print(RaceBike().wheels);
}
output
4
2

Example 4 — A mixin class Used Both Ways

dart
mixin class Timestamp {
  String stamp(String text) => '[10:00] $text';
}

class Note with Timestamp {}

void main() {
  print(Timestamp().stamp('alone'));
  print(Note().stamp('mixed in'));
}
output
[10:00] alone
[10:00] mixed in

Example 5 — An abstract final class of Constants

dart
abstract final class Limits {
  static const maxStudents = 30;
  static const passMark = 50;
}

void main() {
  print('Max: ${Limits.maxStudents}, pass: ${Limits.passMark}');
}
output
Max: 30, pass: 50

Example 6 — Implementing Exception

dart
class LowBalanceException implements Exception {
  LowBalanceException(this.needed);
  final int needed;
  @override
  String toString() => 'Need $needed more';
}

void main() {
  try {
    throw LowBalanceException(250);
  } on LowBalanceException catch (e) {
    print(e);
  }
}
output
Need 250 more

Exception lives in another library (dart:core) and is an interface class, so implements is the only option.

Example 7 — Student Marks Grader

grading.dart

dart
abstract interface class Grader {
  String grade(int marks);
}

main.dart

dart
import 'grading.dart';

class LetterGrader implements Grader {
  @override
  String grade(int marks) => marks >= 80 ? 'A' : 'B';
}

void main() {
  Grader grader = LetterGrader();
  for (var marks in [85, 72]) {
    print('$marks → ${grader.grade(marks)}');
  }
}
output
85 → A
72 → B

Example 8 — Shopping Cart with a final class

cart.dart

dart
final class Item {
  Item(this.name, this.price);
  final String name;
  final int price;
}

int total(List<Item> items) => items.fold(0, (sum, i) => sum + i.price);

main.dart

dart
import 'cart.dart';

void main() {
  var items = [Item('Notebook', 500), Item('Pen', 120)];
  print('Total: ${total(items)}');
}
output
Total: 620

total can trust that every Item is Sara's own class — nobody outside can subclass it.

Example 9 — Bank Balance with a base class

bank.dart

dart
base class Wallet {
  int _coins = 0;
  int get coins => _coins;
  void add(int n) => _coins += n;
}

main.dart

dart
import 'bank.dart';

base class KidsWallet extends Wallet {
  void addPocketMoney() => add(50);
}

void main() {
  var wallet = KidsWallet()
    ..add(100)
    ..addPocketMoney();
  print('Zayan has ${wallet.coins} coins');
}
output
Zayan has 150 coins

Example 10 — Temperature Sensors with an interface class

sensors.dart

dart
interface class Sensor {
  double read() => 25.0;
}

main.dart

dart
import 'sensors.dart';

class FakeSensor implements Sensor {
  @override
  double read() => 30.5;
}

void main() {
  for (var sensor in [Sensor(), FakeSensor()]) {
    print('${sensor.read()} °C');
  }
}
output
25.0 °C
30.5 °C

The default Sensor is usable directly, and tests can swap in a FakeSensor (Lecture 120).


Common Beginner Mistakes

1. Implementing a base class from another library

dart
// main.dart
// class FakeAccount implements Account { ... } // ✗
output
Error: The class 'Account' can't be implemented outside of its library because it's a base class.

Extend it instead, with base class FakeAccount extends Account.

2. Forgetting base on the subclass

dart
// class Savings extends Account {} // ✗ Account is base
base class Savings extends Account {}
output
Error: The type 'Savings' must be 'base', 'final' or 'sealed' because the supertype 'Account' is 'base'.

3. Extending Exception

dart
// class PaymentException extends Exception {} // ✗
class PaymentException implements Exception {}
output
Error: The class 'Exception' can't be extended outside of its library because it's an interface class.

4. Testing a modifier in one file

Putting final class Receipt and class MyReceipt extends Receipt in the same file only triggers the subtype rule — not the "outside of its library" error. To see what users of your library will see, test from a second file.

5. Constructing an abstract interface class

dart
// Payable(); // ✗
output
Error: The class 'Payable' is abstract and can't be instantiated.

6. interface or final on a mixin class

output
Error: A mixin class can't be declared 'interface'.

Best Practices

  • In application code, use plain classes unless a modifier states a real intention.
  • In a library, decide each public class's doors on purpose.
  • Prefer abstract interface class for contracts others implement.
  • Use base when correctness depends on your constructor or private fields.
  • Start with final; loosen later if users need more.
  • Use abstract final class for groups of constants.
  • Verify modifiers from a second file, as a user would.

Mini Challenge

In school.dart, declare Report so that other libraries can create it and implement it, but not extend it. Give it a String title(String name) method returning 'Report: <name>'. In main.dart, implement it as ShortReport returning 'R: <name>', and print both for 'Ali'.

Try it yourself before opening the solution.

Show solution

school.dart

dart
interface class Report {
  String title(String name) => 'Report: $name';
}

main.dart

dart
import 'school.dart';

class ShortReport implements Report {
  @override
  String title(String name) => 'R: $name';
}

void main() {
  print(Report().title('Ali'));
  print(ShortReport().title('Ali'));
}
output
Report: Ali
R: Ali

Practice Questions

Beginner

  1. Which modifier lets other libraries implement a class but not extend it?
  2. Which modifier lets other libraries extend a class but not implement it?
  3. Does final class Receipt stop code in the same file from extending it? Explain.

Application

  1. Write abstract final class Colors with two static const strings, primary = 'blue' and accent = 'gold', and print both.
  2. This file fails. Fix it with one keyword:
    dart
    base class Animal {}
    
    class Dog extends Animal {}
    
    void main() => print(Dog());
  3. In one file, declare interface class Discount with int apply(int price) => price - 50;, extend it as BigDiscount (subtracting 200), and print both for 1000.
  4. Ali's implements Account (Account is base, in another file) fails. Rewrite the declaration so it compiles, given Account has a no-argument constructor.
  5. Write a mixin class Counter with int count = 0; and void tick() => count++;. Use it directly and as a mixin in class Clock with Counter {}; tick each twice and print both counts.

Challenge

  1. You publish a Storage contract users implement with their own save(String key); nobody should construct or extend it. Write the declaration and one implementation MemoryStorage that prints 'Saved key'.
  2. Fill a table row for abstract base class: construct, extend, implement, mix in — from another library.

Practice Answers

Question 1 — Solution

interface class.

Question 2 — Solution

base class.

Question 3 — Solution

No. Modifiers restrict other libraries only. In the same file, final class GiftReceipt extends Receipt is allowed — the subclass just has to be base, final or sealed.

Question 4 — Solution

dart
abstract final class Colors {
  static const primary = 'blue';
  static const accent = 'gold';
}

void main() {
  print(Colors.primary);
  print(Colors.accent);
}
output
blue
gold

Question 5 — Solution

dart
base class Animal {}

base class Dog extends Animal {}

void main() => print(Dog());
output
Instance of 'Dog'

Question 6 — Solution

dart
interface class Discount {
  int apply(int price) => price - 50;
}

class BigDiscount extends Discount {
  @override
  int apply(int price) => price - 200;
}

void main() {
  print(Discount().apply(1000));
  print(BigDiscount().apply(1000));
}
output
950
800

BigDiscount is in the same library, so extending is allowed.

Question 7 — Solution

dart
base class MyAccount extends Account {}

Implementing is closed, so extend — and mark the subclass base.

Question 8 — Solution

dart
mixin class Counter {
  int count = 0;
  void tick() => count++;
}

class Clock with Counter {}

void main() {
  var counter = Counter()
    ..tick()
    ..tick();
  var clock = Clock()
    ..tick()
    ..tick();
  print(counter.count);
  print(clock.count);
}
output
2
2

Question 9 — Solution

dart
abstract interface class Storage {
  void save(String key);
}

class MemoryStorage implements Storage {
  @override
  void save(String key) => print('Saved $key');
}

void main() {
  MemoryStorage().save('key');
}
output
Saved key

Question 10 — Solution

Construct ✗ (abstract) · Extend ✓ (subclass must be base, final or sealed) · Implement ✗ (base) · Mix in ✗ (not a mixin class).


Quick Revision

  1. What four things can another library do with a class?
  2. Which modifiers also apply inside the declaring library?
  3. Why is Exception implemented rather than extended?
  4. What must a subclass of a base class be?
  5. Which two modifiers can combine with mixin class?
  6. What is abstract final class good for?

Key Takeaways

  • Libraries can construct, extend, implement or mix in a class; modifiers close some doors.
  • Modifiers restrict other libraries; only abstract and sealed also block construction inside.
  • interface — implement only; base — extend only; final — neither; sealed — neither, plus exhaustive switches.
  • Subtypes of base and final classes must be base, final or sealed, even in the same file.
  • mixin class adds mix-in ability and combines only with abstract and base.
  • abstract interface class is a pure contract; abstract final class holds statics.
  • Library authors should start strict — loosening never breaks users.

What's Next?

Lecture 131 — Private Named Parameters & Emerging Dart Features. Modifiers rounded out how Dart 3 controls classes. Next we look at the newest language changes — private named parameters and the features still arriving — and how to follow them as Dart evolves.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.