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.

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.