Master Jetpack Compose Layouts in 2025!
“40 Tips & Tricks for Jetpack Compose Layouts” - fast, practical, non-deprecated patterns you actually use.
40 Tips & Tricks for Jetpack Compose Layouts
A compact, production-ready reference for modern Compose layouts. Learn when to use each pattern and how to wire it fast. Copy, tweak, and ship.
// 🚀 Start composing safely
@Composable
fun App() {
/* Entry point */
}
Row ➡️
Lay out children horizontally with predictable spacing. Great for toolbars, list rows, and paired fields. Keep alignment explicit.
// 🧩 Even spacing + center alignment
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text("A")
Text("B")
Text("C")
}
Column ⬇️
Stack UI vertically for forms and settings. Ideal for short, static content. Switch to LazyColumn for long lists.
// 📚 Simple vertical stack
Column(
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text("Top")
Text("Bottom")
}
Box 🧊
Overlay children in layers with minimal code. Perfect for badges, watermarks, and banners. Control positions with align().
// 🧷 Overlay label on media
Box {
Image(/* ... */)
Text("Overlay", Modifier.align(Alignment.TopEnd))
}
BoxWithConstraints 📐
Build responsive UIs by reading parent bounds. Use width breakpoints to swap layouts. Extract heavy branches into functions.
// 🔀 Switch by width breakpoint
BoxWithConstraints {
if (maxWidth > 400.dp) {
Wide()
} else {
Compact()
}
}
Spacer & Divider 📏
Add intentional gaps and subtle separators. Keep lists readable without extra wrappers. Prefer spacedBy for consistent gaps.
// ↔️ Inline gap with Spacer
Row {
Text("L")
Spacer(Modifier.width(8.dp)) // gap
Text("R")
}
// 🧵 Section separator
// Divider()
Alignment & Arrangement 🎯
Control where items sit inside Row/Column. Combine centering with space distribution. Make intent obvious for future readers.
// 🎚️ SpaceBetween + center vertically
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
/* … */
}
Weight ⚖️
Share leftover space by ratio. Create balanced splits without fixed sizes. Works in rows and columns.
// 📏 1:2 proportional split
Row {
Box(Modifier.weight(1f))
Box(Modifier.weight(2f))
}
Size & Padding 📦
Define clear bounds with size modifiers. Add inner breathing room using padding. Keep touch targets comfortable.
// 📐 Fixed size with inner padding
Box(
Modifier
.size(80.dp)
.padding(8.dp)
)
Fill / Wrap 🧯
Fill matches the parent; wrap fits content. Choose intentionally for predictable results. Mix for responsive blocks.
// ↔️ Fill width
Box(Modifier.fillMaxWidth())
// 🧷 Wrap content size
Box(Modifier.wrapContentSize())
Aspect Ratio ▣
Keep media consistent across screens. Lock cards and players to 1:1 or 16:9. Prevent layout jumps.
// 🎬 16:9 area for media
Box(Modifier.aspectRatio(16f / 9f))
Offset & zIndex 🧱
Nudge elements without re-layout. Raise components above siblings when needed. Use sparingly for clarity.
// 🧭 Nudge on X and raise above siblings
Box(
Modifier
.offset(x = 8.dp)
.zIndex(2f)
) {
/* … */
}
Clip / Border / Background 🖼️
Shape, outline, and fill any container. Order matters for visuals. Achieve polished cards and chips.
// 🫓 Rounded, outlined, filled
Box(
Modifier
.clip(RoundedCornerShape(12.dp))
.border(1.dp, Color.Gray)
.background(Color.White)
)
Scrollable Content 🌀
Add vertical scrolling to small layouts. Great for settings and short pages. Use Lazy* for large datasets.
// 🖱️ Lightweight vertical scroll
val scroll = rememberScrollState()
Column(
Modifier.verticalScroll(scroll)
) {
/* content */
}
LazyColumn 📜
Virtualized lists compose only what’s visible. Ideal for feeds, chats, and catalogs. Provide stable keys for smooth diffs.
// 🏎️ Efficient long list
LazyColumn {
items(100) { i ->
Text("Item $i")
}
}
LazyRow 🎞️
Horizontal virtualization for chips and carousels. Add side padding for breathing room. Snap if needed for UX.
// 🎠 Chip carousel
LazyRow {
items(20) { i ->
Chip("Tag $i")
}
}
LazyVerticalGrid 🧱
Build responsive galleries with fixed or adaptive cells. Let columns grow with width. Keep tiles consistent.
// 🧩 Adaptive columns
LazyVerticalGrid(
columns = GridCells.Adaptive(120.dp)
) {
items(60) { i ->
GridTile(i)
}
}
LazyHorizontalGrid 🧩
Scroll grids sideways for TV and dashboards. Control rows instead of columns. Useful for landscape UIs.
// ↔️ Scrolling rows grid
LazyHorizontalGrid(
rows = GridCells.Fixed(2)
) {
items(30) { i ->
Tile(i)
}
}
Lazy Sticky Headers 📌
Pin section titles as content scrolls. Improve discoverability in long lists. Keep headers lightweight.
// 📍 Pinned header + items
LazyColumn {
stickyHeader { Text("Section A") }
items(listA) { item -> RowItem(item) }
}
Lazy State & Scroll Control 🎚️
Jump to items and preserve position. Programmatic control enables deep-linking and restore. Debounce to avoid flicker.
// 🎯 Jump to index 25
val state = rememberLazyListState()
LaunchedEffect(Unit) {
state.scrollToItem(25)
}
LazyColumn(state = state) {
/* … */
}
Keys & contentType (perf) 🚀
Stable keys prevent state leaks on reorder. Content types speed measuring. Essential for mixed cells.
// ⚙️ Diff-friendly items
LazyColumn {
items(
items,
key = { it.id },
contentType = { it.type }
) { item ->
Item(item)
}
}
FlowRow (stable) 🧵
Auto-wrap chips and tags across lines. Perfect for filters and keyword clouds. Use spaced gutters.
// 🌊 Wrapping chips
FlowRow {
repeat(12) { i ->
Chip("Tag $i")
}
}
FlowColumn (stable) 🧶
Wrap vertically when height is tight. Useful for labels and badges. Avoid deep nesting for speed.
// 🧯 Vertical wrapping
FlowColumn {
repeat(6) {
Box(Modifier.height(60.dp))
}
}
ConstraintLayout 📏
Model precise relationships without nesting chains. Overlap, baseline, and guideline with clarity. Reach for it when rules grow.
// 🕸️ Linked references
ConstraintLayout {
val (a, b) = createRefs()
Text(
"A",
Modifier.constrainAs(a) {
start.linkTo(parent.start)
}
)
Text(
"B",
Modifier.constrainAs(b) {
start.linkTo(a.end)
}
)
}
Intrinsic Size 📐
Size parents based on natural child measurements. Align heights and dividers cleanly. Use sparingly-intrinsics cost.
// 📏 Equal heights + divider
Row(Modifier.height(IntrinsicSize.Min)) {
Box(
Modifier
.fillMaxHeight()
.width(1.dp)
)
}
WindowInsets & Safe Areas 🧢
Respect status/nav bars and cutouts. Pad content automatically to avoid underlap. Essential for edge-to-edge.
// 🛡️ Avoid underlap with bars
Column(
Modifier
.statusBarsPadding()
.navigationBarsPadding()
) {
/* UI */
}
Scaffold (Material 3) 🏗️
Screen shell with app bars, FABs, and drawers. Route inner padding to content. Consistent page structure fast.
// 🧭 App page shell
Scaffold(
topBar = { SmallTopAppBar(title = { Text("Title") }) }
) { inner ->
Content(Modifier.padding(inner))
}
Navigation Drawers (M3) 📚
Expose secondary destinations with a side panel. Keep labels short and states clear. Modal for phones; permanent for wide.
// 📁 Modal drawer pattern
ModalNavigationDrawer(
drawerContent = { DrawerContent() }
) {
ScreenContent()
}
NavigationRail / Bars (M3) 🛤️
Use rails on large screens; bottoms on phones. Limit to 3–5 top-level tabs. Keep icons consistent.
// 🧭 Rail for tablets/desktop
NavigationRail {
items.forEach { /* NavigationRailItem(...) */ }
}
SubcomposeLayout (advanced) 🧠
Compose after measuring to adapt dynamically. Perfect for tabs and banners. Cache repeated subcompositions.
// 🧪 Two-phase measure
SubcomposeLayout { constraints ->
val p = subcompose("main") { Content() }
.first()
.measure(constraints)
layout(p.width, p.height) {
p.place(0, 0)
}
}
Custom Layout 🔧
Own the measure/place pipeline fully. Build unique patterns beyond stock layouts. Profile and keep math simple.
// 🧱 Overlapping layout
@Composable
fun Overlap(
content: @Composable () -> Unit
) = Layout(content) { measurables, c ->
val ps = measurables.map { it.measure(c) }
val w = ps.maxOf { it.width }
val h = ps.maxOf { it.height }
layout(w, h) { ps.forEach { it.place(0, 0) } }
}
Arrangements & spacedBy 🧊
Get consistent gutters without extra Spacers. Improves readability and reduces boilerplate. Tweak one value to refit a section.
// 🧵 Consistent vertical gaps
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
/* … */
}
Flow + LazyGrid Combo 💪
Use FlowRow for filters above a grid. Keep controls flexible and content virtualized. Scales from phone to desktop.
// 🔗 Filters above a grid
Column {
FlowRow { Filters() }
LazyVerticalGrid(GridCells.Adaptive(160.dp)) {
/* … */
}
}
Responsive Breakpoints 🧭
Swap UI variants by width thresholds. Keep structure consistent across sizes. Optimize for readability first.
// 📱📲💻 Phone / Tablet / Desktop
BoxWithConstraints {
when {
maxWidth < 360.dp -> Phone()
maxWidth < 840.dp -> Tablet()
else -> Desktop()
}
}
RTL / LayoutDirection 🌍
Mirror layouts for right-to-left languages. Verify arrows and icons manually. Essential for global apps.
// 🌐 Mirror UI for RTL
CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Rtl
) {
Content()
}
Insets + Scaffold Combo 🧱
Pipe innerPadding to avoid underlapping bars. Add imePadding() for input-heavy screens. Works great edge-to-edge.
// 🧯 Safe areas via inner padding
Scaffold { inner ->
Screen(Modifier.padding(inner))
}
Modifier Order Matters 🧠
Modifier order changes layout and drawing. Think: layout → draw → input. Swap to troubleshoot visual bugs.
// ⚠️ Different if reversed
Box(
Modifier
.padding(8.dp) // layout
.background(Color.Gray) // draw
)
Performance Tip 🔥
Use Lazy* for long or dynamic content. Avoid Column+verticalScroll for big lists. Provide keys and content types.
// 🚀 Use LazyColumn, not Column+verticalScroll, for long lists
Testing Layouts 🧪
Assert visibility, size, and semantics with tests. Add tags and content descriptions consistently. Catch regressions early.
// ✅ Visibility assertion
composeTestRule
.onNodeWithText("Item")
.assertIsDisplayed()









































