Adaptive Layouts in Compose: Grid, FlexBox, MediaQuery

AndroidJetpack ComposeAdaptive UIFoldables

The fastest way to build a layout that breaks on a foldable is to branch on if (isTablet). I've inherited more than one codebase that did exactly that, and every one of them shipped broken split-screen and multi-window experiences. Adaptive layouts in Compose start from a different premise: you don't care what the device is, you care how much window you've been given right now — which changes when the user unfolds, rotates, or drags your app into split-screen.

Compose has grown a solid set of tools for this: window size classes to decide what layout to show, and grid and flex primitives to decide how content reflows within it. Used together they let one composable serve a phone, a foldable, and a tablet without device sniffing.

Branch on window size class, never on device

The foundation is WindowSizeClass, which buckets the current window into Compact, Medium, and Expanded on each axis. This is the Compose analog of CSS media queries, and it's the only correct thing to branch on, because it reflects the available window — respecting split-screen, foldables, and desktop windowing — not the physical panel.

@Composable
fun AdaptiveScreen(windowSizeClass: WindowSizeClass) {
    when (windowSizeClass.widthSizeClass) {
        WindowWidthSizeClass.Compact ->
            SinglePaneList()                 // phone portrait
        WindowWidthSizeClass.Medium ->
            SinglePaneList(columns = 2)      // foldable / small tablet
        WindowWidthSizeClass.Expanded ->
            ListDetailPane()                 // tablet / desktop: two panes
    }
}

The rule of thumb: use width classes to switch navigation and pane structure (single pane vs list-detail), and use the layout primitives below to reflow content within a pane. For the canonical patterns — list-detail, supporting-pane — the androidx.compose.material3.adaptive library gives you scaffolds so you don't hand-roll them.

Grid: aligned rows and columns

LazyVerticalGrid handles the classic responsive-gallery case. The key is GridCells.Adaptive, which sizes the number of columns to the available width instead of you picking a fixed count per breakpoint:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 160.dp),
    contentPadding = PaddingValues(16.dp),
    horizontalArrangement = Arrangement.spacedBy(12.dp),
    verticalArrangement = Arrangement.spacedBy(12.dp),
) {
    items(photos, key = { it.id }) { PhotoCell(it) }
}

Adaptive(minSize = 160.dp) says "as many columns as fit, each at least 160dp." A narrow phone gets two columns, a tablet gets five or six — automatically, with no size-class branching for the grid itself. Always pass a stable key in items; unkeyed grids recompose and re-scroll badly, the same recomposition pitfall that bites lists.

FlexBox: fluid wrapping for uneven items

When items vary in width and should wrap to fill the row — filter chips, tags, keyword pills — a grid's fixed cells fight you. That's what FlowRow and FlowColumn are for. They place children in sequence and wrap to the next line when they run out of room, which is the Compose equivalent of CSS flex-wrap.

FlowRow(
    horizontalArrangement = Arrangement.spacedBy(8.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp),
) {
    filters.forEach { FilterChip(it) }
}

Reach for flow layouts whenever content is intrinsically sized and heterogeneous; reach for grid when you want the tidy alignment of consistent cells. Many real screens use both — a flow row of filters above a grid of results.

The pattern that scales: layout as a function of window

Putting it together, the discipline that keeps adaptive UI maintainable is treating layout as a pure function of window metrics, decided at one level, then passed down. Don't sprinkle size checks through leaf composables; resolve the size class near the top and hand children a semantic decision.

Content type Primitive Adapts by
Pane structure (nav, list-detail) WindowSizeClass + adaptive scaffolds width size class
Uniform cells (gallery, cards) LazyVerticalGrid + GridCells.Adaptive available width
Uneven wrapping items (chips) FlowRow / FlowColumn content + width
Fully custom Layout / SubcomposeLayout your own logic

This mirrors the state-hoisting principle from ten years of Compose lessons: make the decision once, high up, and keep the leaves dumb.

Foldables and continuity

Foldables add a wrinkle: the window changes while the app is running when the user folds or unfolds, and there may be a hinge cutting through your layout. Two things matter. First, because you're already driving layout off window size classes, an unfold naturally re-evaluates and reflows — that's the payoff for not device-sniffing. Second, use WindowInfoTracker (from Jetpack WindowManager) to detect fold posture and hinge bounds when you want to do something clever like tabletop mode (content above the fold, controls below). And test state preservation across the fold: rememberSaveable for anything the user would hate to lose, since a fold is effectively a configuration change.

Edge-to-edge and insets

Adaptive layout and system insets go hand in hand. On modern Android, apps draw edge-to-edge by default, so your adaptive content must respect WindowInsets (status bar, navigation bar, display cutout, IME) or it'll draw under system UI. Apply insets at the scaffold level and let padding flow down; combined with size-class-driven structure, that's what makes a layout feel native on everything from a compact phone to a desktop window.

What I'd take away

Stop asking "is this a tablet?" and start asking "how much window do I have?" Branch structure on WindowSizeClass, reflow uniform content with GridCells.Adaptive, wrap uneven content with FlowRow/FlowColumn, resolve size decisions high in the tree, and respect insets. That combination gives you a single codebase that behaves correctly across phones, foldables, tablets, split-screen, and desktop windows — without the fragile device checks that break the moment a new form factor ships.

Resources

Frequently asked questions

How do you build adaptive layouts in Jetpack Compose?

Drive layout decisions from window size classes rather than hardcoded device checks. Use WindowSizeClass to pick between compact, medium, and expanded layouts, and use grid and flex primitives to reflow content. This keeps one composable responsive across phones, foldables, and tablets.

What is the difference between window size class and screen size?

Screen size is the raw pixel dimension; window size class is a semantic bucket (compact, medium, expanded) based on the available window, not the physical display. Size classes are what you should branch on, because they handle split-screen, foldables, and resizable windows correctly.

Should I use a Grid or a FlexBox layout in Compose?

Use a grid when you want items aligned to consistent rows and columns, like a photo gallery. Use a flex-style layout when items have varying sizes and should wrap and fill available space fluidly, like chips or tags. Many adaptive screens use both.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →