article banner (priority)

How modifiers order affects Compose UI appearance

Some time ago I published a game, where the task was to predict what is the output of a certain modifier order in Jetpack Compose. To my surprise, I received a lot of feedback that people have trouble scoring any points. Developers do not understand how modifiers work! People just try them in different configurations until they achieve what they need. That is not a good recipe for an effective and reliable development.

This topic does not only deserve a good explanation but also needs it. Documentation is useful and clear, but far from covering this topic completely. Just the opposite, after reading documentation, I had a feeling that it only scratches the surface. The articles I found on the internet are not better, and many of them are plainly wrong. They offered shallow explanations that might help in some cases, while in others they were misleading.

That is why I decided to write this article, where I want to explain clearly how modifiers work. On the way, you will also learn how to dominate my game. That is not a short read, but once understood, modifiers get very intuitive. I hope after reading this article, you will reach this point, and modifiers will no longer be a mystery for you.

Order matters

Order of operators makes a significant difference. For instance, if you use background before padding, the background will be drawn behind this padding as well, but if you use them in the opposite order, the background will be drawn only over the area inside the padding. The same with other area-related modifiers, like border, clickable or clip.

In my game, the possible answers are different images that are the result of applying the same modifiers in different orders. Just see how different those results can be.

There is a lot of confusion about in which order modifiers are applied. First of all, modifiers are not applied from bottom to top. It can sometimes be considered as a useful metaphor, but it is not how it works. It is better to say that modifiers are applied from top to the bottom, but to be precise, they are first applied from top-to-bottom then bottom-to-top order in the layout phase, and then in top-to-bottom order in the drawing phase. The documentation explains that quite clearly. That is also why when we chain modifiers, we use function then.

Let's see an example. If you use padding and background with different colors alternately, you will achieve a beautiful rainbow. Why? background draws color behind our component. padding enforces a certain amount of space around, making its internal smaller. So we draw red, make internal smaller, draw green, make internal smaller, draw blue, and make internal smaller. In result, the image size is only 40px (100 - 3 * 2 * 10), and it is surrounded with blue, green and red borders, each 10px wide.

A situation looks slightly different when space is unconstrained. Padding does not make the image smaller, it adds space around it, and the Image composable gets bigger. That might lead to a conclusion that modifiers are applied from bottom to top, because it seems illogical that adding a padding after the background not only adds a padding, but also draws a background behind this padding. However, that is not the case. Modifiers are applied from top to the bottom, but in two phases: first in the layout phase, and then in the drawing phase. In the layout phase padding decides that the image should be bigger, with smaller internal image, and in the drawing phase background draws a color behind the whole area. This can be applied repeatedly, and the result will be a bigger and bigger image with a rainbow border.

By the way, if you define custom composable with Modifier parameter, it should be used first for the topmost composable particularly because it should be applied first. The first modifier has the highest control over those below, just like the topmost composable has the most control over those below.

How modifiers are applied

Compose renders a frame through three phases:

  • Composition: Where our composable functions are called, so also modifier builders are called, and passed as arguments to composables.
  • Layout: Where Compose calculates where each composable should be located, and what should be its size. In this phase modifiers like layout, offset, padding or size are used.
  • Drawing: Where each composable is drawn. In this phase modifiers like background or border are used.

It is important to understand that modifiers are not like "parameters" to composables. If they were, we couldn't see rainbow in the previous example, all paddings would be applied in the layout phase, and then the background would be drawn behind the picture in the drawing phase. We can see them because modifiers are rather "decorating" composable.

The same argument applies to all other modifiers, if we use a modifier that works on composable area, like background, clickable or clip, it typically makes a difference if they are before or after modifiers that change the size or offset of the composable. If they are before, they will be applied to the area before, if they are after, they will be applied to the area after (smaller in case of padding, moved in case of offset).

It can be seen a bit like each modifier is like another composable that wraps over the composable it is used on. That is just a metaphor, but a useful one, and in some regards quite close to the truth.

Like in our rainbow example, background is like a composable that draws red square in the background, and padding is like a composable that enforces padding, and thus constraining its internal composable to be smaller and to have an offset.

A similar story with clickable. It is like a background, but instead of drawing a color, it makes a certain area clickable. That is why it makes such a difference if it is before or after padding. If it is before, it makes the whole area clickable, if it is after, it makes only the area inside padding clickable.

So why is our component bigger when we use padding and it is not when constrained? To understand that, we need to learn how composables layout is calculated in the lauout phase. That is a very important topic, not only in understanding modifiers, but in general in using Compose effectively.

Modifiers and layout

In the layout phase, for each composable we must calculate its size (height and width) and position (x and y). Calculating size is harder. How big an element is? Generally, each content composable has its preferred size. For Text, it is this is how much this text takes. For Image it is the size of used image. This is how much this composable will take if it is not constrained. Constraints can make it bigger, for instance when we use fillMaxWidth or fillMaxHeight. Constraints can also make it smaller, for instance when we use padding or size. Different content composables (by that I mean the leaf composables, non-layout composables) behave differently when constrained. For instance, Text will try to fit all text in the given width, and if it cannot, it will cut it. Image will try to fit the image in the given size, and if it cannot, it will scale it. Layout composables (like Row or Box), on the other hand, typically take as much space as their children need.

Constraints are represented as a range of possible width and height. In the layout phase, constraints are propagated down the tree, and on the way get modified by modifiers. Once they reach the content composable, it should respect those constraints and adjust its size accordingly. Once their actual size is known, it gets propagated up the tree, and the parent composable can decide what is their size.

In our rainbow border example, each padding adds padding and modifies constraints by making them smaller. So 0-100dp constraints are modified to 0-80dp, 0-60dp, and to 0-40dp, so in the end, the image (that is bigger than 40dp) decides to take as much space as possible, which is 40dp.

However, if we start with much bigger constraints, let's hypothetically say 0-Inf, padding will not make the image smaller, so visually the composable will get bigger and bigger with every padding. The image will take as much space as it needs. Let's say its size is 100dp, so the whole composable will take 160dp of space.

Let's see a more complicated example. What if size is used the middle of complex modifier chain? size restricts constraints to the closest possible value allowed by the previously received constraints. In the below example, it means that the image will have size 60dp, because since size(100.dp) two times padding(10.dp) was applied, and each time constraint was reduced by 20dp. The whole composable will take 120dp of size, because there is one padding above the size.

Notice that this also means that using methods like size more than once takes no effect. Only the first one will be applied, because it narrows constraints to the closest possible allowed by the previously received constraints, and since then constraint is not flexible anymore. There are functions that force constraints to be of a certain value, like requiredSize, but they are not used in typical cases.

To learn more about constraints and how they are propagated, read this page from documentation.

Clipping behavior

One of the most confusing modifiers is clip. It limits the space where the composable can be drawn. Everything that is outside the clip area is not drawn. You can imagine it as a mask that is applied to the composable.

That can lead to crazy outputs when clip is used with other modifiers.

Understanding modifiers

Now you know how modifiers work. The only thing that you need to dominate the game is to understand how each modifier works. Here is their brief description:

  • size - Narrows constraints to the closest possible allowed by the previously received constraints.
  • padding - Makes internal which is smaller and has an offset. It also modifies constraints by making them smaller.
  • offset - Makes internal with an offset.
  • background - Draws color behind our component.
  • fillMaxWidth, fillMaxHeight, fillMaxSize - Narrows appropriate constraints to the maximum possible value.
  • clip - Limits the space where the composable can be drawn. Everything that is outside of the clip area is not drawn.
  • border - Draws border around the composable, and clips it to prevent drawing on the border.
  • align is a slightly different story, as it enforces position in the Box composable. It located the result composable in the given position (it is like changing offset).

Notice that if you use background twice, the second color will be drawn over the first one. If you use border twice, the first border will be seen, because the second one will be clipped.

Here is an example of how using them together can lead to interesting results:

Summary

  • Modifiers are applied from top to the bottom.
  • Modifiers are not like "parameters" to composable, but rather "decorating" composable. You can see them like another composable that wraps over the composable it is used on.
  • In the layout phase, constraints are propagated down the tree, and on the way get modified by modifiers. Once they reach the content composable, it should respect those constraints and adjust its size accordingly. Once their actual size is known, it gets propagated up the tree, and the parent composable can decide what is their size.
  • To understand how modifiers work, you need to understand how each modifier works. The behavior differs, for instance background draws color behind our component, padding makes internal smaller and with an offset, clip limits the space where the composable can be drawn, and border draws border around the composable, and clips it to prevent drawing on the border. That means using background twice will draw the second color over the first one, and using border twice will draw the first border, because the second one will be clipped.

With that in mind, you should be able to dominate Modifier Order Guessing Game. Remember to share your amazing score on social media!

More...

If you want to learn more about Jetpack Compose, check out my new workshops:

  • Compose Essentials - a workshop for Compose novices, willing to learn the basics of Compose.
  • Advanced Compose - a workshop for Compose developers, willing to better understand Compose and learn its more advanced features.
  • Recomposition Master - a workshop for Compose developers, willing to understand composition and recomposition, and learning how to use is safely and effectively.