Blog Infos
Author
Published
Topics
, , , ,
Published

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 clutterThis 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:

Visualizing the problem: Will the topmost Composable fit?

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:

  1. Inflate the layout: All views would be inflated regardless of space.
  2. Measure in onMeasure: In a custom ViewGroup‘s onMeasure method, you could iteratively measure child views. After measuring a child, you’d check if getMeasuredHeight() of that child, plus the y offset so far, exceeded the heightMeasureSpec (available height).
  3. Conditional Visibility: If a child didn’t fit, you could then set its visibility to View.GONE and 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
  1. Layout(content, modifier, measurePolicy): This composable is your direct gateway to Compose’s layout system. You provide a content lambda (your children) and a measurePolicy lambda where all the magic happens.
  2. measurables: Inside measurePolicymeasurables is a list of all the children passed to your Layout composable. Each Measurable represents a child that you can query and measure.
  3. constraints: This object is passed down from the parent of your Layout composable. It defines the minWidthmaxWidthminHeight, and maxHeight that your Layout composable (and by extension, its children) must adhere to. Crucially, constraints.maxHeight tells us the total vertical space we have available.
  4. 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 given width and infinite height. It’s a lightweight query that avoids a full layout pass. This is invaluable when working with unknown content sizes.
  5. measurable.measure(constraints): Once we know a child fits based on its intrinsic height, we call measure() to actually calculate its final size based on the provided constraints. This returns a Placeable object.
  6. placeable.place(x, y): After all measurements are done, the layout lambda is called. Here, you iterate through your Placeable objects and tell Compose exactly where to draw them on the screen using their x and y coordinates.
🔨 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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Reimagining Android Dialogs with Jetpack Compose

Traditional Android dialogs are hard to test, easy to leak, and painful to customize — and in a world of Compose-first apps, they’re overdue for an upgrade.
Watch Video

Reimagining Android Dialogs with Jetpack Compose

Keith Abdulla
Staff Android Engineer, Design Systems
Block

Reimagining Android Dialogs with Jetpack Compose

Keith Abdulla
Staff Android Engine ...
Block

Reimagining Android Dialogs with Jetpack Compose

Keith Abdulla
Staff Android Engineer, D ...
Block

Jobs

✨ 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.

Menu