On this page24 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. Four Things Code Can Do With a Class
- 2. The Rule: Only Other Libraries Are Restricted
- 3. abstract (Recap): No Construction
- 4. interface class: Implement, Don't Extend
- 5. base class: Extend, Don't Implement
- 6. The Subtype Rule: base and final Spread
- 7. final class: Use Only
- 8. sealed and mixin class (Recap)
- 9. Combining Modifiers
- 10. The Capability Table
- 11. Choosing a Modifier as an API Author
- 12. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 130 — Class Modifiers
| Category | 22 — Modern Dart Features |
| Lecture | 130 of 140 |
| Estimated video length | 15 min |
| Target | Dart 3.12.2 |
| Level | Advanced |
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 classandimplements(Lectures 059, 062–063)mixin,withandmixin class(Lecture 064)sealed classand its same-file rule (Lecture 082)- custom exceptions with
implements Exception(Lecture 085) - splitting code into library files with
importand_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:
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 classandfinal class, and read their errors - Recall what
abstract,sealedandmixin classadd - Apply the rule that subtypes of
baseandfinalclasses must bebase,finalorsealed - Combine modifiers into
abstract interface class,abstract final classand 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
class Student {
String get name => 'Ali';
}main.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);
}Ali
Ali
Sara2. 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.
abstract class Shape {}
void main() {
Shape();
}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
interface class Notifier {
void send(String message) => print('Sent: $message');
}
class LoudNotifier extends Notifier {} // ✓ same librarymain.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');
}Sent: Hi
Sent: Hello
SMS: SalamWith the extends line uncommented:
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
base class Account {
int _balance = 0;
int get balance => _balance;
void deposit(int amount) {
if (amount > 0) _balance += amount;
}
}main.dart
import 'bank.dart';
// class FakeAccount implements Account { ... } // ✗
base class Savings extends Account {}
void main() {
var savings = Savings()..deposit(500);
print(savings.balance);
}500Trying the fake account from the connection section:
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.
base class Account {}
class Current extends Account {}
void main() {}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
final class Receipt {
Receipt(this.total);
final int total;
}
final class GiftReceipt extends Receipt {
GiftReceipt(super.total); // ✓ same library, marked final
}main.dart
import 'shop.dart';
// class MyReceipt extends Receipt { ... } // ✗
// class FakeReceipt implements Receipt { ... } // ✗
void main() {
print(Receipt(1200).total);
print(GiftReceipt(300).total);
}1200
300Error: 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:
interface mixin class Logger {}
void main() {}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 ofstaticconstants.
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));
}1170.010. The Capability Table
What another library may do (✓) — inside the declaring library, every class can be extended and implemented, subject to the subtype rule:
| Declaration | Construct | Extend | Implement | Mix 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
classis 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
statichelpers only →abstract final class.
Loosening later (final → base) never breaks users; tightening later does. Start strict.
12. A Real-World Example
bank.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
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);
}[SMS] Sara: +1000, balance 1000
[SMS] Sara: +100, balance 1100
Receipt for Sara: 1100main.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
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'));
}Hello, Zayan, welcome!Example 2 — Implementing a final class in Its Own Library
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);
}100
0FreePrice must be final (or base/sealed) because of the subtype rule.
Example 3 — A base Chain
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);
}4
2Example 4 — A mixin class Used Both Ways
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'));
}[10:00] alone
[10:00] mixed inExample 5 — An abstract final class of Constants
abstract final class Limits {
static const maxStudents = 30;
static const passMark = 50;
}
void main() {
print('Max: ${Limits.maxStudents}, pass: ${Limits.passMark}');
}Max: 30, pass: 50Example 6 — Implementing Exception
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);
}
}Need 250 moreException lives in another library (dart:core) and is an interface class, so implements is
the only option.
Example 7 — Student Marks Grader
grading.dart
abstract interface class Grader {
String grade(int marks);
}main.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)}');
}
}85 → A
72 → BExample 8 — Shopping Cart with a final class
cart.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
import 'cart.dart';
void main() {
var items = [Item('Notebook', 500), Item('Pen', 120)];
print('Total: ${total(items)}');
}Total: 620total 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
base class Wallet {
int _coins = 0;
int get coins => _coins;
void add(int n) => _coins += n;
}main.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');
}Zayan has 150 coinsExample 10 — Temperature Sensors with an interface class
sensors.dart
interface class Sensor {
double read() => 25.0;
}main.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');
}
}25.0 °C
30.5 °CThe 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
// main.dart
// class FakeAccount implements Account { ... } // ✗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
// class Savings extends Account {} // ✗ Account is base
base class Savings extends Account {}Error: The type 'Savings' must be 'base', 'final' or 'sealed' because the supertype 'Account' is 'base'.3. Extending Exception
// class PaymentException extends Exception {} // ✗
class PaymentException implements Exception {}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
// Payable(); // ✗Error: The class 'Payable' is abstract and can't be instantiated.6. interface or final on a mixin class
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 classfor contracts others implement. - Use
basewhen correctness depends on your constructor or private fields. - Start with
final; loosen later if users need more. - Use
abstract final classfor 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
interface class Report {
String title(String name) => 'Report: $name';
}main.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'));
}Report: Ali
R: AliPractice Questions
Beginner
- Which modifier lets other libraries implement a class but not extend it?
- Which modifier lets other libraries extend a class but not implement it?
- Does
final class Receiptstop code in the same file from extending it? Explain.
Application
- Write
abstract final class Colorswith twostatic conststrings,primary = 'blue'andaccent = 'gold', and print both. - This file fails. Fix it with one keyword:
base class Animal {} class Dog extends Animal {} void main() => print(Dog()); - In one file, declare
interface class Discountwithint apply(int price) => price - 50;, extend it asBigDiscount(subtracting200), and print both for1000. - Ali's
implements Account(Account isbase, in another file) fails. Rewrite the declaration so it compiles, givenAccounthas a no-argument constructor. - Write a
mixin class Counterwithint count = 0;andvoid tick() => count++;. Use it directly and as a mixin inclass Clock with Counter {}; tick each twice and print both counts.
Challenge
- You publish a
Storagecontract users implement with their ownsave(String key); nobody should construct or extend it. Write the declaration and one implementationMemoryStoragethat prints'Saved key'. - 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
abstract final class Colors {
static const primary = 'blue';
static const accent = 'gold';
}
void main() {
print(Colors.primary);
print(Colors.accent);
}blue
goldQuestion 5 — Solution
base class Animal {}
base class Dog extends Animal {}
void main() => print(Dog());Instance of 'Dog'Question 6 — Solution
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));
}950
800BigDiscount is in the same library, so extending is allowed.
Question 7 — Solution
base class MyAccount extends Account {}Implementing is closed, so extend — and mark the subclass base.
Question 8 — Solution
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);
}2
2Question 9 — Solution
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');
}Saved keyQuestion 10 — Solution
Construct ✗ (abstract) · Extend ✓ (subclass must be base, final or sealed) ·
Implement ✗ (base) · Mix in ✗ (not a mixin class).
Quick Revision
- What four things can another library do with a class?
- Which modifiers also apply inside the declaring library?
- Why is
Exceptionimplemented rather than extended? - What must a subclass of a
baseclass be? - Which two modifiers can combine with
mixin class? - What is
abstract final classgood for?
Key Takeaways
- Libraries can construct, extend, implement or mix in a class; modifiers close some doors.
- Modifiers restrict other libraries; only
abstractandsealedalso block construction inside. interface— implement only;base— extend only;final— neither;sealed— neither, plus exhaustive switches.- Subtypes of
baseandfinalclasses must bebase,finalorsealed, even in the same file. mixin classadds mix-in ability and combines only withabstractandbase.abstract interface classis a pure contract;abstract final classholds 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.