Advanced Layout concepts

Simona Milanović
Android Developers
Published in
15 min readFeb 22, 2023

--

Episode 4 of MAD Skills: Compose Layouts and Modifiers

Welcome back to the MAD Skills series on Jetpack Compose layouts and modifiers! In the previous episode, we talked about the Layout phase of Compose to explain how modifier chaining order and incoming parent constraints impact the composable they’re passed to.

In today’s episode, we zoom in even more on the Layout phase and constraints and cover them from another perspective — how to harness their power to build custom layouts in Compose.

To build custom layouts, we’ll go over what the Layout phase is capable of, how to enter it and how to use its sub-phases — measurement and placement — to your advantage for building flexible, custom layouts.

After that, we’ll cover two important, rule-defying Compose APIs: SubcomposeLayout, and Intrinsic measurements, as the last two missing pieces of the Layouts puzzle. These concepts will provide you with additional knowledge to build intricate designs with very specific requirements in Compose.

You can also watch this article as a MAD Skills video:

All of Compose’s Layouts

In the previous episodes, we talked about how Compose transforms data into UI via its three phases: Composition, Layout and Draw, or “what” to show, “where” to place it, and “how” to render it.

But as the name of our series suggests, we’re mostly interested in the Layout phase.

However, the term “Layout” in Compose is used for, well, a lot of different things and might seem confusing because of its many meanings. So far in the series, we’ve learned the following usages:

  • Layout phase: one of three phases of Compose, in which a parent layout defines the sizing and the positioning of its child elements
  • A layout: a broad, abstract term used to quickly define any UI element in Compose
  • A layout node: an abstract concept used as a visual representation of one element in the UI tree, created as a result of the Composition phase in Compose

In this episode, we’ll also get to learn a few additional meanings to complete the whole Layout circle. Let’s quickly break them down first — a more in depth explanation of these terms awaits further down in the post:

  • Layout composable: the composable used as the core component of Compose UI. When called during Composition, it creates and adds a layout node in the Compose UI tree; the basis for all higher level layouts like Column, Row, etc.
  • layout() function — the starting point of placement, which is the second sub-step of the Layout phase and takes care of placing children in a Layout composable, right after the first sub-step of measurement
  • .layout() modifier — a modifier that wraps one single layout node and allows sizing and placing it individually, instead of this being done by its parent layout

Now that we know what’s what, let’s start with the Layout phase and zoom in on it. As mentioned, during the Layout phase, each element in the UI tree measures its children, if any, and places them in the available 2D space.

Every out-of-the-box layout in Compose, such as Row, Column, and many more, handle all of this for you automatically.

But what if your designs require a non-standard layout, for which you need to go custom and build your own layout, like this TimeGraph from our JetLagged sample?

That’s precisely when you need to know more about the Layout phase — how to enter it and how to use its sub-phases of child measurement and placement to your advantage. So, let’s take a look at how we can build a custom layout in Compose from a given design!

Enter the Layout phase matrix

Let’s go over and explain the most important, basic steps for building a custom layout. However, if you wish to follow a detailed, step by step video guide on how and when to create a custom layout for a real life, intricate app design, please take a look at the Custom layouts and graphics in Compose video or directly explore the TimeGraph custom layout from our JetLagged sample.

Calling the Layout composable is the starting point of both the Layout phase and building a custom layout:

@Composable
fun CustomLayout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Layout() { … }
}

The Layout composable is the main protagonist of the Layout phase in Compose and the core component of the Compose layout system:

@Composable inline fun Layout(
content: @Composable @UiComposable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
// …
}

It accepts a composable content as its children, and a measure policy for measuring and positioning its elements. All higher-level layouts, like Column and Row, use this composable under the hood.

Layout composable currently has three overloads:

  • Layout — for measuring and placing 0 or more children, which accepts one composable as content
  • Layout — for leaf nodes of the UI tree with precisely 0 children, so it doesn’t have a content parameter
  • Layout — accepts a contents list for passing multiple different composables

Once we enter the Layout phase, we see that it consists of two steps, measurement and placement, in that specific order:

@Composable
fun CustomLayout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Layout(
content = content,
modifier = modifier,
measurePolicy = { measurables, constraints ->
// 1. Measurement step
// Determine sizes of components
layout(…) {
// 2. Placement step
// Determine positions of components
}
}
)
}

Sizes of child elements are calculated during the measure pass, and their positions during the placement pass. The order of these steps is enforced with Kotlin DSL scopes which are nested in a way that prevents placing something that hasn’t first been measured or doing placement in the measurement scope:

@Composable
fun CustomLayout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Layout(
content = content,
modifier = modifier,
measurePolicy = { measurables, constraints ->
// MEASUREMENT SCOPE
// 1. Measurement step
// Determine sizes of components
layout(…) {
// PLACEMENT SCOPE
// 2. Placement step
// Determine positions of components
}
}
)
}

During measurement, the content of the layout can be accessed as measurables, or components that are ready to be measured. Inside Layout, measurables come as a list by default:

@Composable
fun CustomLayout(
content: @Composable () -> Unit,
// …
) {
Layout(
content = content,
modifier = modifier
measurePolicy = { measurables: List<Measurable>, constraints: Constraints ->
// MEASUREMENT SCOPE
// 1. Measurement step
// Determine sizes of components
}
)
}

Depending on the requirements of your custom layout, you could either take this list and measure every item with the same incoming constraints, to keep its original, predefined size:

@Composable
fun CustomLayout(
content: @Composable () -> Unit,
// …
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
// MEASUREMENT SCOPE
// 1. Measurement step
measurables.map { measurable ->
measurable.measure(constraints)
}
}
}

Or tweak its measurements as needed — by copying constraints you wish to keep and overriding the ones you wish to change:

@Composable
fun CustomLayout(
content: @Composable () -> Unit,
// …
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
// MEASUREMENT SCOPE
// 1. Measurement step
measurables.map { measurable ->
measurable.measure(
constraints.copy(
minWidth = newWidth,
maxWidth = newWidth
)
)
}
}
}

We’ve seen in the previous episode that constraints are passed down from parent to child in the UI tree, during the Layout phase. When a parent node measures its children, it provides these constraints to each child to let them know what’s the minimum and the maximum size they’re allowed to be.

A very important feature of the Layout phase is the single pass measurement. This means that a layout element may not measure any of its children more than once. Single-pass measurement is good for performance, allowing Compose to efficiently handle deep UI trees.

Measuring a list of measurables would in return give a list of placeables, or a component that is now ready to be placed:

@Composable
fun CustomLayout(
content: @Composable () -> Unit,
// …
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
// MEASUREMENT SCOPE
// 1. Measurement step
val placeables = measurables.map { measurable ->
// Returns a placeable
measurable.measure(constraints)
}
}
}

The placement step starts by calling the layout() function and entering the placement scope. At this point, the parent layout will be able to decide on its own size (totalWidth, totalHeight), for example, summing up the widths and heights of its child placeables:

@Composable
fun CustomLayout(
// …
) {
Layout(
// …
) {
// totalWidth could be the sum of all children's widths
// totalHeight could be the sum of all children's heights
layout(totalWidth, totalHeight) {
// PLACEMENT SCOPE
// 2. Placement step
}
}
}

Placement scope now allows us to use all placeables that came as a result of measurement:

@Composable
fun CustomLayout(
// …
) {
Layout(
// …
) {
// …
layout(totalWidth, totalHeight) {
// PLACEMENT SCOPE
// 2. Placement step
placeables // PLACE US! 😎
}
}
}

To start placing children, we need their starting x and y coordinates. Once we define where we want the children to be positioned, we call place() to conclude with the placement pass:

@Composable
fun CustomLayout(
// …
) {
Layout(
// …
) {
// …
layout(totalWidth, totalHeight) {
// PLACEMENT SCOPE
// 2. Placement step
placeables.map { it.place(xPosition, yPosition) }
}
}
}

And with that, we end the placement step, as well as the Layout phase! Your custom layout is now ready to be used and reused.

.layout() modifier for all the single elements out there

Using the Layout composable to create a custom layout enables you to manipulate all children elements and manually control their sizing and positioning. However, there are cases where creating a custom layout just for controlling one specific element is an overkill and not necessary.

In these cases, rather than using custom layouts, Compose offers a better and simpler solution — the .layout()modifier, which allows you to measure and lay out just one, wrapped element.

Let’s look at an example where a UI element is being squashed by its parent in a way that we don’t really like:

We want just one Element in this simple Column to have more width than the parent is enforcing by removing the surrounding 40.dp padding for it, for example, to achieve edge to edge appearance:

@Composable
fun LayoutModifierExample() {
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray)
.padding(40.dp)
) {
Element()
Element()
// Item below should rebel against the enforced padding and go edge to edge
Element()
Element()
}
}

To make the third element control itself and remove the enforced padding, we set a .layout()modifier on it.

The way it works is very similar to the Layout composable. It accepts a lambda that grants you access to the element you’re measuring, passed as a single measurable, and the composable’s incoming constraints from the parent. You then use it to modify how a single, wrapped element is measured and laid out:

Modifier.layout { measurable, constraints ->
// Measurement
val placeable = measurable.measure(...)

layout(placeable.width, placeable.height) {
// Placement
placeable.place(...)
}
}

Back to our example — we then change this Element’s maximum width in the measurement step to add an extra 80.dps:

Element(modifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(
constraints.copy(
// Resize this item's maxWidth by adding DPs to incoming constraints
maxWidth = constraints.maxWidth + 80.dp.roundToPx()
)
)
layout(placeable.width, placeable.height) {
// Place this item in the original position
placeable.place(0, 0)
}
})

As we said previously, one of the strengths of Compose is that you can choose your own path when problem solving, as there are usually multiple ways to achieve the same thing. If you knew the exact, static size you want for this Element, another approach could be to set a .requiredWidth() modifier on it, so that the incoming constraints in the parent layout don’t override its set width and instead respect it. In contrast, use of regular .width() modifier would have the set width overridden by the parent layout and incoming constraints in the measurement phase.

SubcomposeLayout — breaking the Compose phases rule

In earlier episodes, we talked about the phases of Compose and the rule of their precise ordering: 1. Composition, 2. Layout, and 3. Drawing. The Layout phase subsequently breaks down to measurement and placement sub-phases. While this applies to the vast majority of Layout composables, there is one rule-breaking layout that doesn’t follow this schema, but with a good reason — SubcomposeLayout.

Think about the following use case — you’re building a list of a thousand items that simply cannot fit on the screen all at the same time. In that case, composing all these child items would be an unnecessary waste of resources — why compose so many items up front if the majority of them cannot even be seen?

Instead, a better approach would be to 1. measure children to obtain their sizes, then based on that, 2. calculate how many items can fit the available viewport and finally, compose only items that would be visible.

This is one of the main ideas behind SubcomposeLayout — it needs to do the measurement pass first for some or all child composables and then secondly, to use that information to determine whether to compose some or all children.

This is precisely why Lazy components are built on top of SubcomposeLayout, which enables them to add content on demand while scrolling.

SubcomposeLayout defers the Composition phase until the Layout phase, so that the composition or execution of some child composables can be postponed until the parent layout has more information — for example, the sizes of its child composables. Meaning, the measurement step in the Layout phase needs to happen before the Composition phase.

BoxWithConstraints also uses SubcomposeLayout under the hood, but this use case is slightly different — BoxWithConstraints allows you to obtain the constraints passed by the parent and use them in the deferred Composition phase, as constraints are only known in the Layout phase measurement step:

BoxWithConstraints {
// maxHeight is the measurement info available only in BoxWithConstraints,
// due to the deferred Composition phase happening AFTER Layout phase measurement
if (maxHeight < 300.dp) {
SmallImage()
} else {
BigImage()
}
}

SubcomposeLayout DON’Ts

As SubcomposeLayout changes the usual flow of Compose phases to allow for dynamic execution, there are certain costs and limitations when it comes to performance. Therefore, it’s very important to understand when SubcomposeLayout should be used and when it’s not needed.

A good, quick way of knowing when you might need SubcomposeLayout is when at least one child composable’s Composition phase depends on the result of another child composable’s measurement. We’ve seen valid use cases for this in Lazy components and BoxWithConstraints.

If however, you just need one child’s measurement to measure other children, you can do so with a regular Layout composable. This way, you can still measure items separately, depending on each other’s result — you just cannot change their composition.

Intrinsic measurements — breaking the single pass measurement rule

The second rule of Compose we previously mentioned is the single pass measurement in Layout phase, which helps greatly with overall performance of this step and the layout system in general. Think about the amount of recompositions that can happen in a short time and how limiting the measurement of the entire UI tree for each recomposition can improve the overall speed!

Traversing a tree with lots of UI nodes for each recomposition

However, there are use cases when the parent layout needs to know some information about its children before measuring them, so that it could use this information for defining and passing down constraints. And this is precisely what Intrinsic measurements do — they let you query children before they’re measured.

Let’s look at the following example — we want this Column items to have the same width, or to be more precise, have each item’s width as that of the widest child (in our case, the “And Modifiers” item). But, we also want that child to take as much width as it needs. So our first step is:

@Composable
fun IntrinsicExample() {
Column() {
Text(text = "MAD")
Text(text = "Skills")
Text(text = "Layouts")
Text(text = "And Modifiers")
}
}

However, we can see that this isn’t enough. Each item takes only the space it requires. We can try out the following:

@Composable
fun IntrinsicExample() {
Column() {
Text(text = "MAD", Modifier.fillMaxWidth())
Text(text = "Skills", Modifier.fillMaxWidth())
Text(text = "Layouts", Modifier.fillMaxWidth())
Text(text = "And Modifiers", Modifier.fillMaxWidth())
}
}

However, this expands every item and the parent Column to the maximum width available on the screen. Remember, we want the width of the widest item for all items. So, as you can tell, we’re aiming to use Intrinsics here:

@Composable
fun IntrinsicExample() {
Column(Modifier.width(IntrinsicSize.Max)) {
Text(text = "MAD", Modifier.fillMaxWidth())
Text(text = "Skills", Modifier.fillMaxWidth())
Text(text = "Layouts", Modifier.fillMaxWidth())
Text(text = "And Modifiers", Modifier.fillMaxWidth())
}
}

By using IntrinsicSize.Max on the parent Column, we’re querying its children and asking “What is the maximum width you need to display all of your content properly?”. Since we’re displaying text and the phrase “And Modifiers” is the longest, it will define Column’s width.

Once the intrinsic size is determined, it is used to set the size — in this case, the width — of the Column and the remaining children can then fill that width.

Conversely, if we use IntrinsicSize.Min, the question will be “What is the minimum width you need to display all of your content properly?” In the case of text, the minimum intrinsic width is the one that has one word on each line:

@Composable
fun IntrinsicExample() {
Column(Modifier.width(IntrinsicSize.Min) {
Text(text = "MAD", Modifier.fillMaxWidth())
Text(text = "Skills", Modifier.fillMaxWidth())
Text(text = "Layouts", Modifier.fillMaxWidth())
Text(text = "And Modifiers", Modifier.fillMaxWidth())
}
}

To quickly summarize all of the intrinsic combinations available:

  • Modifier.width(IntrinsicSize.Min) — “What’s the minimum width you need to display your content properly?”
  • Modifier.width(IntrinsicSize.Max) — “What’s the maximum width you need to display your content properly?”
  • Modifier.height(IntrinsicSize.Min) — “What’s the minimum height you need to display your content properly?”
  • Modifier.height(IntrinsicSize.Max) — “What’s the maximum height you need to display your content properly?”

However, Intrinsic measurements don’t really measure the children twice. Instead, they do a different kind of calculation — you can think of it as a pre-measure step without requiring exponential measurement time, as it is cheaper and easier. So while this doesn’t exactly break the single measurement rule, it does bend it a little bit and shows a Compose requirement that falls outside of the usual ones.

When creating a custom layout, Intrinsics provide a default implementation based on approximations. However, in some cases, the default calculation might not work for you as intended, so the API provides a way of overriding these defaults.

To specify the Intrinsic measurements of your custom layout, you can override the minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, and maxIntrinsicHeight of the MeasurePolicy interface during the measurement pass:

    Layout(
modifier = modifier,
content = content,
measurePolicy = object : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
// Measure and layout here
}

override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
): Int {
// Logic for calculating custom maxIntrinsicHeight here
}

// Other intrinsics related methods have a default value,
// you can override only the methods that you need.
}
)

And that’s a wrap 🎬

We have covered a lot today — all the various meanings of the term “Layout” and how they relate to one another, how to enter and control the Layout phase to your advantage when building custom layouts, and then we concluded up with SubcomposeLayout and Intrinsic measurements as the additional APIs for achieving very specific layout behaviors.

And with this, we conclude our MAD Skills Compose Layouts and Modifiers series! From the very basics of Layouts and Modifiers, simple and powerful provided Compose layouts, Compose phases, to advanced concepts such as modifier chaining order and subcomposition in just a few episodes — congrats, you’ve come a long way!

We hope you’ve learned new things about Compose, renewed old knowledge and most importantly — that you feel much more prepared and confident to migrate E V E R Y T H I N G to Compose 😀.

This blog post is part of a series:

Episode 1: Fundamentals of Compose layouts and modifiers
Episode 2: Compose phases
Episode 3: Constraints and modifier order
Episode 4: Advanced Layout concepts

--

--

Simona Milanović
Android Developers

Android Developer Relations Engineer @Google, working on Jetpack Compose