Lesson 4: Protecting Your Data (Encapsulation)
Duration: 35 minutes
App Feature: π Making Sure Expenses Are Valid
What You'll Build: Add validation so users can't create invalid expenses
Prerequisites: Complete Lessons 1-3
What We'll Learn Today
By the end of this lesson, you'll be able to:
- β Make properties private to protect data
- β Use getters to read data safely
- β Use setters to validate data before saving
- β Prevent bugs like negative expenses or invalid categories
- β Understand why encapsulation matters
Part 1: The Problem - Unprotected Data
Let's look at our current Expense class:
class Expense {
String description;
double amount;
String category;
DateTime date;
Expense(this.description, this.amount, this.category, this.date);
}
void main() {
var coffee = Expense('Coffee', 4.50, 'Food', DateTime.now());
// Uh oh... we can do this!
coffee.amount = -100.50; // Negative expense?!
coffee.category = 'asdfgh'; // Invalid category!
coffee.description = ''; // Empty description!
print('Amount: \$${coffee.amount}'); // -$100.50 π±
}
Problems:
- β Anyone can set
amount
to a negative number - β Invalid categories like "asdfgh" are allowed
- β Empty descriptions are possible
- β No way to validate data
- β Easy to create bugs
What we need: A way to protect our data and validate it before accepting changes!
Part 2: Making Properties Private
In Dart, you make something private by adding an underscore _
prefix:
class Expense {
String _description; // Private - can't access from outside
double _amount; // Private
String _category; // Private
DateTime _date; // Private
Expense(this._description, this._amount, this._category, this._date);
}
void main() {
var coffee = Expense('Coffee', 4.50, 'Food', DateTime.now());
// β ERROR! These don't work anymore
// coffee._amount = -100;
// print(coffee._description);
// Private properties can't be accessed from outside the class
}
Understanding Private Properties:
String _description; // Underscore = private
- Can only be accessed inside the class
- Protected from outside modification
- No one can directly change the value
But wait... If everything is private, how do we read or change values? π€
Part 3: Getters - Reading Data Safely
Getters let you read private data in a controlled way:
class Expense {
String _description;
double _amount;
String _category;
DateTime _date;
Expense(this._description, this._amount, this._category, this._date);
// Getters - read-only access
String get description => _description;
double get amount => _amount;
String get category => _category;
DateTime get date => _date;
}
void main() {
var coffee = Expense('Coffee', 4.50, 'Food', DateTime.now());
// β
Can read using getters
print('Description: ${coffee.description}');
print('Amount: \$${coffee.amount}');
// β But can't modify directly
// coffee.amount = -100; // ERROR!
}
Understanding Getter Syntax:
double get amount => _amount;
-
double
= return type -
get
= keyword for getter -
amount
= getter name (no underscore!) -
=>
= arrow function (shorthand) -
_amount
= returns the private property
Alternative syntax (with function body):
double get amount {
return _amount;
}
Both work the same way!
Part 4: Setters - Writing Data with Validation
Setters let you change private data, but with validation:
class Expense {
String _description;
double _amount;
String _category;
DateTime _date;
Expense(this._description, this._amount, this._category, this._date);
// Getters
String get description => _description;
double get amount => _amount;
String get category => _category;
DateTime get date => _date;
// Setters with validation
set amount(double value) {
if (value < 0) {
print('β Error: Amount cannot be negative!');
return; // Don't change the value
}
_amount = value;
}
set description(String value) {
if (value.trim().isEmpty) {
print('β Error: Description cannot be empty!');
return;
}
_description = value;
}
set category(String value) {
List<String> validCategories = [
'Food',
'Transport',
'Bills',
'Entertainment',
'Health',
'Shopping',
'Other'
];
if (!validCategories.contains(value)) {
print('β Error: Invalid category "$value". Using "Other".');
_category = 'Other';
return;
}
_category = value;
}
}
void main() {
var coffee = Expense('Coffee', 4.50, 'Food', DateTime.now());
// β
Valid changes work
coffee.amount = 5.00;
print('New amount: \$${coffee.amount}');
// β Invalid changes are rejected
coffee.amount = -10; // Prints error, doesn't change
print('Amount still: \$${coffee.amount}'); // Still 5.00
coffee.description = ''; // Prints error, doesn't change
coffee.category = 'InvalidCategory'; // Changes to 'Other'
}
Output:
New amount: $5.00
β Error: Amount cannot be negative!
Amount still: $5.00
β Error: Description cannot be empty!
β Error: Invalid category "InvalidCategory". Using "Other".
Understanding Setter Syntax:
set amount(double value) {
if (value < 0) {
print('β Error: Amount cannot be negative!');
return;
}
_amount = value;
}
-
set
= keyword for setter -
amount
= setter name (matches getter name) -
(double value)
= the new value being set - Validate before setting
_amount
-
return
early if invalid
Part 5: Validation in Constructor
We should also validate when creating an expense:
class Expense {
String _description;
double _amount;
String _category;
DateTime _date;
Expense(String description, double amount, String category, DateTime date)
: _description = description,
_amount = amount,
_category = category,
_date = date {
// Validate in constructor body
if (_amount < 0) {
throw Exception('Amount cannot be negative!');
}
if (_description.trim().isEmpty) {
throw Exception('Description cannot be empty!');
}
List<String> validCategories = [
'Food', 'Transport', 'Bills', 'Entertainment', 'Health', 'Shopping', 'Other'
];
if (!validCategories.contains(_category)) {
print('β οΈ Warning: Invalid category "$_category". Changed to "Other".');
_category = 'Other';
}
}
// Getters
String get description => _description;
double get amount => _amount;
String get category => _category;
DateTime get date => _date;
// Setters
set amount(double value) {
if (value < 0) {
print('β Error: Amount cannot be negative!');
return;
}
_amount = value;
}
set category(String value) {
List<String> validCategories = [
'Food', 'Transport', 'Bills', 'Entertainment', 'Health', 'Shopping', 'Other'
];
if (!validCategories.contains(value)) {
print('β Error: Invalid category. Using "Other".');
_category = 'Other';
return;
}
_category = value;
}
}
void main() {
try {
var invalid = Expense('', -50, 'Food', DateTime.now());
} catch (e) {
print('Failed to create expense: $e');
}
// This works but changes category
var expense = Expense('Coffee', 4.50, 'InvalidCat', DateTime.now());
print('Category: ${expense.category}'); // 'Other'
}
Part 6: Computed Properties with Getters
Getters can also calculate values on-the-fly:
class Expense {
String _description;
double _amount;
String _category;
DateTime _date;
Expense(this._description, this._amount, this._category, this._date) {
if (_amount < 0) throw Exception('Amount cannot be negative!');
}
// Regular getters
String get description => _description;
double get amount => _amount;
String get category => _category;
DateTime get date => _date;
// Computed getters - calculate on the fly
bool get isMajorExpense => _amount > 100;
bool get isThisMonth {
DateTime now = DateTime.now();
return _date.year == now.year && _date.month == now.month;
}
String get formattedAmount => '\$${_amount.toStringAsFixed(2)}';
String get formattedDate => _date.toString().split(' ')[0];
int get daysAgo => DateTime.now().difference(_date).inDays;
String get summary => '$_description: $formattedAmount [$_category]';
// Emoji based on amount
String get categoryEmoji {
switch (_category) {
case 'Food': return 'π';
case 'Transport': return 'π';
case 'Bills': return 'π‘';
case 'Entertainment': return 'π¬';
case 'Health': return 'π';
case 'Shopping': return 'ποΈ';
default: return 'π';
}
}
}
void main() {
var coffee = Expense('Coffee', 4.50, 'Food', DateTime.now());
var laptop = Expense('Laptop', 899.99, 'Shopping', DateTime(2025, 10, 5));
print(coffee.summary);
print('Is major? ${coffee.isMajorExpense}');
print('Days ago: ${coffee.daysAgo}');
print('${coffee.categoryEmoji} ${coffee.category}');
print('\n${laptop.summary}');
print('Is major? ${laptop.isMajorExpense}');
print('${laptop.categoryEmoji} ${laptop.category}');
}
Output:
Coffee: $4.50 [Food]
Is major? false
Days ago: 0
π Food
Laptop: $899.99 [Shopping]
Is major? true
ποΈ Shopping
Part 7: Complete Protected Expense Class
Here's a production-ready Expense class with full encapsulation:
class Expense {
// Private properties
String _description;
double _amount;
String _category;
DateTime _date;
String? _notes;
bool _isPaid;
// Valid categories list
static const List<String> validCategories = [
'Food',
'Transport',
'Bills',
'Entertainment',
'Health',
'Shopping',
'Other',
];
// Constructor with validation
Expense({
required String description,
required double amount,
required String category,
DateTime? date,
String? notes,
bool isPaid = false,
}) : _description = description,
_amount = amount,
_category = category,
_date = date ?? DateTime.now(),
_notes = notes,
_isPaid = isPaid {
// Validate amount
if (_amount < 0) {
throw Exception('Amount cannot be negative');
}
// Validate description
if (_description.trim().isEmpty) {
throw Exception('Description cannot be empty');
}
// Validate category
if (!validCategories.contains(_category)) {
print('β οΈ Invalid category "$_category". Changed to "Other".');
_category = 'Other';
}
}
// Named constructor
Expense.quick(String description, double amount, String category)
: this(description: description, amount: amount, category: category);
// Getters (read-only)
String get description => _description;
double get amount => _amount;
String get category => _category;
DateTime get date => _date;
String? get notes => _notes;
bool get isPaid => _isPaid;
// Computed getters
bool get isMajorExpense => _amount > 100;
String get formattedAmount => '\$${_amount.toStringAsFixed(2)}';
String get formattedDate => _date.toString().split(' ')[0];
String get categoryEmoji {
switch (_category) {
case 'Food': return 'π';
case 'Transport': return 'π';
case 'Bills': return 'π‘';
case 'Entertainment': return 'π¬';
case 'Health': return 'π';
case 'Shopping': return 'ποΈ';
default: return 'π';
}
}
// Setters with validation
set amount(double value) {
if (value < 0) {
throw Exception('Amount cannot be negative');
}
_amount = value;
}
set description(String value) {
if (value.trim().isEmpty) {
throw Exception('Description cannot be empty');
}
_description = value.trim();
}
set category(String value) {
if (!validCategories.contains(value)) {
throw Exception('Invalid category: $value');
}
_category = value;
}
set notes(String? value) {
_notes = value;
}
set isPaid(bool value) {
_isPaid = value;
}
// Methods
void markAsPaid() {
_isPaid = true;
print('β
Marked as paid: $_description');
}
void markAsUnpaid() {
_isPaid = false;
print('β Marked as unpaid: $_description');
}
void addNote(String note) {
if (_notes == null || _notes!.isEmpty) {
_notes = note;
} else {
_notes = '$_notes; $note';
}
}
void printDetails() {
String paidStatus = _isPaid ? 'β
Paid' : 'β Unpaid';
String noteText = _notes != null ? '\n π $_notes' : '';
print('βββββββββββββββββββββββββββββ');
print('$categoryEmoji $_description');
print('π° $formattedAmount');
print('π $_category');
print('π
$formattedDate');
print('$paidStatus$noteText');
}
}
void main() {
print('π¦ EXPENSE MANAGER WITH VALIDATION\n');
// Create valid expenses
var coffee = Expense.quick('Morning coffee', 4.50, 'Food');
var rent = Expense(
description: 'Monthly rent',
amount: 1200.0,
category: 'Bills',
isPaid: true,
);
coffee.printDetails();
print('');
rent.printDetails();
print('\nπ TESTING VALIDATION:\n');
// Test setters
print('Changing coffee amount to 5.00...');
coffee.amount = 5.00;
print('β
Success! New amount: ${coffee.formattedAmount}');
try {
print('\nTrying to set negative amount...');
coffee.amount = -10;
} catch (e) {
print('β Caught error: $e');
}
try {
print('\nTrying to set empty description...');
coffee.description = '';
} catch (e) {
print('β Caught error: $e');
}
try {
print('\nTrying invalid category...');
coffee.category = 'InvalidCategory';
} catch (e) {
print('β Caught error: $e');
}
print('\nβ
Adding valid note...');
coffee.addNote('Bought at Starbucks');
coffee.printDetails();
}
Part 8: Why Encapsulation Matters
Real-World Benefits:
1. Data Integrity π‘οΈ
// Without encapsulation - bugs everywhere!
expense.amount = -999; // Oops!
expense.category = 'asfgh'; // Typo!
// With encapsulation - protected!
expense.amount = -999; // β Throws error
expense.category = 'asfgh'; // β Throws error
2. Easier Debugging π
// Without encapsulation - amount changes everywhere
// Where did the bug come from? No idea!
// With encapsulation - only through setters
// Search for "set amount" to find where it changes
3. Future Changes π
// Need to change how amount is stored?
// With encapsulation, change internal code only
// External code using getters/setters keeps working!
class Expense {
int _amountInCents; // Changed storage format
double get amount => _amountInCents / 100; // Convert on read
set amount(double value) => _amountInCents = (value * 100).round();
}
4. Business Rules π
// Enforce business logic in one place
set amount(double value) {
if (value < 0) throw Exception('Negative amount');
if (value > 10000) {
print('β οΈ Large amount! Requires approval.');
}
_amount = value;
}
π― Practice Exercises
Exercise 1: Protected Category Class (Easy)
Create a Category
class with:
- Private properties:
_name
,_icon
,_budget
- Getters for all properties
- Setter for
budget
that only allows positive values - Computed getter
isOverBudget(double spent)
that returns true if spent > budget
Solution:
class Category {
String _name;
String _icon;
double _budget;
Category(this._name, this._icon, this._budget) {
if (_budget < 0) {
throw Exception('Budget cannot be negative');
}
}
// Getters
String get name => _name;
String get icon => _icon;
double get budget => _budget;
// Setter with validation
set budget(double value) {
if (value < 0) {
throw Exception('Budget cannot be negative');
}
_budget = value;
}
// Method
bool isOverBudget(double spent) {
return spent > _budget;
}
void printStatus(double spent) {
String status = isOverBudget(spent) ? 'β Over budget!' : 'β
Within budget';
print('$_icon $_name: \$${spent.toStringAsFixed(2)} / \$${_budget.toStringAsFixed(2)} - $status');
}
}
void main() {
var food = Category('Food', 'π', 500.0);
food.printStatus(350.0); // Within budget
food.printStatus(550.0); // Over budget
// Test validation
try {
food.budget = -100;
} catch (e) {
print('Error: $e');
}
}
Exercise 2: Smart User Class (Medium)
Create a User
class with:
- Private:
_name
,_email
,_age
- Validate email must contain '@'
- Validate age must be between 13 and 120
- Computed getter
isAdult
(age >= 18) - Computed getter
displayName
(name + age category)
Solution:
class User {
String _name;
String _email;
int _age;
User({
required String name,
required String email,
required int age,
}) : _name = name,
_email = email,
_age = age {
if (!_email.contains('@')) {
throw Exception('Invalid email format');
}
if (_age < 13 || _age > 120) {
throw Exception('Age must be between 13 and 120');
}
}
// Getters
String get name => _name;
String get email => _email;
int get age => _age;
// Computed getters
bool get isAdult => _age >= 18;
String get ageCategory {
if (_age < 18) return 'Minor';
if (_age < 65) return 'Adult';
return 'Senior';
}
String get displayName => '$_name ($ageCategory, $_age)';
// Setters with validation
set email(String value) {
if (!value.contains('@')) {
throw Exception('Invalid email format');
}
_email = value;
}
set age(int value) {
if (value < 13 || value > 120) {
throw Exception('Age must be between 13 and 120');
}
_age = value;
}
void printProfile() {
print('π€ $displayName');
print('π§ $_email');
print('Status: ${isAdult ? "Adult β
" : "Minor β"}');
}
}
void main() {
var user = User(name: 'John Doe', email: 'john@example.com', age: 25);
user.printProfile();
print('\n--- Testing validation ---');
try {
user.email = 'invalidemail';
} catch (e) {
print('β $e');
}
try {
user.age = 150;
} catch (e) {
print('β $e');
}
}
Exercise 3: Bank Account Class (Hard)
Create a BankAccount
class with:
- Private:
_accountHolder
,_balance
,_pin
- Methods:
deposit(amount)
,withdraw(amount, pin)
- Cannot withdraw without correct PIN
- Cannot withdraw more than balance
- Cannot deposit negative amounts
- Getter for balance (but not setter!)
- Method
changePin(oldPin, newPin)
Solution:
class BankAccount {
String _accountHolder;
double _balance;
String _pin;
BankAccount({
required String accountHolder,
required String pin,
double initialBalance = 0,
}) : _accountHolder = accountHolder,
_pin = pin,
_balance = initialBalance {
if (_pin.length != 4) {
throw Exception('PIN must be 4 digits');
}
if (_balance < 0) {
throw Exception('Initial balance cannot be negative');
}
}
// Getters
String get accountHolder => _accountHolder;
double get balance => _balance; // Read-only, no setter!
// Deposit method
void deposit(double amount) {
if (amount <= 0) {
print('β Deposit amount must be positive');
return;
}
_balance += amount;
print('β
Deposited \$${amount.toStringAsFixed(2)}. New balance: \$${_balance.toStringAsFixed(2)}');
}
// Withdraw method with PIN validation
bool withdraw(double amount, String pin) {
if (pin != _pin) {
print('β Incorrect PIN');
return false;
}
if (amount <= 0) {
print('β Withdrawal amount must be positive');
return false;
}
if (amount > _balance) {
print('β Insufficient funds. Balance: \$${_balance.toStringAsFixed(2)}');
return false;
}
_balance -= amount;
print('β
Withdrew \$${amount.toStringAsFixed(2)}. New balance: \$${_balance.toStringAsFixed(2)}');
return true;
}
// Change PIN
bool changePin(String oldPin, String newPin) {
if (oldPin != _pin) {
print('β Incorrect old PIN');
return false;
}
if (newPin.length != 4) {
print('β New PIN must be 4 digits');
return false;
}
_pin = newPin;
print('β
PIN changed successfully');
return true;
}
void printStatement() {
print('βββββββββββββββββββββββββββββ');
print('Account Holder: $_accountHolder');
print('Balance: \$${_balance.toStringAsFixed(2)}');
print('βββββββββββββββββββββββββββββ');
}
}
void main() {
var account = BankAccount(
accountHolder: 'John Doe',
pin: '1234',
initialBalance: 1000.0,
);
account.printStatement();
print('\n--- Transactions ---');
account.deposit(500.0);
account.withdraw(200.0, '1234');
account.withdraw(200.0, '0000'); // Wrong PIN
account.withdraw(2000.0, '1234'); // Insufficient funds
print('\n--- Change PIN ---');
account.changePin('0000', '5678'); // Wrong old PIN
account.changePin('1234', '5678'); // Success
print('');
account.printStatement();
}
Common Mistakes & How to Fix Them
Mistake 1: Forgetting the Underscore
// β Wrong - not private
String description;
// β
Correct - private
String _description;
Mistake 2: Making Getter and Setter Names Different
// β Wrong - names don't match
double get amount => _amount;
set setAmount(double value) { _amount = value; }
// β
Correct - same name
double get amount => _amount;
set amount(double value) { _amount = value; }
Mistake 3: Forgetting Validation
// β Wrong - no validation
set amount(double value) {
_amount = value;
}
// β
Correct - validate first
set amount(double value) {
if (value < 0) {
throw Exception('Amount cannot be negative');
}
_amount = value;
}
Mistake 4: Direct Access to Private Properties Outside Class
// β Wrong - trying to access private property
print(expense._amount);
// β
Correct - use getter
print(expense.amount);
Key Concepts Review
β
Private properties use underscore _
prefix
β
Getters provide read access with get
keyword
β
Setters provide write access with set
keyword and validation
β
Validation prevents invalid data from entering your objects
β
Encapsulation protects data and enforces business rules
β
Computed getters calculate values on-the-fly
β
Data integrity is maintained through controlled access
Self-Check Questions
1. What does encapsulation mean?
Answer:
Encapsulation means hiding internal data and providing controlled access through getters and setters. It protects data from invalid changes and enforces business rules.
2. Why use getters and setters instead of public properties?
Answer:
- Validate data before accepting changes
- Prevent invalid states (like negative amounts)
- Can change internal implementation without breaking external code
- Enforce business rules in one place
- Better debugging (know where data changes)
3. What's the difference between a regular getter and a computed getter?
Answer:
- Regular getter: Returns a stored private property (
get amount => _amount
) - Computed getter: Calculates a value on-the-fly (
get isAdult => _age >= 18
)
What's Next?
In Lesson 5: Adding Useful Features, we'll learn:
- More advanced methods for our Expense class
- Working with lists of expenses
- Filtering and sorting
- Calculating totals and statistics
- Building helper methods
Example preview:
class ExpenseManager {
List<Expense> _expenses = [];
void addExpense(Expense expense) { ... }
double getTotalSpending() { ... }
List<Expense> getByCategory(String category) { ... }
Expense? getLargestExpense() { ... }
}
See you in Lesson 5! π
Additional Resources
- Practice creating other protected classes:
CreditCard
,Budget
,UserProfile
- Think about what data should be protected in a real app
- Consider what validation rules make sense for your expense manager
Remember: Encapsulation isn't about making everything private - it's about protecting data that needs protection and providing safe ways to interact with it! π
Top comments (0)