On this page22 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. What Is Encapsulation?
- 2. The Problem: Public Fields Can Be Broken
- 3. Making a Field Private with _
- 4. Privacy Is Per File, Not Per Class
- 5. Real Privacy: Two Files and import
- 6. A Public Getter over a Private Field
- 7. Validation in Methods
- 8. Private Methods
- 9. Keeping an Invariant
- 10. 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 57 — Private Members & Encapsulation
| Category | 9 — Object-Oriented Programming |
| Lecture | 057 of 140 |
| Estimated video length | 10 min |
| Target | Dart 3.12.2 |
| Level | Beginner |
A public field lets any code write any value into an object, even
-500into a balance. A leading underscore makes a member private, so the class decides how its data changes.
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 052–056:
- classes, objects, fields, and methods
- constructors, including
this.fieldparameters this, getters (get), and setters (set)- lists and maps, including
List.unmodifiable(Lecture 046)
Connection With Previous Lectures
So far, every field we wrote was open to everyone:
class BankAccount {
String owner;
int balance;
BankAccount(this.owner, this.balance);
}Any code can write account.balance = -500. A setter from Lecture 056 can check values, but
not while the field itself is open: code simply goes around it. We need a way to say "only
the class may touch this."
Learning Objectives
By the end of this lecture you will be able to:
- Explain what encapsulation is and why it matters
- Make fields and methods private with a leading underscore
_ - Explain that privacy works per file (library), not per class
- Split code into two files connected with
import - Expose a private field through a read-only getter
- Validate every change in methods and setters
- Keep an object's invariant (a rule that is always true)
- Recognize the errors for touching another file's private members
1. What Is Encapsulation?
Encapsulation means keeping an object's data inside the object and allowing changes only through methods the class provides.
Analogy — a bank counter. You cannot walk into the vault and change the numbers yourself. You ask the cashier to deposit or withdraw, and the cashier checks the request first.
Fields are the vault; public methods are the cashier.
2. The Problem: Public Fields Can Be Broken
Using the class above:
void main() {
var account = BankAccount('Ali', 1000);
account.balance = -500;
print('${account.owner}: ${account.balance}');
}Ali: -500-500 is a valid int, just not a valid balance.
3. Making a Field Private with _
A name that starts with an underscore is private. Rename balance to _balance and add a
method that changes it:
class BankAccount {
String owner;
int _balance = 0;
BankAccount(this.owner);
void deposit(int amount) {
_balance += amount;
}
void showBalance() {
print('$owner: $_balance');
}
}
void main() {
var account = BankAccount('Ali');
account.deposit(1000);
account.showBalance();
}Ali: 1000The underscore is part of the name. Dart has no private keyword; the _ does that job, on
methods, getters, top-level functions, and classes too.
4. Privacy Is Per File, Not Per Class
Here is the surprise. Replace main() in the file from section 3 with this one:
void main() {
var account = BankAccount('Ali');
account._balance = -500; // allowed: main() is in the same file
account.showBalance();
}Ali: -500It runs. A private name is private to its library, and a library is one .dart file. Everything in that file can see _balance, including main(). Code in a different
file cannot.
5. Real Privacy: Two Files and import
To see privacy work, put the class in its own file, next to a main.dart.
bank_account.dart
class BankAccount {
String owner;
int _balance = 0;
BankAccount(this.owner);
void deposit(int amount) {
_balance += amount;
}
void showBalance() {
print('$owner: $_balance');
}
}main.dart
import 'bank_account.dart';
void main() {
var account = BankAccount('Ali');
account.deposit(1000);
account.showBalance();
}Run dart run main.dart:
Ali: 1000import 'bank_account.dart'; lets main.dart use the public names from the other file. The
path is relative to main.dart. We will study libraries and imports properly in Lectures 101 —
Dart Libraries and 102 — Creating Your Own Libraries.
Now try to break in from main.dart:
import 'bank_account.dart';
void main() {
var account = BankAccount('Ali');
account._balance = -500; // ✗
print(account._balance); // ✗
}main.dart:5:11: Error: The setter '_balance' isn't defined for the type 'BankAccount'.
- 'BankAccount' is from 'bank_account.dart'.
Try correcting the name to the name of an existing setter, or defining a setter or field named '_balance'.
account._balance = -500; // ✗
^^^^^^^^
main.dart:6:17: Error: The getter '_balance' isn't defined for the type 'BankAccount'.
- 'BankAccount' is from 'bank_account.dart'.
Try correcting the name to the name of an existing getter, or defining a getter or field named '_balance'.
print(account._balance); // ✗
^^^^^^^^From outside the file, _balance does not exist at all. Dart says "isn't defined", not
"forbidden".
6. A Public Getter over a Private Field
Outside code still needs to read the balance. Add a getter, with no setter, to section 3's class:
int get balance => _balance;Now print(account.balance) works from any file.
balance is now read-only from outside. Writing to it fails, even in the same file, because
no setter named balance exists:
account.balance = 1000000; // ✗Error: The setter 'balance' isn't defined for the type 'BankAccount'.7. Validation in Methods
Now that methods are the only way in, they can check every request:
class BankAccount {
String owner;
int _balance = 0;
BankAccount(this.owner);
int get balance => _balance;
void deposit(int amount) {
if (amount <= 0) {
print('Deposit rejected: $amount');
return;
}
_balance += amount;
}
void withdraw(int amount) {
if (amount <= 0 || amount > _balance) {
print('Withdrawal rejected: $amount');
return;
}
_balance -= amount;
}
}
void main() {
var account = BankAccount('Zayan');
account.deposit(500);
account.deposit(-50);
account.withdraw(800);
account.withdraw(200);
print('Balance: ${account.balance}');
}Deposit rejected: -50
Withdrawal rejected: 800
Balance: 300Lecture 085 — throw & Custom Exceptions shows how to reject bad values with an error.
8. Private Methods
Helpers that only the class needs should be private too. In the class from section 7,
deposit can use two private helpers:
bool _isValid(int amount) => amount > 0;
void _log(String message) {
print('[$owner] $message');
}
void deposit(int amount) {
if (!_isValid(amount)) {
_log('Deposit rejected: $amount');
return;
}
_balance += amount;
_log('Deposited $amount');
}For Ali, deposit(300) then deposit(0) prints:
[Ali] Deposited 300
[Ali] Deposit rejected: 0Private helpers can be renamed or deleted later without breaking other files.
9. Keeping an Invariant
An invariant is a rule that is always true for an object. Ours: the balance is never negative. Encapsulation makes it possible: every change, constructor included, goes through code the class controls.
class BankAccount {
int _balance = 0;
BankAccount(int openingBalance) {
if (openingBalance > 0) {
_balance = openingBalance;
}
}
int get balance => _balance;
bool withdraw(int amount) {
if (amount <= 0 || amount > _balance) {
return false;
}
_balance -= amount;
return true;
}
}
void main() {
var ali = BankAccount(1000);
var sara = BankAccount(-300);
print(ali.withdraw(400));
print(ali.withdraw(5000));
print('Ali: ${ali.balance}, Sara: ${sara.balance}');
}true
false
Ali: 600, Sara: 0The bool result tells the caller whether it worked.
10. A Real-World Example
A bank account with a private history. The getter returns an unmodifiable copy, so nobody outside can add fake entries:
class BankAccount {
final String owner;
int _balance = 0;
final List<String> _history = [];
BankAccount(this.owner);
int get balance => _balance;
List<String> get history => List<String>.unmodifiable(_history);
bool _isValid(int amount) => amount > 0;
void _record(String entry) {
_history.add('$entry → $_balance');
}
bool deposit(int amount) {
if (!_isValid(amount)) return false;
_balance += amount;
_record('+$amount');
return true;
}
bool withdraw(int amount) {
if (!_isValid(amount) || amount > _balance) return false;
_balance -= amount;
_record('-$amount');
return true;
}
}
void main() {
var account = BankAccount('Sara');
account.deposit(1000);
account.withdraw(250);
for (var entry in account.history) {
print(entry);
}
print('Balance: ${account.balance}');
}+1000 → 1000
-250 → 750
Balance: 750Private data, private helpers, a small public surface, and an invariant no other file can break.
Code Examples
Each example is one file, so main() could reach private members. It uses only public ones,
as another file would have to.
Example 1 — Private Field with a Getter
class Student {
String name;
int _marks = 0;
Student(this.name);
int get marks => _marks;
void setMarks(int value) {
if (value >= 0 && value <= 100) {
_marks = value;
}
}
}
void main() {
var student = Student('Ali');
student.setMarks(85);
student.setMarks(140);
print('${student.name}: ${student.marks}');
}Ali: 85Example 2 — A Private Helper Method
class Greeter {
String name;
Greeter(this.name);
String _capitalize(String text) =>
text[0].toUpperCase() + text.substring(1).toLowerCase();
void greet() {
print('Hello, ${_capitalize(name)}!');
}
}
void main() {
Greeter('zAYAN').greet();
}Hello, Zayan!Example 3 — A Validating Setter
class Person {
final String name;
int _age = 0;
Person(this.name);
int get age => _age;
set age(int value) {
if (value >= 0) {
_age = value;
}
}
}
void main() {
var person = Person('Sara');
person.age = 18;
person.age = -4;
print('${person.name} is ${person.age}');
}Sara is 18The getter and setter share the public name age; the data lives in _age.
Example 4 — Read-Only Counter
class Counter {
int _count = 0;
int get count => _count;
void increment() {
_count++;
}
}
void main() {
var counter = Counter();
counter.increment();
counter.increment();
counter.increment();
print('Count: ${counter.count}');
}Count: 3The count can only go up by one.
Example 5 — Private Top-Level Names
const _taxRate = 0.1;
double _withTax(double price) => price + price * _taxRate;
void main() {
print(_withTax(200));
print(_withTax(50));
}220.0
55.0Any file importing this one cannot see these two names.
Example 6 — Same File, No Protection
class Wallet {
int _cash = 100;
}
void main() {
var wallet = Wallet();
wallet._cash = 999; // same file, so this compiles
print(wallet._cash);
}999Example 7 — Student Marks
class Report {
final String student;
final List<int> _marks = [];
Report(this.student);
void addMark(int mark) {
if (mark < 0 || mark > 100) {
print('Ignored invalid mark: $mark');
return;
}
_marks.add(mark);
}
double get average =>
_marks.isEmpty ? 0 : _marks.reduce((a, b) => a + b) / _marks.length;
}
void main() {
var report = Report('Zayan');
report.addMark(80);
report.addMark(75);
report.addMark(120);
report.addMark(85);
print('${report.student}: ${report.average}');
}Ignored invalid mark: 120
Zayan: 80.0Only valid marks reach the list, so average can trust its data.
Example 8 — Shopping Total
class Cart {
final Map<String, double> _items = {};
void add(String product, double price) {
if (price <= 0) {
print('Invalid price for $product');
return;
}
_items[product] = price;
}
int get itemCount => _items.length;
double get total => _items.values.fold(0, (sum, price) => sum + price);
}
void main() {
var cart = Cart();
cart.add('Notebook', 250);
cart.add('Pen', 40);
cart.add('Bag', -100);
print('Items: ${cart.itemCount}');
print('Total: ${cart.total}');
}Invalid price for Bag
Items: 2
Total: 290.0Outside code sees a count and a total, never the map.
Example 9 — Bank Balance
class BankAccount {
int _balance;
BankAccount(this._balance);
int get balance => _balance;
bool withdraw(int amount) {
if (amount <= 0 || amount > _balance) return false;
_balance -= amount;
return true;
}
}
void main() {
var account = BankAccount(1000);
print(account.withdraw(300));
print(account.withdraw(900));
print('Balance: ${account.balance}');
}true
false
Balance: 700this._balance works as a positional constructor parameter.
Example 10 — Temperature
class Thermometer {
double _celsius = 0;
double get celsius => _celsius;
double get fahrenheit => _celsius * 9 / 5 + 32;
set celsius(double value) {
if (value < -273.15) {
print('Below absolute zero: $value');
return;
}
_celsius = value;
}
}
void main() {
var room = Thermometer();
room.celsius = 25;
room.celsius = -300;
print('${room.celsius} °C = ${room.fahrenheit} °F');
}Below absolute zero: -300.0
25.0 °C = 77.0 °FOne private value, two public views that can never disagree.
Common Beginner Mistakes
1. Thinking _ protects a field from the rest of the same file
It does not (see section 4). To protect a class from other code, put that code in a different file.
2. Using a private field or method from another file
// in main.dart
// account._balance = -500;
// account._log('Hacked!');Error: The setter '_balance' isn't defined for the type 'BankAccount'.
Error: The method '_log' isn't defined for the type 'BankAccount'.Use the public API instead.
3. Returning the private list itself
List<String> get history => _history; // hands out the real listAfter deposit(500), outside code adds '+1000000' to account.history, and printing it
shows:
[+500, +1000000]Return List<String>.unmodifiable(_history) instead. Then the same add stops with:
Unsupported operation: Cannot add to an unmodifiable list4. Putting _ on local variables
A local variable is already invisible outside its function, so _ adds nothing. dart analyze
reports:
The local variable '_total' starts with an underscore. Try renaming the variable to not start with an underscore.Best Practices
- Make fields private by default; make something public only when other code needs it.
- Expose data through getters. Change it only through methods or setters that validate.
- Keep helper methods private so you can change them freely.
- Never return a private list or map directly. Return an unmodifiable copy.
- Make a private field that never changes
final.dart analyzereminds you:The private field _balance could be 'final'.
Mini Challenge
Write a ScoreBoard with private _score and _best and a getter for each.
addPoints(int points) prints Ignored: … for values of 0 or less, otherwise adds them and
updates _best when beaten. reset() zeroes the score but keeps the best. For 'Zayan': add
40, 25, -10, reset, add 30.
Try it yourself before opening the solution.
Show solution
class ScoreBoard {
final String player;
int _score = 0;
int _best = 0;
ScoreBoard(this.player);
int get score => _score;
int get best => _best;
void addPoints(int points) {
if (points <= 0) {
print('Ignored: $points');
return;
}
_score += points;
if (_score > _best) {
_best = _score;
}
}
void reset() {
_score = 0;
}
}
void main() {
var board = ScoreBoard('Zayan');
board.addPoints(40);
board.addPoints(25);
board.addPoints(-10);
board.reset();
board.addPoints(30);
print('${board.player}: score ${board.score}, best ${board.best}');
}Ignored: -10
Zayan: score 30, best 65Nobody outside can set best, so it is always a score the player really reached.
Practice Questions
Work through these in order. They run from beginner to application to challenge.
Beginner
- Write a class
Boxwith a private_itemsfield (starting at0), a getteritems, and a methodaddItem()that adds one. CalladdItem()twice and printitems. - In one sentence: what decides whether a Dart name is private, and private to what?
- A class
Safehas a private field_code = '1234'. In the same file,main()runssafe._code = '0000';and printssafe._code. Does it compile? What prints?
Application
- In Example 8, list the
Cartmembers that are public and those that are private. Why is it safe to change_itemsfrom a map to a list later? - Write
Lampwith a private_isOn(startingfalse), a methodtoggle(), and a getterstatusreturning'ON'or'OFF'. Print the status, toggle once, print, toggle twice, print. - Write
Invoicewith a private method_tax(double amount)returning 15% of the amount, and a publicprintTotal(double amount)that printsTax:andTotal:. Use200. - Write
Shopwith a private_stockset by the constructor, a getterstock, andbool sell(int quantity)that refuses quantities of0or less or more than the stock. Start at10, sell4, then8, then printStock: …. - Write
ShoppingListwith a privateList<String>, anaddthat ignores duplicates, a gettercount, and a getteritemsreturning an unmodifiable copy. Add'Milk','Bread','Milk', then printcount: items.
Challenge
- Put a
Counter(private_count, gettercount,increment()) incounter.dart. Inmain.dart, import it, increment, printcount, then writecounter._count = 10;. Runmain.dart. Does theprintrun? - Give
BankAccount(owner, private_balancefrom the constructor, getterbalance) a methodbool transfer(BankAccount to, int amount)that refuses invalid amounts. Ali has1000, Sara200. Ali sends Sara300; Sara tries to send Ali900. Print both results and balances. Why maytransfertouchto._balance?
Practice Answers
Question 1 — Solution
class Box {
int _items = 0;
int get items => _items;
void addItem() {
_items++;
}
}
void main() {
var box = Box();
box.addItem();
box.addItem();
print(box.items);
}2Question 2 — Solution
A name is private when it starts with _, and it is private to its library: the one
.dart file it is written in, not the class.
Question 3 — Solution
It compiles, because main() is in the same file as Safe. It prints 0000.
Question 4 — Solution
Public: add, itemCount, total. Private: _items. No other file can use _items, so
changing it only means updating the three public members inside Cart. Callers keep working.
Question 5 — Solution
class Lamp {
bool _isOn = false;
String get status => _isOn ? 'ON' : 'OFF';
void toggle() {
_isOn = !_isOn;
}
}
void main() {
var lamp = Lamp();
print(lamp.status);
lamp.toggle();
print(lamp.status);
lamp.toggle();
lamp.toggle();
print(lamp.status);
}OFF
ON
ONQuestion 6 — Solution
class Invoice {
double _tax(double amount) => amount * 0.15;
void printTotal(double amount) {
print('Tax: ${_tax(amount)}');
print('Total: ${amount + _tax(amount)}');
}
}
void main() {
Invoice().printTotal(200);
}Tax: 30.0
Total: 230.0Question 7 — Solution
class Shop {
int _stock;
Shop(this._stock);
int get stock => _stock;
bool sell(int quantity) {
if (quantity <= 0 || quantity > _stock) return false;
_stock -= quantity;
return true;
}
}
void main() {
var shop = Shop(10);
print(shop.sell(4));
print(shop.sell(8));
print('Stock: ${shop.stock}');
}true
false
Stock: 6Question 8 — Solution
class ShoppingList {
final List<String> _items = [];
int get count => _items.length;
List<String> get items => List<String>.unmodifiable(_items);
void add(String item) {
if (_items.contains(item)) return;
_items.add(item);
}
}
void main() {
var list = ShoppingList();
list.add('Milk');
list.add('Bread');
list.add('Milk');
print('${list.count}: ${list.items}');
}2: [Milk, Bread]Question 9 — Solution
With Counter in counter.dart, main.dart is:
import 'counter.dart';
void main() {
var counter = Counter();
counter.increment();
print(counter.count);
counter._count = 10;
}main.dart:7:11: Error: The setter '_count' isn't defined for the type 'Counter'.
- 'Counter' is from 'counter.dart'.
Try correcting the name to the name of an existing setter, or defining a setter or field named '_count'.
counter._count = 10;
^^^^^^The print never runs: the error is found at compile time, so the program never starts.
Question 10 — Solution
class BankAccount {
final String owner;
int _balance;
BankAccount(this.owner, this._balance);
int get balance => _balance;
bool transfer(BankAccount to, int amount) {
if (amount <= 0 || amount > _balance) return false;
_balance -= amount;
to._balance += amount;
return true;
}
}
void main() {
var ali = BankAccount('Ali', 1000);
var sara = BankAccount('Sara', 200);
print(ali.transfer(sara, 300));
print(sara.transfer(ali, 900));
print('Ali: ${ali.balance}, Sara: ${sara.balance}');
}true
false
Ali: 700, Sara: 500transfer is written in the same file as _balance, and privacy is per file, so it may
reach another object's _balance.
Quick Revision
- What is encapsulation?
- How do you make a field or method private in Dart?
- In Dart, what is a library?
- Can
main()read_balanceif it is in the same file as the class? - How do you make a property read-only from outside?
- What is an invariant? Give one for a bank account.
- Why should a getter not return a private list directly?
Key Takeaways
- Encapsulation keeps data inside an object; only the class's methods change it.
- A leading
_makes a field, method, getter, function, or class private. Dart has noprivatekeyword. - Privacy is per library, which means per file, not per class. Code in the same file can see private members, and code in another file cannot.
- From another file, a private member simply "isn't defined".
- A public getter over a private field gives read-only access.
- Methods and setters validate every change, which keeps the object's invariant true.
What's Next?
Lecture 58 — Static Members. Every field so far belonged to one object: Ali's balance,
Sara's balance. Some data belongs to the class itself, such as a count of all accounts opened.
Next we learn static fields and methods, shared by the whole class.