DEV Community

Putra Prima A
Putra Prima A

Posted on

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

Lesson 3: Different Ways to Create Expenses

Duration: 25 minutes

App Feature: πŸ”§ Making Expense Creation Easier

What You'll Build: Add multiple ways to create expenses with constructors

Prerequisites: Complete Lesson 1 & 2


What We'll Learn Today

By the end of this lesson, you'll be able to:

  • βœ… Create named constructors for easier object creation
  • βœ… Use optional parameters
  • βœ… Set default values
  • βœ… Build multiple constructor variations
  • βœ… Make your code more flexible and user-friendly

Review: What We Know About Constructors

In Lesson 2, we learned the basic constructor:

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

The Problem: Every time we create an expense, we need to provide ALL four values. But what if:

  • Most expenses are from today? (we always type DateTime.now())
  • We want quick shortcuts for common scenarios?
  • Some values have sensible defaults?

The Solution: Named constructors and optional parameters!


Part 1: Named Constructors

A named constructor is an alternative way to create objects. You can have multiple constructors in one class, each with a different name.

Syntax:

ClassName.constructorName(parameters)
Enter fullscreen mode Exit fullscreen mode

Example: Quick Expense Constructor

class Expense {
  String description;
  double amount;
  String category;
  DateTime date;

  // Regular constructor
  Expense(this.description, this.amount, this.category, this.date);

  // Named constructor - assumes today's date
  Expense.quick(this.description, this.amount, this.category) 
    : date = DateTime.now();
}

void main() {
  // Using regular constructor
  var laptop = Expense('Laptop', 899.99, 'Electronics', DateTime(2025, 10, 5));

  // Using named constructor - much shorter!
  var coffee = Expense.quick('Coffee', 4.50, 'Food');
  var lunch = Expense.quick('Lunch', 12.75, 'Food');

  print('${coffee.description}: \$${coffee.amount} on ${coffee.date}');
}
Enter fullscreen mode Exit fullscreen mode

Understanding the Syntax:

Expense.quick(this.description, this.amount, this.category) 
  : date = DateTime.now();
Enter fullscreen mode Exit fullscreen mode
  • Expense.quick = class name + dot + constructor name
  • Parameters in () work like normal constructor
  • : date = DateTime.now() is the initializer list
  • Sets date property before the object is fully created

Part 2: Building a Better Expense Class

Let's add several named constructors for common scenarios:

class Expense {
  String description;
  double amount;
  String category;
  DateTime date;

  // 1. Regular constructor - full control
  Expense(this.description, this.amount, this.category, this.date);

  // 2. Quick constructor - assumes today
  Expense.quick(this.description, this.amount, this.category) 
    : date = DateTime.now();

  // 3. Monthly bill constructor - assumes "Bills" category
  Expense.monthly(this.description, this.amount) 
    : category = 'Bills',
      date = DateTime.now();

  // 4. Yesterday constructor - for when you forget to log
  Expense.yesterday(this.description, this.amount, this.category) 
    : date = DateTime.now().subtract(Duration(days: 1));

  void printDetails() {
    print('$description: \$${amount.toStringAsFixed(2)} [$category] on ${date.toString().split(' ')[0]}');
  }
}

void main() {
  print('MY EXPENSES:\n');

  // Using different constructors
  var rent = Expense.monthly('Rent', 1200.0);
  var coffee = Expense.quick('Coffee', 4.50, 'Food');
  var dinner = Expense.yesterday('Dinner', 35.00, 'Food');
  var vacation = Expense('Flight', 450.0, 'Travel', DateTime(2025, 12, 20));

  rent.printDetails();
  coffee.printDetails();
  dinner.printDetails();
  vacation.printDetails();
}
Enter fullscreen mode Exit fullscreen mode

Output:

MY EXPENSES:

Rent: $1200.00 [Bills] on 2025-10-09
Coffee: $4.50 [Food] on 2025-10-09
Dinner: $35.00 [Food] on 2025-10-08
Flight: $450.00 [Travel] on 2025-12-20
Enter fullscreen mode Exit fullscreen mode

Part 3: Multiple Initializations in Initializer List

You can set multiple properties in the initializer list:

class Expense {
  String description;
  double amount;
  String category;
  DateTime date;

  Expense(this.description, this.amount, this.category, this.date);

  // Initialize BOTH category and date
  Expense.subscription(this.description, this.amount) 
    : category = 'Entertainment',
      date = DateTime.now();

  // Initialize with calculations
  Expense.weekly(this.description, double weeklyAmount) 
    : amount = weeklyAmount * 52,  // Convert to yearly
      category = 'Recurring',
      date = DateTime.now();
}

void main() {
  var netflix = Expense.subscription('Netflix', 15.99);
  var coffee = Expense.weekly('Coffee habit', 25.00);  // $25/week

  netflix.printDetails();
  print('Coffee yearly: \$${coffee.amount.toStringAsFixed(2)}');
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • Separate multiple initializations with commas
  • Can include calculations or method calls
  • Runs BEFORE the constructor body (if you have one)

Part 4: Optional Parameters

Sometimes you want parameters that are... optional! Dart has two ways to do this:

A. Optional Positional Parameters (with [])

class Expense {
  String description;
  double amount;
  String category;
  DateTime date;
  String? notes;  // Nullable - can be null

  // Optional positional parameters in square brackets
  Expense(this.description, this.amount, this.category, [DateTime? date, this.notes]) 
    : date = date ?? DateTime.now();  // Use provided date OR current date
}

void main() {
  // With date and notes
  var expense1 = Expense('Laptop', 899.99, 'Electronics', DateTime(2025, 10, 5), 'Work laptop');

  // Without date (uses today)
  var expense2 = Expense('Coffee', 4.50, 'Food');

  // With date but no notes
  var expense3 = Expense('Dinner', 45.00, 'Food', DateTime(2025, 10, 8));
}
Enter fullscreen mode Exit fullscreen mode

Understanding ?? operator:

  • date ?? DateTime.now() means: "use date if provided, otherwise use DateTime.now()"
  • This is called the "null-aware" operator

B. Named Optional Parameters (with {})

Named parameters are more readable and can be provided in any order:

class Expense {
  String description;
  double amount;
  String category;
  DateTime date;
  String? notes;
  bool isPaid;

  // Named optional parameters in curly braces
  Expense({
    required this.description,
    required this.amount,
    required this.category,
    DateTime? date,
    this.notes,
    this.isPaid = false,  // Default value
  }) : date = date ?? DateTime.now();
}

void main() {
  // Can provide parameters in any order
  var expense1 = Expense(
    amount: 45.00,
    description: 'Dinner',
    category: 'Food',
    notes: 'Birthday dinner',
    isPaid: true,
  );

  // Can skip optional parameters
  var expense2 = Expense(
    description: 'Coffee',
    amount: 4.50,
    category: 'Food',
  );

  // Can provide date if needed
  var expense3 = Expense(
    description: 'Rent',
    amount: 1200.0,
    category: 'Bills',
    date: DateTime(2025, 10, 1),
  );
}
Enter fullscreen mode Exit fullscreen mode

Key Differences:

Feature Positional [] Named {}
Order matters βœ… Yes ❌ No
More readable ❌ Less βœ… More
Can be required ❌ No βœ… Yes (with required)
Default values βœ… Yes βœ… Yes

Part 5: Default Values

You can provide default values for optional parameters:

class Expense {
  String description;
  double amount;
  String category;
  DateTime date;
  String currency;
  bool isPaid;

  Expense({
    required this.description,
    required this.amount,
    this.category = 'Other',        // Default: 'Other'
    DateTime? date,
    this.currency = 'USD',           // Default: 'USD'
    this.isPaid = false,             // Default: false
  }) : date = date ?? DateTime.now();

  void printDetails() {
    String paidStatus = isPaid ? 'βœ… Paid' : '❌ Unpaid';
    print('$description: $currency \$${amount.toStringAsFixed(2)} [$category] - $paidStatus');
  }
}

void main() {
  // Using all defaults
  var expense1 = Expense(
    description: 'Mystery purchase',
    amount: 25.00,
  );

  // Override some defaults
  var expense2 = Expense(
    description: 'Rent',
    amount: 1200.0,
    category: 'Bills',
    isPaid: true,
  );

  // Override currency
  var expense3 = Expense(
    description: 'Tokyo hotel',
    amount: 15000,
    currency: 'JPY',
    category: 'Travel',
  );

  expense1.printDetails();
  expense2.printDetails();
  expense3.printDetails();
}
Enter fullscreen mode Exit fullscreen mode

Output:

Mystery purchase: USD $25.00 [Other] - ❌ Unpaid
Rent: USD $1200.00 [Bills] - βœ… Paid
Tokyo hotel: JPY $15000.00 [Travel] - ❌ Unpaid
Enter fullscreen mode Exit fullscreen mode

Part 6: Combining Named Constructors with Optional Parameters

The most powerful approach combines both techniques:

class Expense {
  String description;
  double amount;
  String category;
  DateTime date;
  String? notes;
  bool isPaid;

  // Regular constructor with named parameters
  Expense({
    required this.description,
    required this.amount,
    required this.category,
    DateTime? date,
    this.notes,
    this.isPaid = false,
  }) : date = date ?? DateTime.now();

  // Named constructor: quick expense (today, unpaid)
  Expense.quick(
    this.description,
    this.amount,
    this.category, {
    this.notes,
  }) : date = DateTime.now(),
       isPaid = false;

  // Named constructor: monthly bill (bills category, today, paid)
  Expense.bill(
    this.description,
    this.amount, {
    this.notes,
  }) : category = 'Bills',
       date = DateTime.now(),
       isPaid = true;

  // Named constructor: recurring subscription
  Expense.subscription({
    required this.description,
    required this.amount,
    this.notes,
  }) : category = 'Entertainment',
       date = DateTime.now(),
       isPaid = true;

  void printDetails() {
    String paid = isPaid ? 'βœ…' : '❌';
    String noteText = notes != null ? ' - $notes' : '';
    print('$paid $description: \$${amount.toStringAsFixed(2)} [$category]$noteText');
  }
}

void main() {
  print('OCTOBER EXPENSES:\n');

  // Different ways to create expenses
  var coffee = Expense.quick('Coffee', 4.50, 'Food');

  var rent = Expense.bill('Rent', 1200.0, notes: 'October payment');

  var netflix = Expense.subscription(
    description: 'Netflix',
    amount: 15.99,
    notes: 'Premium plan',
  );

  var dinner = Expense(
    description: 'Dinner date',
    amount: 85.50,
    category: 'Food',
    notes: 'Anniversary',
  );

  coffee.printDetails();
  rent.printDetails();
  netflix.printDetails();
  dinner.printDetails();
}
Enter fullscreen mode Exit fullscreen mode

Output:

OCTOBER EXPENSES:

❌ Coffee: $4.50 [Food]
βœ… Rent: $1200.00 [Bills] - October payment
βœ… Netflix: $15.99 [Entertainment] - Premium plan
❌ Dinner date: $85.50 [Food] - Anniversary
Enter fullscreen mode Exit fullscreen mode

Part 7: Real-World Example - Complete Expense Class

Here's a production-ready Expense class with multiple constructors:

class Expense {
  String description;
  double amount;
  String category;
  DateTime date;
  String? notes;
  bool isPaid;
  String paymentMethod;

  // Main constructor - full control
  Expense({
    required this.description,
    required this.amount,
    required this.category,
    DateTime? date,
    this.notes,
    this.isPaid = false,
    this.paymentMethod = 'Cash',
  }) : date = date ?? DateTime.now();

  // Quick daily expense
  Expense.quick(
    this.description,
    this.amount,
    this.category,
  ) : date = DateTime.now(),
      notes = null,
      isPaid = false,
      paymentMethod = 'Cash';

  // Monthly recurring bill
  Expense.monthlyBill(
    this.description,
    this.amount, {
    this.paymentMethod = 'Auto-pay',
  }) : category = 'Bills',
       date = DateTime.now(),
       notes = 'Monthly recurring',
       isPaid = true;

  // Credit card purchase
  Expense.creditCard(
    this.description,
    this.amount,
    this.category, {
    this.notes,
  }) : date = DateTime.now(),
       isPaid = false,
       paymentMethod = 'Credit Card';

  // Cash purchase (already paid)
  Expense.cash(
    this.description,
    this.amount,
    this.category, {
    this.notes,
  }) : date = DateTime.now(),
       isPaid = true,
       paymentMethod = 'Cash';

  // Yesterday's expense (for when you forget to log)
  Expense.yesterday(
    this.description,
    this.amount,
    this.category,
  ) : date = DateTime.now().subtract(Duration(days: 1)),
      notes = 'Logged late',
      isPaid = true,
      paymentMethod = 'Cash';

  void printDetails() {
    String paid = isPaid ? 'βœ… Paid' : '❌ Unpaid';
    String noteText = notes != null ? '\n   Note: $notes' : '';
    print('─────────────────────────────');
    print('$description');
    print('Amount: \$${amount.toStringAsFixed(2)}');
    print('Category: $category');
    print('Date: ${date.toString().split(' ')[0]}');
    print('Payment: $paymentMethod');
    print('Status: $paid$noteText');
  }

  bool isMajorExpense() => amount > 100;

  String getSummary() {
    String emoji = isMajorExpense() ? 'πŸ”΄' : '🟒';
    return '$emoji \$${amount.toStringAsFixed(2)} - $description [$category]';
  }
}

void main() {
  print('πŸ“Š EXPENSE TRACKER\n');

  var expenses = [
    Expense.quick('Morning coffee', 4.50, 'Food'),
    Expense.monthlyBill('Internet', 59.99),
    Expense.creditCard('Groceries', 127.50, 'Food', notes: 'Weekly shopping'),
    Expense.cash('Parking', 5.00, 'Transport'),
    Expense.yesterday('Lunch', 15.00, 'Food'),
    Expense(
      description: 'New laptop',
      amount: 1299.99,
      category: 'Electronics',
      paymentMethod: 'Credit Card',
      notes: 'Work equipment',
    ),
  ];

  // Print all summaries
  print('Summary:');
  for (var expense in expenses) {
    print(expense.getSummary());
  }

  // Print details of major expenses
  print('\n\nMAJOR EXPENSES (>\$100):');
  for (var expense in expenses) {
    if (expense.isMajorExpense()) {
      expense.printDetails();
    }
  }

  // Calculate totals
  double total = 0;
  double unpaid = 0;

  for (var expense in expenses) {
    total += expense.amount;
    if (!expense.isPaid) {
      unpaid += expense.amount;
    }
  }

  print('\n─────────────────────────────');
  print('πŸ’° Total Expenses: \$${total.toStringAsFixed(2)}');
  print('❌ Unpaid: \$${unpaid.toStringAsFixed(2)}');
  print('βœ… Paid: \$${(total - unpaid).toStringAsFixed(2)}');
}
Enter fullscreen mode Exit fullscreen mode

🎯 Practice Exercises

Exercise 1: Create a Budget Class (Easy)

Create a Budget class with:

  • Properties: category, limit, month, year
  • Regular constructor
  • Named constructor Budget.monthly(category, limit) that uses current month/year

Solution:

class Budget {
  String category;
  double limit;
  int month;
  int year;

  Budget(this.category, this.limit, this.month, this.year);

  Budget.monthly(this.category, this.limit)
    : month = DateTime.now().month,
      year = DateTime.now().year;

  void printDetails() {
    print('$category: \$${limit.toStringAsFixed(2)} for $month/$year');
  }
}

void main() {
  var foodBudget = Budget.monthly('Food', 500.0);
  var rentBudget = Budget('Rent', 1200.0, 10, 2025);

  foodBudget.printDetails();
  rentBudget.printDetails();
}
Enter fullscreen mode Exit fullscreen mode

Exercise 2: Enhanced Expense Creation (Medium)

Add these named constructors to your Expense class:

  1. Expense.splitBill(description, totalAmount, numberOfPeople) - divides amount by number of people
  2. Expense.tip(description, baseAmount, tipPercent) - calculates total with tip
  3. Expense.recurring(description, amount, frequency) - where frequency is 'weekly', 'monthly', or 'yearly'

Solution:

class Expense {
  String description;
  double amount;
  String category;
  DateTime date;
  String? notes;

  Expense(this.description, this.amount, this.category, this.date, {this.notes});

  // Split bill constructor
  Expense.splitBill(
    String desc,
    double totalAmount,
    int people,
  ) : description = '$desc (split $people ways)',
      amount = totalAmount / people,
      category = 'Food',
      date = DateTime.now(),
      notes = 'Split with $people people';

  // Tip calculator constructor
  Expense.tip(
    String desc,
    double baseAmount,
    double tipPercent,
  ) : description = desc,
      amount = baseAmount * (1 + tipPercent / 100),
      category = 'Food',
      date = DateTime.now(),
      notes = '${tipPercent}% tip included';

  // Recurring expense constructor
  Expense.recurring(
    this.description,
    this.amount,
    String frequency,
  ) : category = 'Bills',
      date = DateTime.now(),
      notes = 'Recurring: $frequency';
}

void main() {
  var dinner = Expense.splitBill('Restaurant dinner', 120.00, 4);
  var lunch = Expense.tip('Lunch', 25.00, 20);
  var gym = Expense.recurring('Gym membership', 45.00, 'monthly');

  print('${dinner.description}: \$${dinner.amount.toStringAsFixed(2)} - ${dinner.notes}');
  print('${lunch.description}: \$${lunch.amount.toStringAsFixed(2)} - ${lunch.notes}');
  print('${gym.description}: \$${gym.amount.toStringAsFixed(2)} - ${gym.notes}');
}
Enter fullscreen mode Exit fullscreen mode

Exercise 3: Flexible User Class (Hard)

Create a User class for your expense app with:

  • Properties: name, email, currency, monthlyBudget, notifications
  • Main constructor with named parameters (all required except notifications default = true)
  • User.quick(name, email) - uses USD currency, $2000 budget
  • User.premium(name, email, monthlyBudget) - USD currency, notifications on

Solution:

class User {
  String name;
  String email;
  String currency;
  double monthlyBudget;
  bool notifications;

  User({
    required this.name,
    required this.email,
    required this.currency,
    required this.monthlyBudget,
    this.notifications = true,
  });

  User.quick(this.name, this.email)
    : currency = 'USD',
      monthlyBudget = 2000.0,
      notifications = true;

  User.premium(this.name, this.email, this.monthlyBudget)
    : currency = 'USD',
      notifications = true;

  void printProfile() {
    String notif = notifications ? 'βœ…' : '❌';
    print('─────────────────');
    print('πŸ‘€ $name');
    print('πŸ“§ $email');
    print('πŸ’΅ Budget: $currency \$${monthlyBudget.toStringAsFixed(2)}/month');
    print('πŸ”” Notifications: $notif');
  }
}

void main() {
  var user1 = User.quick('John Doe', 'john@example.com');
  var user2 = User.premium('Jane Smith', 'jane@example.com', 3500.0);
  var user3 = User(
    name: 'Bob Wilson',
    email: 'bob@example.com',
    currency: 'EUR',
    monthlyBudget: 2500.0,
    notifications: false,
  );

  user1.printProfile();
  user2.printProfile();
  user3.printProfile();
}
Enter fullscreen mode Exit fullscreen mode

Common Mistakes & How to Fix Them

Mistake 1: Forgetting Initializer List Colon

// ❌ Wrong
Expense.quick(this.description, this.amount, this.category) 
  date = DateTime.now();

// βœ… Correct - need the colon
Expense.quick(this.description, this.amount, this.category) 
  : date = DateTime.now();
Enter fullscreen mode Exit fullscreen mode

Mistake 2: Mixing Positional and Named Parameters Incorrectly

// ❌ Wrong - can't mix freely
Expense(this.description, {required this.amount}, this.category);

// βœ… Correct - positional first, then named
Expense(this.description, this.category, {required this.amount});
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Not Using required for Named Parameters

// ❌ Wrong - amount could be null
Expense({this.description, this.amount, this.category});

// βœ… Correct - make required fields explicit
Expense({
  required this.description,
  required this.amount,
  required this.category,
});
Enter fullscreen mode Exit fullscreen mode

Mistake 4: Forgetting this in Named Constructors

// ❌ Wrong
Expense.quick(description, amount, category)
  : date = DateTime.now();

// βœ… Correct
Expense.quick(this.description, this.amount, this.category)
  : date = DateTime.now();
Enter fullscreen mode Exit fullscreen mode

Key Concepts Review

βœ… Named constructors let you create objects in different ways

βœ… Initializer lists (:) set properties before object creation

βœ… Optional positional parameters use [] brackets

βœ… Named parameters use {} curly braces and are more readable

βœ… Default values make parameters optional with sensible defaults

βœ… required keyword makes named parameters mandatory

βœ… Combine techniques for maximum flexibility


Self-Check Questions

1. What's the difference between a regular constructor and a named constructor?

Answer:

  • Regular constructor: Expense(...) - only one per class
  • Named constructor: Expense.quick(...) - can have multiple with different names, each providing different ways to create objects

2. What does the : (colon) mean in a constructor?

Answer:
The colon starts the initializer list, where you can set property values before the constructor body runs. It's used to initialize properties that aren't in the parameter list.

3. When should you use named parameters vs positional parameters?

Answer:

  • Positional: When order is obvious and there are few parameters (2-3)
  • Named: When you have many parameters, some are optional, or readability is important

What's Next?

In Lesson 4: Encapsulation, we'll learn:

  • How to make properties private (with _ underscore)
  • Why we should hide internal data
  • Getters and setters for controlled access
  • Validation to prevent invalid data
  • Keeping our expense data safe!

Example preview:

class Expense {
  String _description;  // Private!
  double _amount;       // Private!

  // Can't do: expense._amount = -100;
  // Must use: expense.setAmount(100);

  set amount(double value) {
    if (value < 0) {
      print('❌ Amount cannot be negative!');
      return;
    }
    _amount = value;
  }
}
Enter fullscreen mode Exit fullscreen mode

See you in Lesson 4! πŸš€


Additional Resources

  • Try creating constructors for other classes: Category, PaymentMethod, Budget
  • Experiment with combining multiple techniques
  • Think about which constructor style makes your code most readable

Remember: The goal is to make object creation easy and intuitive for whoever uses your class (including future you)! πŸ’ͺ

Top comments (0)