What You Will Build
A furniture e-commerce grid with a badged shopping cart, product cards featuring square image placeholders, pricing, and add-to-cart actions. This extends the standard e-commerce pattern with furniture-specific design: larger images, minimal text, and prominent pricing.
Why This Pattern Matters
Furniture and home decor apps emphasize visual browsing. The 2-column grid with square aspect ratios gives products room to breathe. This is the layout used by IKEA, Wayfair, and West Elm mobile apps.
Step 1: Product Grid with Badge
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FurnitureAppScreen() {
var cartCount by remember { mutableIntStateOf(0) }
val products = remember { listOf(
Product("Modern Sofa", 899.99, "Minimalist design", Icons.Default.Chair),
Product("Floor Lamp", 149.99, "Ambient lighting", Icons.Default.Lightbulb),
Product("Coffee Table", 349.99, "Solid oak", Icons.Default.TableBar),
Product("Bookshelf", 279.99, "5-tier storage", Icons.Default.Shelves),
Product("Desk Chair", 459.99, "Ergonomic", Icons.Default.EventSeat),
Product("Side Table", 129.99, "Compact design", Icons.Default.Weekend)
) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Furniture App") },
actions = {
BadgedBox(badge = {
if (cartCount > 0) Badge { Text("$cartCount") }
}) {
IconButton(onClick = {}) {
Icon(Icons.Default.ShoppingCart, "Cart")
}
}
}
)
}
) { padding ->
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.padding(padding).padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(products) { product ->
FurnitureProductCard(product) { cartCount++ }
}
}
}
}
Step 2: Product Card Component
@Composable
fun FurnitureProductCard(product: Product, onAddToCart: () -> Unit) {
Card(Modifier.fillMaxWidth()) {
Column(Modifier.padding(12.dp)) {
Box(
Modifier.fillMaxWidth()
.aspectRatio(1f)
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center
) {
Icon(product.icon, null, modifier = Modifier.size(48.dp))
}
Spacer(Modifier.height(8.dp))
Text(product.name, fontWeight = FontWeight.SemiBold,
maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(product.desc, fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant)
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("${'$'}${String.format("%.2f", product.price)}",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary)
IconButton(onClick = onAddToCart) {
Icon(Icons.Default.AddShoppingCart, "Add")
}
}
}
}
}
Tips and Pitfalls
- Pass lambda callbacks (onAddToCart) instead of mutating state inside the card. This keeps cards stateless and reusable.
- remember { listOf(...) } prevents the product list from being recreated on every recomposition.
- For real images: Replace the Box+Icon placeholder with AsyncImage from Coil, keeping the same aspectRatio(1f) modifier.
- Badge auto-hides when placed inside the if-check. No need for AnimatedVisibility unless you want a transition.
Min SDK: 21 | Compose BOM: 2024.01.00+