Watch the video (10 min)
On this page22 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is Encapsulation?
  5. 2. The Problem: Public Fields Can Be Broken
  6. 3. Making a Field Private with _
  7. 4. Privacy Is Per File, Not Per Class
  8. 5. Real Privacy: Two Files and import
  9. 6. A Public Getter over a Private Field
  10. 7. Validation in Methods
  11. 8. Private Methods
  12. 9. Keeping an Invariant
  13. 10. A Real-World Example
  14. Code Examples
  15. Common Beginner Mistakes
  16. Best Practices
  17. Mini Challenge
  18. Practice Questions
  19. Practice Answers
  20. Quick Revision
  21. Key Takeaways
  22. What's Next?

Lecture 57 — Private Members & Encapsulation

Category9 — Object-Oriented Programming
Lecture057 of 140
Estimated video length10 min
TargetDart 3.12.2
LevelBeginner

A public field lets any code write any value into an object, even -500 into 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.field parameters
  • 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:

dart
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:

dart
void main() {
  var account = BankAccount('Ali', 1000);
  account.balance = -500;
  print('${account.owner}: ${account.balance}');
}
output
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:

dart
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();
}
output
Ali: 1000

The 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:

dart
void main() {
  var account = BankAccount('Ali');
  account._balance = -500; // allowed: main() is in the same file
  account.showBalance();
}
output
Ali: -500

It 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

dart
class BankAccount {
  String owner;
  int _balance = 0;

  BankAccount(this.owner);

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

  void showBalance() {
    print('$owner: $_balance');
  }
}

main.dart

dart
import 'bank_account.dart';

void main() {
  var account = BankAccount('Ali');
  account.deposit(1000);
  account.showBalance();
}

Run dart run main.dart:

output
Ali: 1000

import '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:

dart
import 'bank_account.dart';

void main() {
  var account = BankAccount('Ali');
  account._balance = -500; // ✗
  print(account._balance); // ✗
}
output
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:

dart
  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:

dart
account.balance = 1000000; // ✗
output
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:

dart
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}');
}
output
Deposit rejected: -50
Withdrawal rejected: 800
Balance: 300

Lecture 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:

dart
  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:

output
[Ali] Deposited 300
[Ali] Deposit rejected: 0

Private 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.

dart
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}');
}
output
true
false
Ali: 600, Sara: 0

The 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:

dart
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}');
}
output
+1000 → 1000
-250 → 750
Balance: 750

Private 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

dart
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}');
}
output
Ali: 85

Example 2 — A Private Helper Method

dart
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();
}
output
Hello, Zayan!

Example 3 — A Validating Setter

dart
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}');
}
output
Sara is 18

The getter and setter share the public name age; the data lives in _age.

Example 4 — Read-Only Counter

dart
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}');
}
output
Count: 3

The count can only go up by one.

Example 5 — Private Top-Level Names

dart
const _taxRate = 0.1;

double _withTax(double price) => price + price * _taxRate;

void main() {
  print(_withTax(200));
  print(_withTax(50));
}
output
220.0
55.0

Any file importing this one cannot see these two names.

Example 6 — Same File, No Protection

dart
class Wallet {
  int _cash = 100;
}

void main() {
  var wallet = Wallet();
  wallet._cash = 999; // same file, so this compiles
  print(wallet._cash);
}
output
999

Example 7 — Student Marks

dart
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}');
}
output
Ignored invalid mark: 120
Zayan: 80.0

Only valid marks reach the list, so average can trust its data.

Example 8 — Shopping Total

dart
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}');
}
output
Invalid price for Bag
Items: 2
Total: 290.0

Outside code sees a count and a total, never the map.

Example 9 — Bank Balance

dart
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}');
}
output
true
false
Balance: 700

this._balance works as a positional constructor parameter.

Example 10 — Temperature

dart
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');
}
output
Below absolute zero: -300.0
25.0 °C = 77.0 °F

One 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

dart
// in main.dart
// account._balance = -500;
// account._log('Hacked!');
output
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

dart
List<String> get history => _history; // hands out the real list

After deposit(500), outside code adds '+1000000' to account.history, and printing it shows:

output
[+500, +1000000]

Return List<String>.unmodifiable(_history) instead. Then the same add stops with:

output
Unsupported operation: Cannot add to an unmodifiable list

4. Putting _ on local variables

A local variable is already invisible outside its function, so _ adds nothing. dart analyze reports:

output
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 analyze reminds 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
dart
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}');
}
output
Ignored: -10
Zayan: score 30, best 65

Nobody 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

  1. Write a class Box with a private _items field (starting at 0), a getter items, and a method addItem() that adds one. Call addItem() twice and print items.
  2. In one sentence: what decides whether a Dart name is private, and private to what?
  3. A class Safe has a private field _code = '1234'. In the same file, main() runs safe._code = '0000'; and prints safe._code. Does it compile? What prints?

Application

  1. In Example 8, list the Cart members that are public and those that are private. Why is it safe to change _items from a map to a list later?
  2. Write Lamp with a private _isOn (starting false), a method toggle(), and a getter status returning 'ON' or 'OFF'. Print the status, toggle once, print, toggle twice, print.
  3. Write Invoice with a private method _tax(double amount) returning 15% of the amount, and a public printTotal(double amount) that prints Tax: and Total:. Use 200.
  4. Write Shop with a private _stock set by the constructor, a getter stock, and bool sell(int quantity) that refuses quantities of 0 or less or more than the stock. Start at 10, sell 4, then 8, then print Stock: ….
  5. Write ShoppingList with a private List<String>, an add that ignores duplicates, a getter count, and a getter items returning an unmodifiable copy. Add 'Milk', 'Bread', 'Milk', then print count: items.

Challenge

  1. Put a Counter (private _count, getter count, increment()) in counter.dart. In main.dart, import it, increment, print count, then write counter._count = 10;. Run main.dart. Does the print run?
  2. Give BankAccount (owner, private _balance from the constructor, getter balance) a method bool transfer(BankAccount to, int amount) that refuses invalid amounts. Ali has 1000, Sara 200. Ali sends Sara 300; Sara tries to send Ali 900. Print both results and balances. Why may transfer touch to._balance?

Practice Answers

Question 1 — Solution

dart
class Box {
  int _items = 0;

  int get items => _items;

  void addItem() {
    _items++;
  }
}

void main() {
  var box = Box();
  box.addItem();
  box.addItem();
  print(box.items);
}
output
2

Question 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

dart
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);
}
output
OFF
ON
ON

Question 6 — Solution

dart
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);
}
output
Tax: 30.0
Total: 230.0

Question 7 — Solution

dart
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}');
}
output
true
false
Stock: 6

Question 8 — Solution

dart
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}');
}
output
2: [Milk, Bread]

Question 9 — Solution

With Counter in counter.dart, main.dart is:

dart
import 'counter.dart';

void main() {
  var counter = Counter();
  counter.increment();
  print(counter.count);
  counter._count = 10;
}
output
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

dart
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}');
}
output
true
false
Ali: 700, Sara: 500

transfer is written in the same file as _balance, and privacy is per file, so it may reach another object's _balance.


Quick Revision

  1. What is encapsulation?
  2. How do you make a field or method private in Dart?
  3. In Dart, what is a library?
  4. Can main() read _balance if it is in the same file as the class?
  5. How do you make a property read-only from outside?
  6. What is an invariant? Give one for a bank account.
  7. 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 no private keyword.
  • 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.


Video lecture

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