Constraints and modifier order

Jolanda Verhoef
Android Developers
Published in
8 min readFeb 15, 2023

Episode 3 of MAD Skills: Compose Layouts and Modifiers

In the previous MAD Skills article, you learned about the three phases of Compose that transform data into UI. We created a mental model to help us reason about our app’s design implementation. In this episode, we will use that mental model to learn to reason about modifier chaining and how it influences the sizes of our composables.

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

Remember from last episode that we have three phases of transforming data into UI:

  1. Composition: What to show
  2. Layout: Where to place it
  3. Drawing: How to render it

Modifiers can affect different phases. For example, the size and padding modifier influence the size and spacing of a composable during the layout phase, and the clip modifier influences the shape of a composable during the draw phase:

Flow diagram of Data to Composition to Layout to Drawing to UI. Layout is annotated with size and padding, Drawing is annotated with clip.

We also know that we can add more than one modifier to a composable, creating a chain. However, it is not always apparent how the different modifiers in those chains affect each other.

Try it yourself

Let’s start with some exercises. For each of the following code snippets, try to figure out which option would be the result of executing that snippet, option A or B:

Problem 1

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

Image(
painterResource(R.drawable.frag),
contentDescription = null,
Modifier
.fillMaxSize()
.size(50.dp)
)
Option A shows an image filling the height of the box, centered horizontally. Option B shows a small image, centered in the top left of the box.

Problem 2

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

Image(
painterResource(R.drawable.frag),
contentDescription = null,
Modifier
.fillMaxSize()
.wrapContentSize()
.size(50.dp)
)
Option A shows an image filling the height of the box, centered horizontally. Option B shows a small image, centered in the box.

Problem 3

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

Image(
painterResource(R.drawable.frag),
contentDescription = null,
Modifier
.clip(CircleShape)
.padding(10.dp)
.size(100.dp)
)
Option A shows a round image centered in the box. Option B shows a clipped off round image, centered in the box.

Not exactly sure about the answers? You’ve come to the right place then! Continue reading to understand more. (psst, the answers are at the end of the blog post)

Constraints

To learn how to reason about modifier order, we will have to learn about the role of Constraints during the layout phase.

Remember that in the last article we discussed how the layout phase follows a three-step algorithm to find each layout node’s width, height and x, y coordinate:

  1. Measure children: A node measures its children, if any.
  2. Decide own size: Based on those measurements, a node decides on its own size.
  3. Place children: Each child node is placed relative to a node’s own position.

Constraints help finding the right sizes for our nodes during the first two steps of this algorithm. They are minimum and maximum bounds for a node’s width and height. When the node decides on its size, its measured size should fall in this given size range.

Constraints are passed down from parent to child in the UI tree, during the first step of the algorithm. When a parent node measures its children, it provides these constraints to each child to let them know how big or small they’re allowed to be. Then, when it decides its own size, it also adheres to the constraints that were passed in by its own parents.

Types of constraints

Constraints can be bounded, indicating a minimum and a maximum width and height:

Container with arrows from 100 to 300 horizontally, and 100 to 200 vertically. The area that falls in those bounds is colored.
Bounded constraints

Constraints can also be unbounded, in which case the node is not constrained to any size. The maximum width and height bounds are then set to infinity:

Container that moves off the screen, with arrows from 0 to infinity both horizontally and vertically. The whole area is colored.
Unbounded constraints

Or constraints can be exact, asking the node to follow an exact size requirement. The minimum and maximum bounds are set to the same value:

Container with dots at 300 horizontally, and 200 vertically. The whole area is colored in a light color.
Exact constraints

Of course, combinations of these are also valid, for example bounding the width, while allowing for an unbounded maximum height, or setting an exact width but providing a bounded height:

Combinations of bounded, unbounded, and exact widths and heights

Walk-through of the algorithm

To understand how constraints are passed from parent to child, and how sizes are then resolved based on those constraints, it’s best to walk through an example. However, this is much easier to present in a video format, so I’d suggest you watch the chapter “an example” of the MAD Skills video:

Modifiers and their influence on constraints

By watching the video you should have a good understanding of how constraints affect the size of composables, and how modifiers affect those constraints. In this section we’ll take a closer look at some specific modifiers and how they impact constraints.

size modifier

Let’s look at the following UI tree, that should be rendered in a container of 300dp by 200dp. The constraints are bounded, allowing widths between 100dp and 300dp, and heights between 100dp and 200dp.

UI tree showing incoming constraints of 100 to 300 width, 100 to 200 height. Shows wrapper modifier node called size without specifying the size. The node wraps a generic layout node.

The size modifier adapts the incoming constraints to match the value passed to it, for example150dp:

Size is set to 150 and exact constraints of 150 by 150 are shown in a container.

But what if the requested size is too small or too big? That is, what if the width and height are smaller than the smallest constraint bound, or larger than the largest constraint bound?

Size of 50 and size of 400 are set in the wrapper modifier node. The containers show how these sized do not fall in the constraints bounds.

In this case, the modifier will try to match the passed constraints as closely as it can, while still adhering to the constraints passed in:

The containers show how the small size resolves to fixed constraints of 100 by 100, and the large size resolves to fixed constraints of 300 by 200.

This also explains why chaining multiple size modifiers doesn’t work. The first size modifier will set both the minimum and maximum constraints to a fixed value, and even though the second size modifier requests a smaller or larger size, it still needs to adhere to the exact bounds passed in, so it will not override those values:

size modifier with size 100 wrapping size modifier with size 50, showing how this still resolves in a size of 100, as the second modifier needs to adhere to the minimum bounds set by the first modfier.

requiredSize modifier

If you do need your node to override the incoming constraints, you can replace the size modifier with another modifier called requiredSize. It will replace the incoming constraints and pass the size you specify instead, as exact bounds. Then, when the size is passed back up the tree, the child node will be centered in the available space:

Size modifier of 100 wraps requiredSize modifier of 50. Constraints are updated to exact constraints of 100, then set to exact constraints of 50. The layout reports a size of 50, but the requiredSize modifier reports the original size of 100. It centers the content in the available space.

width and height modifiers

In previous examples we used a size modifier, that adapts both width and height of the constraints. However, we can also replace these with the width modifier, that sets a fixed width but leaves the height undecided. Or we can use the height modifier, that sets a fixed height but leaves the width undecided:

sizeIn modifier

If you need fine-grained control over the constraints, and want to adapt them to your exact needs, you can use the sizeIn modifier:

Exercise answers

Now that we learned about constraints and how they influence measurements, let’s return to our original use cases and find the right solutions.

Problem 1

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

Image(
painterResource(R.drawable.frag),
contentDescription = null,
Modifier
.fillMaxSize()
.size(50.dp)
)

Here’s the solution:

Option A was right!
  • The fillMaxSize modifier changes the constraints to set both the minimum width and height to the maximum value — 300dp in width, and 200dp in height.
  • So even though the size modifier wants to use a size of 50dp, it still needs to adhere to the incoming minimum constraints. And thus the size modifier will also output the exact constraint bounds of 300 by 200, effectively ignoring the value provided in the size modifier.
  • The Image follows these bounds and reports a size of 300 by 200, which is passed all the way up.

Problem 2

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

Image(
painterResource(R.drawable.frag),
contentDescription = null,
Modifier
.fillMaxSize()
.wrapContentSize()
.size(50.dp)
)

Here’s the solution:

Option B was right!
  • The fillMaxSize modifier will still behave the same, and adapt the constraints to set both the minimum width and height to the maximum value — 300dp in width, and 200dp in height.
  • The wrapContentSize modifier resets the minimum constraints. So while fillMaxSize resulted in fixed constraints, wrapContentSize resets it back to bounded constraints. The following node can now take up the whole space again, or be smaller than the entire space.
  • The size modifier sets the constraints to minimum and maximum bounds of 50.
  • The Image resolves to a size of 50 by 50, and the size modifier forwards that.
  • The wrapContentSize modifier has a special property. It takes its child, and puts it in the center of the available minimum bounds that were passed to it. The size it communicates to its parents is thus equal to the minimum bounds that were passed into it.

By combining just three modifiers, we were able to define a size for our composable and center it in its parent!

Problem 3

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

Image(
painterResource(R.drawable.frag),
contentDescription = null,
Modifier
.clip(CircleShape)
.padding(10.dp)
.size(100.dp)
)

Here’s the solution:

Option B was right!
  • The clip modifier does not change the constraints.
  • The padding modifier lowers the maximum constraints.
  • The size modifier sets all constraints to 100dp.
  • The Image adheres to those constraints and reports a size of 100 by 100dp.
  • The padding modifier adds 10dp on all sizes, so it increases the reported width and height by 20dp.
  • Now in the drawing phase, the clip modifier acts on a canvas of 120 by 120dp. Thus it creates a circle mask of that size.
  • The padding modifier then insets its content by 10dp on all sizes, so it lowers the canvas size to 100 by 100dp.
  • The Image is then drawn in that canvas. You can see that the image is clipped based on the original circle of 120dp, and thus we see a non-round result.

Wrapping up

Wow, that was a lot! You learned about constraints, and used them to reason about modifiers, how to order them, and measurements.

In the next post we will show you how you can use this information to start implementing your own custom layout.

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

--

--