Lesson 7: Inheritance - Different Types of Expenses
Duration: 45 minutes
App Feature: π¨ Specialized Expense Types
What You'll Build: Create RecurringExpense and OneTimeExpense classes
Prerequisites: Complete Lessons 1-6
What We'll Learn Today
By the end of this lesson, you'll be able to:
- β Understand what inheritance means
- β Create child classes that extend parent classes
- β
Use the
extends
keyword - β
Call parent constructors with
super
- β
Override methods with
@override
- β Add specialized features to child classes
- β Understand the "IS-A" relationship
Part 1: The Problem - All Expenses Are the Same
Currently, all our expenses are treated identically:
void main() {
var coffee = Expense.quick('Coffee', 4.50, 'Food');
var netflix = Expense.quick('Netflix subscription', 15.99, 'Entertainment');
var birthday = Expense.quick('Birthday gift', 50.0, 'Gifts');
// But these are fundamentally different!
// Netflix repeats monthly
// Birthday gift is one-time
// Coffee is daily
// We need a way to distinguish them!
}
The Problem:
- Some expenses repeat (subscriptions, bills)
- Some are one-time (gifts, emergency)
- We want to calculate yearly costs for recurring expenses
- We want to tag one-time expenses by occasion
The Solution: Create specialized expense types using inheritance!
Part 2: Understanding Inheritance
Inheritance lets you create new classes based on existing ones.
Real-World Analogy:
Think about vehicles:
π Vehicle (Parent)
βββ Has wheels
βββ Has engine
βββ Can drive()
βββ π Car (Child)
β βββ Everything from Vehicle PLUS:
β βββ Has trunk
β βββ Seats 4-5 people
β
βββ π Truck (Child)
β βββ Everything from Vehicle PLUS:
β βββ Has cargo bed
β βββ Can tow()
β
βββ ποΈ Motorcycle (Child)
βββ Everything from Vehicle PLUS:
βββ Has 2 wheels (overrides parent)
βββ Can wheelie()
Key Points:
- Car IS-A Vehicle (IS-A relationship)
- Car inherits all properties and methods from Vehicle
- Car can add its own properties and methods
- Car can override inherited methods to change behavior
Part 3: Creating Your First Child Class
Let's create RecurringExpense
that inherits from Expense
:
// Parent class (what we already have)
class Expense {
String description;
double amount;
String category;
DateTime date;
Expense(this.description, this.amount, this.category, this.date);
void printDetails() {
print('$description: \$${amount.toStringAsFixed(2)} [$category]');
}
bool isMajorExpense() => amount > 100;
}
// Child class - extends Expense
class RecurringExpense extends Expense {
String frequency; // New property!
// Constructor
RecurringExpense(
String description,
double amount,
String category,
this.frequency,
) : super(description, amount, category, DateTime.now());
// New method specific to RecurringExpense
double yearlyTotal() {
if (frequency == 'monthly') return amount * 12;
if (frequency == 'weekly') return amount * 52;
if (frequency == 'daily') return amount * 365;
return amount; // yearly
}
}
void main() {
var netflix = RecurringExpense('Netflix', 15.99, 'Entertainment', 'monthly');
// Can use methods from parent class
netflix.printDetails();
print('Is major? ${netflix.isMajorExpense()}');
// Can use methods from child class
print('Yearly cost: \$${netflix.yearlyTotal().toStringAsFixed(2)}');
}
Output:
Netflix: $15.99 [Entertainment]
Is major? false
Yearly cost: $191.88
Part 4: Understanding the Syntax
Let's break down the child class syntax:
class RecurringExpense extends Expense {
-
extends
keyword means RecurringExpense inherits from Expense - RecurringExpense is the child (or subclass)
- Expense is the parent (or superclass)
String frequency; // New property
- Child classes can add their own properties
- This property doesn't exist in the parent
RecurringExpense(
String description,
double amount,
String category,
this.frequency,
) : super(description, amount, category, DateTime.now());
-
super(...)
calls the parent constructor - Must pass required values to parent
- Can also initialize child properties
double yearlyTotal() { ... }
- New method that only exists in RecurringExpense
- Parent class doesn't have this method
Part 5: Overriding Methods
Child classes can override parent methods to change their behavior:
class Expense {
String description;
double amount;
String category;
DateTime date;
Expense(this.description, this.amount, this.category, this.date);
void printDetails() {
print('$description: \$${amount.toStringAsFixed(2)} [$category]');
}
}
class RecurringExpense extends Expense {
String frequency;
RecurringExpense(
String description,
double amount,
String category,
this.frequency,
) : super(description, amount, category, DateTime.now());
// Override parent method
@override
void printDetails() {
print('π RECURRING ($frequency)');
super.printDetails(); // Call parent version
print('Yearly: \$${yearlyTotal().toStringAsFixed(2)}');
}
double yearlyTotal() {
if (frequency == 'monthly') return amount * 12;
if (frequency == 'weekly') return amount * 52;
return amount;
}
}
void main() {
var regular = Expense('Coffee', 4.50, 'Food', DateTime.now());
var netflix = RecurringExpense('Netflix', 15.99, 'Entertainment', 'monthly');
print('Regular expense:');
regular.printDetails();
print('\nRecurring expense:');
netflix.printDetails();
}
Output:
Regular expense:
Coffee: $4.50 [Food]
Recurring expense:
π RECURRING (monthly)
Netflix: $15.99 [Entertainment]
Yearly: $191.88
Understanding @override:
@override
void printDetails() {
-
@override
annotation tells Dart you're intentionally overriding - Not required, but highly recommended (helps catch typos)
-
super.printDetails()
calls the parent version
Part 6: Creating Multiple Child Classes
Let's create another child class for one-time expenses:
class Expense {
String description;
double amount;
String category;
DateTime date;
Expense(this.description, this.amount, this.category, this.date);
void printDetails() {
print('$description: \$${amount.toStringAsFixed(2)} [$category]');
}
String getSummary() => '$description: \$${amount.toStringAsFixed(2)}';
}
class RecurringExpense extends Expense {
String frequency;
RecurringExpense(
String description,
double amount,
String category,
this.frequency,
) : super(description, amount, category, DateTime.now());
@override
void printDetails() {
print('π RECURRING ($frequency)');
super.printDetails();
print(' Yearly total: \$${yearlyTotal().toStringAsFixed(2)}');
}
double yearlyTotal() {
if (frequency == 'monthly') return amount * 12;
if (frequency == 'weekly') return amount * 52;
if (frequency == 'daily') return amount * 365;
return amount;
}
}
class OneTimeExpense extends Expense {
String occasion; // 'birthday', 'emergency', 'vacation', etc.
OneTimeExpense(
String description,
double amount,
String category,
DateTime date,
this.occasion,
) : super(description, amount, category, date);
@override
void printDetails() {
print('π― ONE-TIME ($occasion)');
super.printDetails();
}
bool isSpecialOccasion() {
return occasion == 'birthday' ||
occasion == 'anniversary' ||
occasion == 'holiday';
}
}
void main() {
var expenses = [
Expense('Coffee', 4.50, 'Food', DateTime.now()),
RecurringExpense('Netflix', 15.99, 'Entertainment', 'monthly'),
OneTimeExpense('Birthday gift', 75.0, 'Gifts', DateTime.now(), 'birthday'),
RecurringExpense('Gym', 45.0, 'Health', 'monthly'),
OneTimeExpense('Car repair', 350.0, 'Transport', DateTime.now(), 'emergency'),
];
print('ALL EXPENSES:\n');
for (var expense in expenses) {
expense.printDetails();
print('');
}
}
Output:
ALL EXPENSES:
Coffee: $4.50 [Food]
π RECURRING (monthly)
Netflix: $15.99 [Entertainment]
Yearly total: $191.88
π― ONE-TIME (birthday)
Birthday gift: $75.00 [Gifts]
π RECURRING (monthly)
Gym: $45.00 [Health]
Yearly total: $540.00
π― ONE-TIME (emergency)
Car repair: $350.00 [Transport]
Part 7: Polymorphism - Treating Different Types Uniformly
Polymorphism means "many forms" - you can treat different types the same way:
void main() {
// All three types in one list!
List<Expense> expenses = [
Expense('Coffee', 4.50, 'Food', DateTime.now()),
RecurringExpense('Spotify', 9.99, 'Entertainment', 'monthly'),
OneTimeExpense('Concert', 85.0, 'Entertainment', DateTime.now(), 'fun'),
];
// Same method call, different behavior!
for (var expense in expenses) {
expense.printDetails(); // Each type prints differently
print('');
}
// Can check type and use specific features
for (var expense in expenses) {
if (expense is RecurringExpense) {
print('${expense.description} costs \$${expense.yearlyTotal().toStringAsFixed(2)}/year');
}
if (expense is OneTimeExpense) {
print('${expense.description} is for: ${expense.occasion}');
}
}
}
Key Points:
-
List<Expense>
can hold any type of Expense (parent or children) - Each object responds to
printDetails()
based on its actual type - Use
is
keyword to check the type - Can cast to child type to access child-specific features
Part 8: Complete Example with ExpenseManager
Let's update our ExpenseManager to work with different expense types:
class ExpenseManager {
List<Expense> _expenses = [];
void addExpense(Expense expense) {
_expenses.add(expense);
}
List<Expense> getAllExpenses() => List.from(_expenses);
// Get only recurring expenses
List<RecurringExpense> getRecurringExpenses() {
List<RecurringExpense> recurring = [];
for (var expense in _expenses) {
if (expense is RecurringExpense) {
recurring.add(expense);
}
}
return recurring;
}
// Get only one-time expenses
List<OneTimeExpense> getOneTimeExpenses() {
List<OneTimeExpense> oneTime = [];
for (var expense in _expenses) {
if (expense is OneTimeExpense) {
oneTime.add(expense);
}
}
return oneTime;
}
// Calculate yearly cost of all recurring expenses
double getYearlyRecurringCost() {
double total = 0;
for (var expense in getRecurringExpenses()) {
total += expense.yearlyTotal();
}
return total;
}
// Get count of each type
Map<String, int> getTypeCounts() {
int regular = 0;
int recurring = 0;
int oneTime = 0;
for (var expense in _expenses) {
if (expense is RecurringExpense) {
recurring++;
} else if (expense is OneTimeExpense) {
oneTime++;
} else {
regular++;
}
}
return {
'regular': regular,
'recurring': recurring,
'oneTime': oneTime,
};
}
void printSummary() {
print('\nβββββββββββββββββββββββββββββββββββ');
print('π° EXPENSE SUMMARY');
print('βββββββββββββββββββββββββββββββββββ');
var counts = getTypeCounts();
print('Total expenses: ${_expenses.length}');
print(' Regular: ${counts['regular']}');
print(' Recurring: ${counts['recurring']}');
print(' One-time: ${counts['oneTime']}');
double total = 0;
for (var expense in _expenses) {
total += expense.amount;
}
print('\nTotal spent: \$${total.toStringAsFixed(2)}');
double yearlyRecurring = getYearlyRecurringCost();
print('Yearly recurring: \$${yearlyRecurring.toStringAsFixed(2)}');
print('βββββββββββββββββββββββββββββββββββ\n');
}
void printAllExpenses() {
print('ALL EXPENSES:\n');
for (var expense in _expenses) {
expense.printDetails();
print('');
}
}
}
void main() {
var manager = ExpenseManager();
// Add different types of expenses
manager.addExpense(Expense('Coffee', 4.50, 'Food', DateTime.now()));
manager.addExpense(RecurringExpense('Netflix', 15.99, 'Entertainment', 'monthly'));
manager.addExpense(OneTimeExpense('Birthday gift', 75.0, 'Gifts', DateTime.now(), 'birthday'));
manager.addExpense(RecurringExpense('Gym', 45.0, 'Health', 'monthly'));
manager.addExpense(OneTimeExpense('Car repair', 350.0, 'Transport', DateTime.now(), 'emergency'));
manager.addExpense(Expense('Lunch', 12.50, 'Food', DateTime.now()));
manager.printSummary();
manager.printAllExpenses();
print('π RECURRING EXPENSES BREAKDOWN:');
for (var expense in manager.getRecurringExpenses()) {
print('${expense.description}: \$${expense.amount}/${expense.frequency}');
print(' β \$${expense.yearlyTotal().toStringAsFixed(2)}/year');
}
}
π― Practice Exercises
Exercise 1: Create BusinessExpense Class (Easy)
Create a BusinessExpense
class that extends Expense
with:
- Property:
client
(String) - Property:
isReimbursable
(bool) - Override
printDetails()
to show client and reimbursable status
Solution:
class BusinessExpense extends Expense {
String client;
bool isReimbursable;
BusinessExpense({
required String description,
required double amount,
required String category,
required this.client,
this.isReimbursable = true,
}) : super(description, amount, category, DateTime.now());
@override
void printDetails() {
print('πΌ BUSINESS EXPENSE');
super.printDetails();
print(' Client: $client');
print(' Reimbursable: ${isReimbursable ? "Yes β
" : "No β"}');
}
}
void main() {
var expense = BusinessExpense(
description: 'Client lunch',
amount: 85.0,
category: 'Meals',
client: 'Acme Corp',
isReimbursable: true,
);
expense.printDetails();
}
Exercise 2: Create TravelExpense Class (Medium)
Create a TravelExpense
class with:
- Properties:
destination
,tripDuration
(days) - Method:
getDailyCost()
- returns amount per day - Method:
isInternational()
- returns true if destination is outside country - Override
printDetails()
Solution:
class TravelExpense extends Expense {
String destination;
int tripDuration;
TravelExpense({
required String description,
required double amount,
required this.destination,
required this.tripDuration,
DateTime? date,
}) : super(description, amount, 'Travel', date ?? DateTime.now());
double getDailyCost() {
if (tripDuration == 0) return amount;
return amount / tripDuration;
}
bool isInternational() {
// Simple check - could be improved with a country list
return destination.contains('Japan') ||
destination.contains('USA') ||
destination.contains('Europe');
}
@override
void printDetails() {
print('βοΈ TRAVEL EXPENSE');
super.printDetails();
print(' Destination: $destination');
print(' Duration: $tripDuration days');
print(' Daily cost: \$${getDailyCost().toStringAsFixed(2)}');
print(' International: ${isInternational() ? "Yes π" : "No π "}');
}
}
void main() {
var trip = TravelExpense(
description: 'Tokyo vacation',
amount: 2500.0,
destination: 'Tokyo, Japan',
tripDuration: 7,
);
trip.printDetails();
}
Exercise 3: Subscription Management System (Hard)
Create a SubscriptionExpense
class that extends RecurringExpense
with:
- Properties:
provider
,plan
,startDate
,endDate
- Method:
isActive()
- checks if subscription is currently active - Method:
getRemainingMonths()
- months until expiration - Method:
getTotalCost()
- total cost from start to end date - Override
printDetails()
Solution:
class SubscriptionExpense extends RecurringExpense {
String provider;
String plan;
DateTime startDate;
DateTime? endDate;
SubscriptionExpense({
required String description,
required double amount,
required this.provider,
required this.plan,
required this.startDate,
this.endDate,
}) : super(description, amount, 'Subscriptions', 'monthly');
bool isActive() {
DateTime now = DateTime.now();
if (endDate == null) return true; // No end date = active
return now.isBefore(endDate!);
}
int getRemainingMonths() {
if (endDate == null) return -1; // Unlimited
DateTime now = DateTime.now();
if (now.isAfter(endDate!)) return 0;
int months = (endDate!.year - now.year) * 12 +
(endDate!.month - now.month);
return months;
}
double getTotalCost() {
if (endDate == null) {
// If no end date, calculate for 1 year
return yearlyTotal();
}
int months = (endDate!.year - startDate.year) * 12 +
(endDate!.month - startDate.month);
return amount * months;
}
@override
void printDetails() {
print('π± SUBSCRIPTION');
print('$description ($provider - $plan)');
print('Amount: \$${amount.toStringAsFixed(2)}/month');
print('Started: ${startDate.toString().split(' ')[0]}');
if (endDate != null) {
print('Expires: ${endDate.toString().split(' ')[0]}');
print('Remaining: ${getRemainingMonths()} months');
} else {
print('Expires: Never (ongoing)');
}
print('Status: ${isActive() ? "Active β
" : "Expired β"}');
print('Total cost: \$${getTotalCost().toStringAsFixed(2)}');
}
}
void main() {
var netflix = SubscriptionExpense(
description: 'Netflix Premium',
amount: 19.99,
provider: 'Netflix',
plan: 'Premium 4K',
startDate: DateTime(2024, 1, 1),
endDate: null, // Ongoing
);
var trial = SubscriptionExpense(
description: 'Adobe Creative Cloud',
amount: 54.99,
provider: 'Adobe',
plan: 'All Apps',
startDate: DateTime(2025, 9, 1),
endDate: DateTime(2025, 12, 31),
);
netflix.printDetails();
print('');
trial.printDetails();
}
Common Mistakes & How to Fix Them
Mistake 1: Forgetting to Call super()
// β Wrong - parent constructor not called
class RecurringExpense extends Expense {
String frequency;
RecurringExpense(String desc, double amt, String cat, this.frequency);
}
// β
Correct - call parent constructor
class RecurringExpense extends Expense {
String frequency;
RecurringExpense(String desc, double amt, String cat, this.frequency)
: super(desc, amt, cat, DateTime.now());
}
Mistake 2: Wrong Parameter Order in super()
// β Wrong - parameters in wrong order
RecurringExpense(String desc, double amt, String cat, this.frequency)
: super(cat, amt, desc, DateTime.now()); // Wrong order!
// β
Correct - match parent constructor signature
RecurringExpense(String desc, double amt, String cat, this.frequency)
: super(desc, amt, cat, DateTime.now());
Mistake 3: Not Using @override
// β Wrong - typo in method name, no error!
class RecurringExpense extends Expense {
void printDetail() { // Missing 's' - creates new method instead of overriding
print('Details...');
}
}
// β
Correct - @override catches typos
class RecurringExpense extends Expense {
@override
void printDetails() { // Compiler checks this matches parent
print('Details...');
}
}
Mistake 4: Trying to Access Private Parent Properties
class Expense {
String _description; // Private
// ...
}
class RecurringExpense extends Expense {
@override
void printDetails() {
print(_description); // β Error! Can't access private property
}
}
// β
Correct - use getter or make property protected
class Expense {
String description; // Public or protected
// ...
}
class RecurringExpense extends Expense {
@override
void printDetails() {
print(description); // β
Works!
}
}
Key Concepts Review
β
Inheritance lets you create specialized classes based on existing ones
β
extends keyword creates child class from parent
β
super() calls the parent constructor
β
@override marks methods that replace parent methods
β
IS-A relationship - child IS-A type of parent
β
Polymorphism - treat different types uniformly
β
Child classes inherit all parent properties and methods
β
Child classes can add new properties and methods
β
Use is
keyword to check object type
Self-Check Questions
1. What's the difference between inheritance and composition?
Answer:
- Inheritance (IS-A): RecurringExpense IS-A Expense - creates specialized versions
- Composition (HAS-A): ExpenseManager HAS-A list of Expenses - contains other objects
2. When should you use inheritance?
Answer:
Use inheritance when:
- You have a clear IS-A relationship
- Child needs most/all of parent's features
- You want to treat different types uniformly (polymorphism)
- You're specializing behavior, not completely changing it
3. What does @override do?
Answer:
@override tells the compiler you're intentionally replacing a parent method. It helps catch typos - if the method doesn't exist in parent, you'll get an error.
What's Next?
In Lesson 8: Polymorphism Deep Dive, we'll learn:
- Working with mixed lists of different types
- Type checking and casting
- Abstract classes (contracts that child classes must follow)
- Interfaces with
implements
- When to use inheritance vs interfaces
Example preview:
abstract class Payable {
void processPayment();
}
class RecurringExpense extends Expense implements Payable {
@override
void processPayment() {
// Auto-pay logic
}
}
See you in Lesson 8! π
Additional Resources
- Try creating more specialized expense types (GiftExpense, EmergencyExpense, etc.)
- Think about what makes each type unique
- Practice using polymorphism with mixed lists
- Consider when inheritance makes sense vs when it doesn't
Remember: Use inheritance to model IS-A relationships and create specialized versions of existing classes. Don't force it where it doesn't fit naturally! π³
Top comments (0)