zsmb.coEst. 2017



How I Finally Memorized Modifier Ordering in Compose

2024-06-04 • Márton Braun

For the longest time, I was a proud member of the “no idea how Modifier ordering works, just try it one way and flip it around if it doesn’t work” club.

I’ve read and watched a bunch of content explaining how constraints are propagated up and down the chain of modifiers and the tree of layout nodes, and… None of that really stuck with me. I would still take a guess every single time I added a modifier, and just move it around if it didn’t immediately have the desired effect.

However, I’ve recently realized that I accidentally learned how Modifier ordering works; perhaps the way I understand it now will help you too.

The modifier parameter for components

I’ve always been fascinated with how Compose makes excellent use of Kotlin’s advanced language features in its API design.

One important guideline in Compose is that well-written @Composable UI component functions should take a modifier: Modifier parameter. This should default to the value Modifier, which represents an empty implementation, applying no customization to the component’s basic looks or behaviour.

@Composable
fun Greeting(
    name: String,
    modifier: Modifier = Modifier, // This one, here!
)

It’s then the component’s responsibility to apply this modifier to the root node of whatever UI it emits:

@Composable
fun Greeting(..., modifier: Modifier = Modifier) {
    Column(modifier) { // ✅ Applied at the root
        // The rest of the component
    }
}

This is often done as shown above, by directly passing modifier to the root node. Components may also choose to add additional modifiers onto this using the usual chaining API, however – and this is the crucial part – they may only chain additional Modifiers after modifier, and never before it. This, for example, is a valid application of the parameter:

@Composable
fun Greeting(..., modifier: Modifier = Modifier) {
    Column(modifier.padding(8.dp)) { // ✅ Chaining after the parameter
        // The rest of the component
    }
}

But this is not valid:

@Composable
fun Greeting(..., modifier: Modifier = Modifier) {
    Column(
        Modifier.padding(8.dp).then(modifier)) // ❌ modifier must come first
    ) {
        // The rest of the component
    }
}

How to remember modifier ordering

So, how does this help us understand modifier ordering?

When you look at a component that has a Column as its root (sticking to our example here) as its user, that component is, essentially, a column of stuff to you. When you pass in a modifier to that Composable, you expect the modifier to be applied to the Column – because that’s what the component is.

If you add padding, the padding should be added around the column. If you add a border, the border should be added around the column. Visually speaking, it makes sense that the component should always be decorated from the outside.

Combining this with our knowledge that the modifier passed into a component always ends up at the beginning of a potential chain of modifiers, we arrive at the following conclusion:

Modifiers are applied last-to-first, inside-to-outside.

The further up the chain a Modifier is, the later it gets applied. The last modifier to be applied is the modifier which sits at the very front of the chain for components, as customization coming from the outside must be applied around what the component already contains.

Show me the code

Let’s say that our Greeting component is implemented like this, with a background colour and a padding chained onto the modifier parameter as it gets applied.

@Composable
fun Greeting(..., modifier: Modifier = Modifier) {
    Column(
        modifier
            .background(Color(0xFFD4B0FF))
            .padding(28.dp),
        ...
    ) {
        // The rest of the component
    }
}

If we call the Greeting function without passing in a modifier, only these two modifiers declared inside the function get applied by default.

Greeting("Kodee")

The complete modifier chain in this case, substituting just Modifier for the parameter is:

Modifier
    .background(Color(0xFFD4B0FF))
    .padding(28.dp)

Reading the modifiers last-to-first, we can imagine that first the padding surrounds the column, and then the background colour fills that space.

This is what our Composable component looks like out-of-the-box, with no customization passed in from the parameters.


Now, let’s add a real modifier parameter, which will end up at the front of the chain, and customize the component from the outside.

Greeting(
    "Kodee",
    modifier = Modifier
        .padding(48.dp)
        .border(4.dp, Color.Magenta)
)

The effective Modifier chain here becomes:

Modifier
    // "External" modifiers, from the parameter
    .padding(48.dp)
    .border(4.dp, Color.Magenta)
    // "Internal" modifiers, within the component
    .background(Color(0xFFD4B0FF))
    .padding(28.dp)

This means that we perform the same steps as before to get the component with its base looks, and then keep going last-to-first. We add the border, and then add some more padding.

Conclusion

That’s all! I hope that this explanation helps you with ordering your modifiers correctly. Remember, if a modifier needs to go around another modifier, it should be in front of it in the modifier chain. Last-to-first, inside-to-outside.

Have fun coding Compose!



You might also like...

The conflation problem of testing StateFlows

StateFlow behaves as a state holder and a Flow of values at the same time. Due to conflation, a collector of a StateFlow might not receive all values that it holds over time. This article covers what that means for your tests.

Wrap-up 2021

Another year over, a new one's almost begun. Here's a brief summary of what I've done in this one.

Compose O'Clock

I started learning Jetpack Compose this week. Two days into that adventure, here's a quick look at how a neat clock design can be built up in Compose, step-by-step.

Building a macOS screen saver in Kotlin

This article documents how I created my own custom screen saver for macOS, built almost entirely in Kotlin, using the powers of Kotlin Multiplatform compiled to macOS. Join me for a tale of Kotlin success and macOS failures.