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)