DEV Community

Putra Prima A
Putra Prima A

Posted on

Dart Object Oriented For Beginner : Expense Manager Case Study Part 5

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)}');
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()}');
}
Enter fullscreen mode Exit fullscreen mode

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)}%');
}
Enter fullscreen mode Exit fullscreen mode

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%
Enter fullscreen mode Exit fullscreen mode

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);
  });
}
Enter fullscreen mode Exit fullscreen mode

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)}');
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

🎯 Practice Exercises

Exercise 1: Time-Based Methods (Easy)

Add these methods to the Expense class:

  1. getWeekNumber() - returns the week number of the year
  2. getQuarter() - returns which quarter (1-4) the expense is in
  3. 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()}');
}
Enter fullscreen mode Exit fullscreen mode

Exercise 2: Statistical Methods (Medium)

Add these methods:

  1. getAmountRounded() - returns amount rounded to nearest dollar
  2. getDailyAverage(int days) - returns average per day over specified period
  3. 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)}');
}
Enter fullscreen mode Exit fullscreen mode

Exercise 3: Smart Analysis Methods (Hard)

Create these advanced methods:

  1. getRiskLevel() - returns 'Low', 'Medium', or 'High' based on amount
  2. getSavingSuggestion() - returns a tip for reducing this expense
  3. 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)}');
}
Enter fullscreen mode Exit fullscreen mode

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}';
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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}) { ... }
Enter fullscreen mode Exit fullscreen mode

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() { ... }
Enter fullscreen mode Exit fullscreen mode

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() { ... }
}
Enter fullscreen mode Exit fullscreen mode

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)