Lesson 5: Adding Useful Features (Methods)
Duration: 30 minutes
App Feature: ✨ Making Expenses Smarter
What You'll Build: Add helpful methods to analyze expenses
Prerequisites: Complete Lessons 1-4
What We'll Learn Today
By the end of this lesson, you'll be able to:
- ✅ Add methods that perform calculations
- ✅ Create methods that return different types
- ✅ Use optional and named parameters in methods
- ✅ Build methods that check conditions
- ✅ Format data for display
- ✅ Make your Expense class more powerful
Part 1: Methods That Return Boolean Values
Let's add methods to check various conditions about an expense:
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;
// Boolean methods - return true or false
bool isMajorExpense() {
return _amount > 100;
}
bool isThisMonth() {
DateTime now = DateTime.now();
return _date.year == now.year && _date.month == now.month;
}
bool isThisWeek() {
DateTime now = DateTime.now();
DateTime startOfWeek = now.subtract(Duration(days: now.weekday - 1));
DateTime endOfWeek = startOfWeek.add(Duration(days: 6));
return _date.isAfter(startOfWeek) && _date.isBefore(endOfWeek);
}
bool isToday() {
DateTime now = DateTime.now();
return _date.year == now.year &&
_date.month == now.month &&
_date.day == now.day;
}
bool isCategory(String category) {
return _category == category;
}
bool isOlderThan(int days) {
DateTime now = DateTime.now();
int difference = now.difference(_date).inDays;
return difference > days;
}
}
void main() {
var coffee = Expense('Coffee', 4.50, 'Food', DateTime.now());
var laptop = Expense('Laptop', 899.99, 'Electronics', DateTime(2025, 10, 5));
print('Coffee is major expense? ${coffee.isMajorExpense()}');
print('Coffee is this month? ${coffee.isThisMonth()}');
print('Coffee is today? ${coffee.isToday()}');
print('Coffee is Food? ${coffee.isCategory("Food")}');
print('\nLaptop is major expense? ${laptop.isMajorExpense()}');
print('Laptop is older than 3 days? ${laptop.isOlderThan(3)}');
}
Output:
Coffee is major expense? false
Coffee is this month? true
Coffee is today? true
Coffee is Food? true
Laptop is major expense? true
Laptop is older than 3 days? true
Part 2: Methods That Return Formatted Strings
Add methods that return nicely formatted data:
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;
// Formatting methods
String getFormattedAmount() {
return '\$${_amount.toStringAsFixed(2)}';
}
String getFormattedDate() {
List<String> months = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
];
return '${months[_date.month - 1]} ${_date.day}, ${_date.year}';
}
String getShortDate() {
return '${_date.month}/${_date.day}/${_date.year}';
}
String getSummary() {
String emoji = _amount > 100 ? '🔴' : '🟢';
return '$emoji $_description: ${getFormattedAmount()} [$_category]';
}
String getDetailedSummary() {
return '''
$_description
Amount: ${getFormattedAmount()}
Category: $_category
Date: ${getFormattedDate()}
''';
}
String getCategoryEmoji() {
switch (_category) {
case 'Food': return '🍔';
case 'Transport': return '🚗';
case 'Bills': return '💡';
case 'Entertainment': return '🎬';
case 'Health': return '💊';
case 'Shopping': return '🛍️';
default: return '📝';
}
}
String getFullDisplay() {
return '${getCategoryEmoji()} $_description - ${getFormattedAmount()}';
}
}
void main() {
var coffee = Expense('Coffee', 4.50, 'Food', DateTime.now());
var laptop = Expense('Laptop', 899.99, 'Electronics', DateTime(2025, 10, 5));
print(coffee.getSummary());
print(coffee.getFormattedDate());
print(coffee.getFullDisplay());
print('\n${laptop.getDetailedSummary()}');
}
Part 3: Methods with Calculations
Add methods that perform calculations:
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;
// Calculation methods
int getDaysAgo() {
DateTime now = DateTime.now();
return now.difference(_date).inDays;
}
double getTax({double taxRate = 0.10}) {
return _amount * taxRate;
}
double getTotalWithTax({double taxRate = 0.10}) {
return _amount + getTax(taxRate: taxRate);
}
double getPercentageOf(double total) {
if (total == 0) return 0;
return (_amount / total) * 100;
}
double getMonthlyAverage() {
// Assumes this is an annual expense
return _amount / 12;
}
// Split expense among people
double splitAmount(int numberOfPeople) {
if (numberOfPeople <= 0) return _amount;
return _amount / numberOfPeople;
}
// Calculate what this expense would be with a discount
double applyDiscount(double discountPercent) {
if (discountPercent < 0 || discountPercent > 100) {
return _amount;
}
return _amount * (1 - discountPercent / 100);
}
}
void main() {
var laptop = Expense('Laptop', 1000.0, 'Electronics', DateTime(2025, 10, 5));
print('Amount: \$${laptop.amount}');
print('Days ago: ${laptop.getDaysAgo()}');
print('Tax (10%): \$${laptop.getTax().toStringAsFixed(2)}');
print('Total with tax: \$${laptop.getTotalWithTax().toStringAsFixed(2)}');
print('Split 4 ways: \$${laptop.splitAmount(4).toStringAsFixed(2)}');
print('With 20% discount: \$${laptop.applyDiscount(20).toStringAsFixed(2)}');
double totalSpending = 5000.0;
print('Percentage of total: ${laptop.getPercentageOf(totalSpending).toStringAsFixed(1)}%');
}
Output:
Amount: $1000.0
Days ago: 4
Tax (10%): $100.00
Total with tax: $1100.00
Split 4 ways: $250.00
With 20% discount: $800.00
Percentage of total: 20.0%
Part 4: Methods with Optional Parameters
Create flexible methods using optional parameters:
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;
// Method with optional positional parameter
void printDetails([bool showEmoji = false]) {
String emoji = showEmoji ? getCategoryEmoji() : '';
print('$emoji $_description: \$${_amount.toStringAsFixed(2)} [$_category]');
}
// Method with multiple optional parameters
String format({
bool showDate = false,
bool showCategory = true,
bool showEmoji = false,
String currency = '\$',
}) {
String result = _description;
if (showEmoji) {
result = '${getCategoryEmoji()} $result';
}
result += ': $currency${_amount.toStringAsFixed(2)}';
if (showCategory) {
result += ' [$_category]';
}
if (showDate) {
result += ' (${_date.toString().split(' ')[0]})';
}
return result;
}
String getCategoryEmoji() {
switch (_category) {
case 'Food': return '🍔';
case 'Transport': return '🚗';
case 'Bills': return '💡';
default: return '📝';
}
}
// Method with optional callback
void process({void Function(String)? onComplete}) {
// Do some processing
print('Processing expense: $_description');
// Call callback if provided
if (onComplete != null) {
onComplete('✅ Expense processed successfully');
}
}
}
void main() {
var coffee = Expense('Coffee', 4.50, 'Food', DateTime.now());
// Different ways to call printDetails
coffee.printDetails();
coffee.printDetails(true);
// Different ways to format
print('\n${coffee.format()}');
print(coffee.format(showEmoji: true));
print(coffee.format(showDate: true, showEmoji: true));
print(coffee.format(currency: '€', showCategory: false));
// Using callback
print('');
coffee.process(onComplete: (message) {
print(message);
});
}
Part 5: Comparison Methods
Add methods to compare expenses:
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;
// Comparison methods
bool isMoreExpensiveThan(Expense other) {
return _amount > other._amount;
}
bool isSameCategory(Expense other) {
return _category == other._category;
}
bool isSameDay(Expense other) {
return _date.year == other._date.year &&
_date.month == other._date.month &&
_date.day == other._date.day;
}
bool isNewerThan(Expense other) {
return _date.isAfter(other._date);
}
int compareByAmount(Expense other) {
if (_amount < other._amount) return -1;
if (_amount > other._amount) return 1;
return 0;
}
int compareByDate(Expense other) {
return _date.compareTo(other._date);
}
double getDifferenceFrom(Expense other) {
return (_amount - other._amount).abs();
}
}
void main() {
var coffee = Expense('Coffee', 4.50, 'Food', DateTime.now());
var lunch = Expense('Lunch', 12.75, 'Food', DateTime.now());
var laptop = Expense('Laptop', 899.99, 'Electronics', DateTime(2025, 10, 5));
print('Lunch is more expensive than coffee? ${lunch.isMoreExpensiveThan(coffee)}');
print('Lunch same category as coffee? ${lunch.isSameCategory(coffee)}');
print('Lunch same day as coffee? ${lunch.isSameDay(coffee)}');
print('Coffee newer than laptop? ${coffee.isNewerThan(laptop)}');
print('Difference between lunch and coffee: \$${lunch.getDifferenceFrom(coffee).toStringAsFixed(2)}');
}
Part 6: Complete Enhanced Expense Class
Here's a comprehensive Expense class with all useful methods:
class Expense {
String _description;
double _amount;
String _category;
DateTime _date;
String? _notes;
bool _isPaid;
static const List<String> validCategories = [
'Food', 'Transport', 'Bills', 'Entertainment', 'Health', 'Shopping', 'Other'
];
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 {
if (_amount < 0) throw Exception('Amount cannot be negative');
if (_description.trim().isEmpty) throw Exception('Description cannot be empty');
if (!validCategories.contains(_category)) {
_category = 'Other';
}
}
// Getters
String get description => _description;
double get amount => _amount;
String get category => _category;
DateTime get date => _date;
String? get notes => _notes;
bool get isPaid => _isPaid;
// Setters
set amount(double value) {
if (value < 0) throw Exception('Amount cannot be negative');
_amount = value;
}
set isPaid(bool value) => _isPaid = value;
set notes(String? value) => _notes = value;
// Boolean check methods
bool isMajorExpense() => _amount > 100;
bool isThisMonth() {
DateTime now = DateTime.now();
return _date.year == now.year && _date.month == now.month;
}
bool isToday() {
DateTime now = DateTime.now();
return _date.year == now.year && _date.month == now.month && _date.day == now.day;
}
bool isCategory(String cat) => _category == cat;
bool isOlderThan(int days) => DateTime.now().difference(_date).inDays > days;
// Formatting methods
String getFormattedAmount() => '\$${_amount.toStringAsFixed(2)}';
String getFormattedDate() {
List<String> months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return '${months[_date.month - 1]} ${_date.day}, ${_date.year}';
}
String getCategoryEmoji() {
switch (_category) {
case 'Food': return '🍔';
case 'Transport': return '🚗';
case 'Bills': return '💡';
case 'Entertainment': return '🎬';
case 'Health': return '💊';
case 'Shopping': return '🛍️';
default: return '📝';
}
}
String getSummary() {
String emoji = isMajorExpense() ? '🔴' : '🟢';
return '$emoji $_description: ${getFormattedAmount()} [$_category]';
}
String getFullDisplay() {
String paid = _isPaid ? '✅' : '❌';
String noteText = _notes != null ? ' - $_notes' : '';
return '$paid ${getCategoryEmoji()} $_description: ${getFormattedAmount()}$noteText';
}
// Calculation methods
int getDaysAgo() => DateTime.now().difference(_date).inDays;
double getTax({double taxRate = 0.10}) => _amount * taxRate;
double getTotalWithTax({double taxRate = 0.10}) => _amount + getTax(taxRate: taxRate);
double getPercentageOf(double total) {
if (total == 0) return 0;
return (_amount / total) * 100;
}
double splitAmount(int numberOfPeople) {
if (numberOfPeople <= 0) return _amount;
return _amount / numberOfPeople;
}
// Comparison methods
bool isMoreExpensiveThan(Expense other) => _amount > other._amount;
bool isSameCategory(Expense other) => _category == other._category;
bool isNewerThan(Expense other) => _date.isAfter(other._date);
int compareByAmount(Expense other) {
if (_amount < other._amount) return -1;
if (_amount > other._amount) return 1;
return 0;
}
// Action methods
void markAsPaid() {
_isPaid = true;
}
void markAsUnpaid() {
_isPaid = false;
}
void addNote(String note) {
if (_notes == null || _notes!.isEmpty) {
_notes = note;
} else {
_notes = '$_notes; $note';
}
}
void printDetails() {
print('─────────────────────────────');
print('${getCategoryEmoji()} $_description');
print('💰 ${getFormattedAmount()}');
print('📁 $_category');
print('📅 ${getFormattedDate()}');
print('${_isPaid ? "✅" : "❌"} ${_isPaid ? "Paid" : "Unpaid"}');
if (_notes != null && _notes!.isNotEmpty) {
print('📝 $_notes');
}
print('🕒 ${getDaysAgo()} days ago');
}
}
void main() {
print('🏦 ENHANCED EXPENSE CLASS DEMO\n');
var expenses = [
Expense(description: 'Morning coffee', amount: 4.50, category: 'Food'),
Expense(description: 'Uber to work', amount: 12.00, category: 'Transport'),
Expense(description: 'Laptop', amount: 899.99, category: 'Shopping', isPaid: true),
Expense(description: 'Groceries', amount: 127.50, category: 'Food', notes: 'Weekly shopping'),
];
// Print all summaries
print('SUMMARY:');
for (var expense in expenses) {
print(expense.getSummary());
}
// Calculate totals
double total = 0;
for (var expense in expenses) {
total += expense.amount;
}
print('\n─────────────────────────────');
print('Total: \$${total.toStringAsFixed(2)}');
// Show percentages
print('\nPERCENTAGES:');
for (var expense in expenses) {
print('${expense.description}: ${expense.getPercentageOf(total).toStringAsFixed(1)}%');
}
// Show this month's expenses
print('\nTHIS MONTH:');
for (var expense in expenses) {
if (expense.isThisMonth()) {
print(expense.getFullDisplay());
}
}
// Show major expenses
print('\nMAJOR EXPENSES (>\$100):');
for (var expense in expenses) {
if (expense.isMajorExpense()) {
expense.printDetails();
}
}
}
🎯 Practice Exercises
Exercise 1: Time-Based Methods (Easy)
Add these methods to the Expense class:
-
getWeekNumber()
- returns the week number of the year -
getQuarter()
- returns which quarter (1-4) the expense is in -
isWeekend()
- returns true if expense was on Saturday or Sunday
Solution:
int getWeekNumber() {
int dayOfYear = int.parse(DateFormat("D").format(_date));
return ((dayOfYear - _date.weekday + 10) / 7).floor();
}
int getQuarter() {
return ((_date.month - 1) / 3).floor() + 1;
}
bool isWeekend() {
return _date.weekday == DateTime.saturday || _date.weekday == DateTime.sunday;
}
void main() {
var expense = Expense(
description: 'Weekend brunch',
amount: 45.00,
category: 'Food',
date: DateTime(2025, 10, 11), // Saturday
);
print('Quarter: ${expense.getQuarter()}');
print('Is weekend? ${expense.isWeekend()}');
}
Exercise 2: Statistical Methods (Medium)
Add these methods:
-
getAmountRounded()
- returns amount rounded to nearest dollar -
getDailyAverage(int days)
- returns average per day over specified period -
projectedYearly()
- if this was monthly, what would yearly total be?
Solution:
double getAmountRounded() {
return _amount.roundToDouble();
}
double getDailyAverage(int days) {
if (days <= 0) return 0;
return _amount / days;
}
double projectedYearly() {
return _amount * 12;
}
void main() {
var subscription = Expense(
description: 'Netflix',
amount: 15.99,
category: 'Entertainment',
);
print('Amount: ${subscription.getFormattedAmount()}');
print('Rounded: \$${subscription.getAmountRounded().toStringAsFixed(2)}');
print('Daily average (30 days): \$${subscription.getDailyAverage(30).toStringAsFixed(2)}');
print('Projected yearly: \$${subscription.projectedYearly().toStringAsFixed(2)}');
}
Exercise 3: Smart Analysis Methods (Hard)
Create these advanced methods:
-
getRiskLevel()
- returns 'Low', 'Medium', or 'High' based on amount -
getSavingSuggestion()
- returns a tip for reducing this expense -
compareToAverage(double avgExpense)
- returns how much above/below average
Solution:
String getRiskLevel() {
if (_amount < 50) return 'Low';
if (_amount < 200) return 'Medium';
return 'High';
}
String getSavingSuggestion() {
if (_category == 'Food' && _amount > 50) {
return 'Consider meal prepping to reduce food costs';
}
if (_category == 'Transport' && _amount > 100) {
return 'Look into public transportation or carpooling';
}
if (_category == 'Entertainment' && _amount > 50) {
return 'Try free entertainment options like parks or libraries';
}
if (_amount > 500) {
return 'Major purchase - ensure it aligns with your budget goals';
}
return 'Expense looks reasonable';
}
String compareToAverage(double avgExpense) {
double difference = _amount - avgExpense;
double percentDiff = (difference / avgExpense * 100).abs();
if (difference > 0) {
return '\$${difference.toStringAsFixed(2)} (${percentDiff.toStringAsFixed(1)}%) above average';
} else if (difference < 0) {
return '\$${difference.abs().toStringAsFixed(2)} (${percentDiff.toStringAsFixed(1)}%) below average';
} else {
return 'Exactly average';
}
}
void main() {
var lunch = Expense(description: 'Lunch', amount: 85.00, category: 'Food');
print('Risk level: ${lunch.getRiskLevel()}');
print('Suggestion: ${lunch.getSavingSuggestion()}');
print('vs Average: ${lunch.compareToAverage(50.0)}');
}
Common Mistakes & How to Fix Them
Mistake 1: Not Returning Values
// ❌ Wrong - method doesn't return anything
String getSummary() {
'$_description: \$${_amount}';
}
// ✅ Correct - use return
String getSummary() {
return '$_description: \$${_amount}';
}
Mistake 2: Forgetting to Handle Edge Cases
// ❌ Wrong - division by zero possible
double splitAmount(int numberOfPeople) {
return _amount / numberOfPeople;
}
// ✅ Correct - check for invalid input
double splitAmount(int numberOfPeople) {
if (numberOfPeople <= 0) return _amount;
return _amount / numberOfPeople;
}
Mistake 3: Not Using Named Parameters When Appropriate
// ❌ Wrong - hard to remember order
String format(bool showDate, bool showCategory, bool showEmoji) { ... }
// ✅ Correct - use named parameters
String format({bool showDate = false, bool showCategory = true, bool showEmoji = false}) { ... }
Mistake 4: Making Methods Too Complex
// ❌ Wrong - doing too much
String doEverything() {
// 100 lines of code doing multiple things
}
// ✅ Correct - separate concerns
String getSummary() { ... }
String getFormattedDate() { ... }
double calculateTotal() { ... }
Key Concepts Review
✅ Boolean methods return true/false for conditions
✅ Formatting methods return nicely formatted strings
✅ Calculation methods perform math and return results
✅ Optional parameters make methods flexible
✅ Comparison methods compare objects
✅ Action methods modify object state
✅ Keep methods focused - each does one thing well
Self-Check Questions
1. When should you use a method vs a computed getter?
Answer:
- Method: When there are parameters, complex logic, or side effects
- Getter: For simple property access or calculations without parameters
- Example:
getTax(taxRate: 0.15)
is a method,isMajorExpense
is a getter
2. What's the benefit of optional parameters?
Answer:
Optional parameters make methods flexible - users can provide values when needed but use defaults otherwise. This reduces the need for multiple method versions.
3. Why should methods be focused on one task?
Answer:
- Easier to understand and test
- More reusable in different contexts
- Simpler to debug when issues arise
- Follows "Single Responsibility Principle"
What's Next?
In Lesson 6: Managing Multiple Expenses, we'll learn:
- Creating an ExpenseManager class
- Working with lists of expenses
- Filtering and sorting
- Calculating totals across multiple expenses
- Building reports
Example preview:
class ExpenseManager {
List<Expense> _expenses = [];
void addExpense(Expense expense) { ... }
double getTotalSpending() { ... }
List<Expense> getByCategory(String category) { ... }
List<Expense> getThisMonth() { ... }
Expense? getLargestExpense() { ... }
}
See you in Lesson 6! 🚀
Additional Resources
- Try adding more methods to your Expense class
- Think about what calculations would be useful for your app
- Consider what formatting options users might want
- Practice writing focused, single-purpose methods
Remember: Methods should make your objects useful and intelligent. Each method should do one thing well! 💪
Top comments (0)