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());
}
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)
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}');
}
Understanding the Syntax:
Expense.quick(this.description, this.amount, this.category)
: date = DateTime.now();
-
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();
}
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
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)}');
}
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));
}
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),
);
}
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();
}
Output:
Mystery purchase: USD $25.00 [Other] - β Unpaid
Rent: USD $1200.00 [Bills] - β
Paid
Tokyo hotel: JPY $15000.00 [Travel] - β Unpaid
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();
}
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
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)}');
}
π― 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();
}
Exercise 2: Enhanced Expense Creation (Medium)
Add these named constructors to your Expense class:
-
Expense.splitBill(description, totalAmount, numberOfPeople)
- divides amount by number of people -
Expense.tip(description, baseAmount, tipPercent)
- calculates total with tip -
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}');
}
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();
}
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();
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});
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,
});
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();
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;
}
}
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)