What You Will Build
A complete expense tracker screen with a balance summary card showing income vs expenses, and a scrollable list of recent transactions with color-coded icons. The balance card uses the primary color scheme, while income items are green and expenses are red.
Why This Pattern Matters
Finance dashboards are among the most requested app UIs. This project teaches Material 3 Card layouts, Scaffold with TopAppBar, conditional coloring, and data formatting — all patterns you will reuse in any data-driven app.
Step 1: Define the Transaction Model
data class Transaction(
val name: String,
val amount: Double,
val icon: ImageVector,
val isIncome: Boolean
)
val transactions = listOf(
Transaction("Salary", 5000.0,
Icons.Default.ArrowDownward, true),
Transaction("Rent", -1200.0,
Icons.Default.Home, false),
Transaction("Groceries", -85.50,
Icons.Default.ShoppingCart, false),
Transaction("Freelance", 750.0,
Icons.Default.ArrowDownward, true),
Transaction("Utilities", -120.0,
Icons.Default.ElectricalServices, false)
)
Step 2: Build the Balance Summary Card
Card(
Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Column(Modifier.padding(24.dp)) {
Text("Total Balance",
color = Color.White.copy(alpha = 0.8f))
Text(
"${'$'}${String.format("%,.2f", balance)}",
fontSize = 36.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(Modifier.height(16.dp))
Row(Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween) {
Column {
Text("Income",
color = Color.White.copy(alpha = 0.7f))
Text("+${'$'}5,750.00",
color = Color(0xFF81C784),
fontWeight = FontWeight.SemiBold)
}
Column {
Text("Expenses",
color = Color.White.copy(alpha = 0.7f))
Text("-${'$'}1,405.50",
color = Color(0xFFEF9A9A),
fontWeight = FontWeight.SemiBold)
}
}
}
}
Step 3: Transaction List Items
transactions.forEach { tx ->
Card(Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
Row(Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically) {
Box(
Modifier.size(40.dp)
.clip(CircleShape)
.background(
if (tx.isIncome)
Color(0xFF4CAF50).copy(alpha = 0.1f)
else Color(0xFFF44336).copy(alpha = 0.1f)
),
contentAlignment = Alignment.Center
) {
Icon(tx.icon, null,
tint = if (tx.isIncome)
Color(0xFF4CAF50) else Color(0xFFF44336))
}
Spacer(Modifier.width(12.dp))
Text(tx.name, modifier = Modifier.weight(1f))
Text(
"${'$'}${String.format("%.2f", tx.amount)}",
fontWeight = FontWeight.Bold,
color = if (tx.isIncome)
Color(0xFF4CAF50) else Color(0xFFF44336)
)
}
}
}
Tips and Pitfalls
- CardDefaults.cardColors(containerColor = primary) turns a Card into a hero banner. This is cleaner than applying background color manually.
- CircleShape icon background with 10% alpha tint creates the Material Design icon container pattern.
- String.format("%,.2f") adds comma separators for currency display.
- Negative amounts: Store expenses as negative numbers and use conditional formatting to show + or - prefix.