What You Will Build
A financial dashboard showing a balance summary card with income and expenses breakdown, plus a scrollable transaction list with color-coded amounts. This pattern is used in banking apps, expense trackers, and any app displaying financial summaries behind a paywall.
Why This Pattern Matters
Financial dashboards require precise number formatting, color-coded positive/negative values, and clear visual hierarchy between summary and detail views. These skills apply to any data-heavy dashboard: analytics, health metrics, or inventory management.
Key Concepts
- Card with primary containerColor for hero summary sections.
- String.format for currency formatting with commas and decimals.
- Conditional coloring based on income vs expense boolean flags.
The Dashboard Screen
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PaywallViewScreen() {
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)
)
val balance = transactions.sumOf { it.amount }
Scaffold(
topBar = { TopAppBar(title = { Text("Paywall View") }) }
) { padding ->
Column(
Modifier.fillMaxSize().padding(padding).padding(16.dp)
) {
// 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(
"+${'$'}...",
color = Color(0xFF81C784),
fontWeight = FontWeight.SemiBold
)
}
Column {
Text("Expenses",
color = Color.White.copy(alpha = 0.7f))
Text(
"-${'$'}...",
color = Color(0xFFEF9A9A),
fontWeight = FontWeight.SemiBold
)
}
}
}
}
Spacer(Modifier.height(16.dp))
Text("Recent Transactions",
fontWeight = FontWeight.Bold, fontSize = 18.sp)
Spacer(Modifier.height(8.dp))
// Transaction list
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(
"${'$'}{if (tx.isIncome) "+" else ""}...",
fontWeight = FontWeight.Bold,
color = if (tx.isIncome) Color(0xFF4CAF50)
else Color(0xFFF44336)
)
}
}
}
}
}
}
Tips and Pitfalls
- Primary color card: Using
MaterialTheme.colorScheme.primaryas containerColor creates a branded hero card that stands out. - Currency formatting: Use
String.format("%,.2f", amount)for locale-aware thousand separators and two decimal places. - Color coding convention: Green (0xFF4CAF50) for income, red (0xFFF44336) for expenses follows universal financial UI conventions.
- LazyColumn migration: For longer transaction lists, replace
forEachwithLazyColumn+items()for better performance.
Minimum SDK: API 21+ with Compose BOM