
Building conditional layouts in Jetpack Compose
Building dynamic UIs often means adapting to varying screen sizes and content. Learn how to intelligently show or hide Composables based on available space, without hard-coded values.
Jetpack Compose has revolutionized Android UI development, offering a declarative and intuitive approach. However, one common challenge developers face is creating layouts that gracefully adapt when there isn’t enough space to display all elements. You might have a scenario where you want to show a particular UI element only if it fits, otherwise, it should remain hidden to prevent clipping or overflow.
🚗 The High Cost of Screen Real Estate: Lessons from Android Auto
During my time working on Android Auto at Google, one of the most critical aspects of UI development was the extreme scarcity of screen real estate. Car displays come in a bewildering variety of sizes, aspect ratios, and orientations, and often present significantly less usable space than a typical phone or tablet.
Every pixel counted.
Pushing too many elements onto the screen not only led to inevitable clipping and poor user experience but, more importantly, introduced UI clutter. This clutter was a significant concern for driver distraction, directly impacting safety and the overall polish of the interface. We constantly grappled with the challenge of displaying essential information and controls while maintaining a clean, focused, and adaptable layout.
This experience hammered home the vital need for highly dynamic and context-aware layouts — precisely the problem we’re solving today with Jetpack Compose.
🚧 The Challenge: Space Management
Imagine you have a Column of three Text elements. Your requirement is simple: the topmost Text should only be visible if there’s enough vertical space on the screen. If not, only the bottom two should be displayed. This directly mirrors the kind of decisions we had to make for every single UI component in Android Auto.
📏 Here’s a visual example of the problem we’re trying to solve:

How This Was “Easier” in the View System
In the traditional Android View system, achieving this kind of conditional visibility based on available space often felt more straightforward, even if it involved more imperative code.
You would typically:
- Inflate the layout: All views would be inflated regardless of space.
- Measure in
onMeasure: In a customViewGroup‘sonMeasuremethod, you could iteratively measure child views. After measuring a child, you’d check ifgetMeasuredHeight()of that child, plus theyoffset so far, exceeded theheightMeasureSpec(available height). - Conditional Visibility: If a child didn’t fit, you could then set its
visibilitytoView.GONEand re-layout.
The key difference was the direct access to View properties like getMeasuredHeight() and the ability to imperatively set visibility during or after the measure/layout pass. You’d typically just measure all children first, then iterate again to decide which ones to draw or set visible based on the space calculations. This often felt like a more linear process.
In Compose, the world is declarative. We don’t set View.GONE. Instead, we decide whether to compose a Composable at all. This paradigm shift requires understanding Compose’s two-pass layout system and leveraging tools like intrinsic measurements to make informed decisions before composition.
The Problem with Hardcoded Thresholds (Even in Compose)
Even in Compose, an initial thought might lead to BoxWithConstraints and a hardcoded threshold:
| @Composable | |
| fun ConditionalColumnLayoutWithThreshold() { | |
| BoxWithConstraints(modifier = Modifier.fillMaxSize()) { | |
| // This '200.dp' is brittle and doesn't adapt to content changes! | |
| if (maxHeight > 200.dp) { | |
| Column { | |
| Text("Top Element", Modifier.background(Color.Yellow).height(100.dp)) | |
| Text("Middle Element", Modifier.background(Color.Green).height(50.dp)) | |
| Text("Bottom Element", Modifier.background(Color.Blue).height(50.dp)) | |
| } | |
| } else { | |
| Column { | |
| Text("Middle Element", Modifier.background(Color.Green).height(50.dp)) | |
| Text("Bottom Element", Modifier.background(Color.Blue).height(50.dp)) | |
| } | |
| } | |
| } | |
| } |
𝌣 The Solution: Intrinsic Measurements with Layout
Jetpack Compose provides a powerful mechanism called intrinsic measurements. This allows you to ask a composable, “What’s the minimum or maximum width/height you would naturally take given certain constraints?” This query doesn’t perform a full layout pass, making it very efficient.
Combined with the Layout composable, which gives us full control over measuring and placing its children, we can create a highly adaptive solution.
Let’s build a generic Layout composable that can conditionally place any number of children based on available space, from top to bottom.
| import androidx.compose.runtime.Composable | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.layout.Layout | |
| import androidx.compose.ui.layout.Placeable | |
| import androidx.compose.ui.unit.Constraints | |
| import androidx.compose.ui.layout.Measurable | |
| @Composable | |
| fun GenericConditionalLayout( | |
| modifier: Modifier = Modifier, | |
| content: @Composable () -> Unit | |
| ) { | |
| Layout( | |
| content = content, | |
| modifier = modifier | |
| ) { measurables, constraints -> // 'constraints' here are from the parent | |
| var yOffset = 0 // Tracks the current vertical position for placing children | |
| val placedChildren = mutableListOf<Placeable>() // Stores children that will be placed | |
| for (measurable in measurables) { | |
| // Step 1: Query the child's natural height using minIntrinsicHeight | |
| // This tells us the minimum height the child needs to display its content | |
| // given the maximum width it can take. | |
| val childIntrinsicHeight = measurable.minIntrinsicHeight(constraints.maxWidth) | |
| // Step 2: Check if placing this child would exceed the available space | |
| // constraints.maxHeight is the total height allowed by the parent. | |
| if (yOffset + childIntrinsicHeight <= constraints.maxHeight) { | |
| // If it fits: | |
| // Perform a full measure pass for the child using the remaining space. | |
| val placeable = measurable.measure( | |
| constraints.copy(minHeight = 0, maxHeight = constraints.maxHeight - yOffset) | |
| ) | |
| placedChildren.add(placeable) | |
| yOffset += placeable.height // Update offset for the next child | |
| } else { | |
| // If it does NOT fit: | |
| // Stop the loop. No more children will be measured or placed, | |
| // ensuring no overflow. | |
| break | |
| } | |
| } | |
| // Step 3: Determine the size of our custom layout and place the children | |
| layout(constraints.maxWidth, constraints.maxHeight) { | |
| var currentY = 0 | |
| placedChildren.forEach { placeable -> | |
| placeable.place(x = 0, y = currentY) // Place each child at its calculated Y position | |
| currentY += placeable.height // Update Y for the next child | |
| } | |
| } | |
| } | |
| } |
Understanding the Key Concepts
Layout(content, modifier, measurePolicy): This composable is your direct gateway to Compose’s layout system. You provide acontentlambda (your children) and ameasurePolicylambda where all the magic happens.measurables: InsidemeasurePolicy,measurablesis a list of all the children passed to yourLayoutcomposable. EachMeasurablerepresents a child that you can query and measure.constraints: This object is passed down from the parent of yourLayoutcomposable. It defines theminWidth,maxWidth,minHeight, andmaxHeightthat yourLayoutcomposable (and by extension, its children) must adhere to. Crucially,constraints.maxHeighttells us the total vertical space we have available.measurable.minIntrinsicHeight(width): This is the hero of our solution. Instead of giving us the measured height (which changes based on constraints), it tells us the minimum height this child would ideally take if givenwidthand infinite height. It’s a lightweight query that avoids a full layout pass. This is invaluable when working with unknown content sizes.measurable.measure(constraints): Once we know a child fits based on its intrinsic height, we callmeasure()to actually calculate its final size based on the providedconstraints. This returns aPlaceableobject.placeable.place(x, y): After all measurements are done, thelayoutlambda is called. Here, you iterate through yourPlaceableobjects and tell Compose exactly where to draw them on the screen using theirxandycoordinates.
🔨 Putting it to Use
Now, let’s see how incredibly simple it is to use our GenericConditionalLayout:
| import androidx.compose.foundation.background | |
| import androidx.compose.foundation.layout.fillMaxSize | |
| import androidx.compose.foundation.layout.padding | |
| import androidx.compose.material3.Text | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.graphics.Color | |
| import androidx.compose.ui.unit.dp | |
| @Composable | |
| fun ArticleDemoScreen() { | |
| GenericConditionalLayout(modifier = Modifier.fillMaxSize()) { | |
| Text( | |
| text = "This is the first element. It's relatively short.", | |
| modifier = Modifier.background(Color.Yellow).padding(8.dp) | |
| ) | |
| Text( | |
| text = "This is the second element. It's a bit taller with more content and padding.", | |
| modifier = Modifier.background(Color.Green).padding(16.dp) | |
| ) | |
| Text( | |
| text = "This is the third element. A standard line of text.", | |
| modifier = Modifier.background(Color.Blue).padding(8.dp) | |
| ) | |
| Text( | |
| text = "This is the fourth and final element. It has significant padding and more text, making it quite tall. It will only show if all previous elements AND itself fit on screen.", | |
| modifier = Modifier.background(Color.Red).padding(32.dp) | |
| ) | |
| // Add more elements here to test how it adapts | |
| } | |
| } |
And voila! now our Composables only get rendered if there’s enough space for them to be rendered.
Job Offers
✨ Stay in the Loop for More!
If you found this article helpful, you’re in for a treat.
I plan to publish more stories from my time at Google developing for the automotive space and from my experience at Meta working on Orion and the future of Augmented Reality.
To be the first to know when a new article drops, be sure to follow me on Medium and connect with me on LinkedIn.
Thank you for reading!
This article was previously published on proandroiddev.com.



