Jetpack Compose - Order of Modifiers
Asked Answered
L

5

40

Documentation says that Modifiers are applied from the left. But from this example it looks like they are applied from the right: First border and then padding because there is no space between text and border

Text("Hi there!", Modifier.padding(10.dp).border(2.dp, Color.Magenta))

enter image description here

Letendre answered 5/10, 2020 at 10:14 Comment(1)
But if I look at the screenshot, I see 10 pixels of padding, and then 2 pixels of border, which sounds like left to right.Conceptionconceptual
L
17
  • In Android Compose resulting Image is being constructed from the outside layer toward the Composable in the center. This means that first defined Green border is outer border and the last defined Red border is inner border . This is very confusing since Green Modifier that is closest to Text Composable in the Code is furthest from it in the result.
  • This is in contrast to SwiftUI where Modifiers appear in the same order both in the Code and in the resulting Image. Modifier that is closest to the Composable in the Code is also closest to it in the resulting Image.
  • If you want to imagine that resulting Image is being constructed from the center where your Composable is positioned (like in SwiftUI) then Modifiers are applied in the opposite order from which they are given (from the bottom upward).
  • So if you have Text Composable with two border Modifiers
    • border Modifier that is furthest away from the Text Composable in the Code (the bottom Red one)
    • will be closest to the Text Composable in the resulting Image
  • Modifiers are applied from outer toward inner layer
    • Applying .border(2.dp, Color.Green) to the outmost layer
    • Applying .padding(50.dp) going inward
    • Applying .border(2.dp, Color.Red) to the innermost layer
package com.example.myapplication

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.unit.dp

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      Text("Hi there!",
        Modifier
          .border(2.dp, Color.Green)
          .padding(50.dp)
          .border(2.dp, Color.Red)
      )
    }
  }
}

enter image description here

Letendre answered 6/10, 2020 at 11:19 Comment(1)
This is unintuitive API. What you would expect here is first border to be applied to the content and the second to padded contentEdible
S
112

There’s Layouts in Jetpack Compose codelab containing Layout modifiers under the hood step which explains the modifier order, see "Order matters" section.

order matters when chaining modifiers as they're applied to the composable they modify from earlier to later, meaning that the measurement and layout of the modifiers on the left will affect the modifier on the right. The final size of the composable depends on all modifiers passed as a parameter. First, modifiers will update the constraints from left to right, and then, they return back the size from right to left.

To understand it better I'd recommend to figure out how layouts work in Compose. In short, padding() is a LayoutModifer, it takes in some constraints, measures its child size based on a projection of that constraints and places the child at some coordinates.

Let’s see an example:

Box(
  modifier = Modifier
    .border(1.dp, Color.Red)
    .size(32.dp)
    .padding(8.dp)
    .border(1.dp, Color.Blue)
)

And the result:

enter image description here

But let's swap the .size() and the .padding()

Box(
  modifier = Modifier
    .border(1.dp, Color.Red)
    .padding(8.dp)
    .size(32.dp)
    .border(1.dp, Color.Blue)
)

Now we have a different result:

enter image description here

I hope this sample helps you to figure out how the modifiers are applied.

One can expect that the red border should be the closest to the box since it was added first, so the order might seem reversed, but such an order has pros too. Let’s take a look at this composable:

@Composable
fun MyFancyButton(modifier: Modifier = Modifier) {
  Text(
    text = "Ok",
    modifier = modifier
      .clickable(onClick = { /*do something*/ })
      .background(Color.Blue, RoundedCornerShape(4.dp))
      .padding(8.dp)
  )
}

Just by moving the modifier to the arguments the composable allows its parents to add additional modifiers such as extra margin. Because the lastly added modifiers are the closest to the button, the border and the inner padding won’t be affected.

Square answered 13/1, 2021 at 8:25 Comment(2)
Wow, not only orders matter but border() can be called multiple times to create multiple borders, that's interestingRowlett
@Valeriy Great explanation! I understood way more from this answer than even the most recent google videos. Have you written a blog on this by any chance? :DHelsinki
L
17
  • In Android Compose resulting Image is being constructed from the outside layer toward the Composable in the center. This means that first defined Green border is outer border and the last defined Red border is inner border . This is very confusing since Green Modifier that is closest to Text Composable in the Code is furthest from it in the result.
  • This is in contrast to SwiftUI where Modifiers appear in the same order both in the Code and in the resulting Image. Modifier that is closest to the Composable in the Code is also closest to it in the resulting Image.
  • If you want to imagine that resulting Image is being constructed from the center where your Composable is positioned (like in SwiftUI) then Modifiers are applied in the opposite order from which they are given (from the bottom upward).
  • So if you have Text Composable with two border Modifiers
    • border Modifier that is furthest away from the Text Composable in the Code (the bottom Red one)
    • will be closest to the Text Composable in the resulting Image
  • Modifiers are applied from outer toward inner layer
    • Applying .border(2.dp, Color.Green) to the outmost layer
    • Applying .padding(50.dp) going inward
    • Applying .border(2.dp, Color.Red) to the innermost layer
package com.example.myapplication

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.unit.dp

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      Text("Hi there!",
        Modifier
          .border(2.dp, Color.Green)
          .padding(50.dp)
          .border(2.dp, Color.Red)
      )
    }
  }
}

enter image description here

Letendre answered 6/10, 2020 at 11:19 Comment(1)
This is unintuitive API. What you would expect here is first border to be applied to the content and the second to padded contentEdible
L
6

The Modifier allows us to customize the appearance of the composable. Using it, you can:

  • Change a Composable’s appearance, size, offset, padding or margin
  • Add interactions, like making an element clickable, scrollable, draggable, or zoomable
  • Change its scale, position in screen while its layout somewhere different entirely, or shape that changes its touch area.

Based on which order you place these modifiers your Composable's visual and behavioral structure is shaped.

Most of the modifiers are applied from from top to bottom or left to right. One exception is Modifier.pointerInput() it gets applied from right to left or bottom to top by default pass.

Modifier.padding()

Modifier.padding() in Jetpack Compose acts as padding or margin depending on order.

Modifier.padding(10.dp).size(200.dp) adds space before setting size you have a Composable with 200.dp size

Modifier.size(200.dp).padding(10.dp) adds padding which you have 180.dp width and height after setting 10.dp padding on each side.

Box(
    Modifier
        .border(2.dp, Color.Green)
        .padding(10.dp)
        .border(2.dp, Color.Red)
        .size(200.dp)
)

Box(
    Modifier
        .border(2.dp, Color.Cyan)
        .size(200.dp)
        .padding(10.dp)
        .border(2.dp, Color.Magenta)
)

enter image description here

And padding modifiers are cumulative. Modifier.padding(20.dp).padding(20.dp) is summed as 40.dp.

enter image description here

Box(
    Modifier
        .border(2.dp, Color.Green)
        .padding(20.dp)
        .border(2.dp, Color.Red)
        .size(200.dp)
)

Box(
    Modifier
        .border(2.dp, Color.Green)
        .padding(20.dp)
        .padding(20.dp)
        .border(2.dp, Color.Red)
        .size(200.dp)
)

Modifier.shadow()

Another modifier that changes appearance of Composable based on which order it's applied. For shadow to be applied as outside of Composable it should be applied before background or other modifiers. If you apply it after Modifier.background you can have outer shadow.

Box(
    Modifier
        .shadow(5.dp, RoundedCornerShape(8.dp))
        .background(Color.White)
        .size(100.dp)
)

Box(
    Modifier
        .background(Color.White)
        .size(100.dp)
        .shadow(5.dp, RoundedCornerShape(8.dp))
) 

enter image description here

Modifier.clip()

This Modifier also clips the Modifiers depending on order it's placed. Good thing with this modifier if you place it before Modifier.clickable{} you can change or clip clickable area of a Composable. Having a circle, triangle or diamond circle area or creating before/after layout is possible using this modifier and Shapes.

It's Modifier.graphicsLayer{} under the hood, you can check out my detailed answer about it here, here and here. It helps you create complex layouts using scale, shape, clip, translate, and other cool properties.

enter image description here

Modifier.offset()

This Modifier is useful for changing position of a Composable after it's laid out unlike Modifier.padding changing value of this Modifier does not change position of a Composable relative to its sibling Composable. However depending on where you set Modifier.offset you can change touch area of a Composable and it has two variants. One that takes lambda defers state read which is advised by google over the one that takes value.

I used one with value for demonstration. You can see if offset is applied first ever modifier that follow offset is moved as Slider changes values. In second example touch area of Composable is not changed because Modifier.clickable{} is applied before Modifier.offset{}

var offset by remember {
    mutableStateOf(0f)
}
Box(
    Modifier
        .offset(x = offset.dp)
        .clickable {}
        .background(Color.Red)
        .size(100.dp)
)

Box(
    Modifier
        .clickable {}
        .offset(x = offset.dp)
        .background(Color.Red)
        .size(100.dp)
)

Slider(value = offset, onValueChange = { offset = it }, valueRange = 0f..200f)

enter image description here

Modifier.pointerInput(keys)

This modifier is basis of gesture and touch events. Using it drag, tap, press, double tap, zoom, rotation and many gesturer can be invoked. In this answer how it's used is explained to build onTouchEvent counterpart of View system.

Unlike Modifiers above it propagates by default from bottom to top unless you consume PointerInputChange. In Compose gesture system consuming continuous events cancel next one in line to receive it. So you can prevent gestures like scroll not happening when you zoom an image for instance.

Modifier
   .pointerInput() // Second one that is invoked
   .pointerInput() // First one that is invoked
Loriloria answered 20/10, 2022 at 19:25 Comment(0)
S
4

The first padding is like the margin for the element in this case.

Compare these Composables and you will see the difference.

@Composable
fun Example() {
    // Default
    Box(modifier = Modifier.background(Color.Cyan), alignment = Alignment.Center){
        Text("Hi there!", Modifier.border(2.dp, Color.Magenta))
    }
    Divider()
    // 10dp margin
    Box(modifier = Modifier.background(Color.Cyan), alignment = Alignment.Center){
        Text("Hi there!", Modifier.padding(10.dp).border(2.dp, Color.Magenta))
    }
    Divider()
    // 10dp margin and 10dp padding
    Box(modifier = Modifier.background(Color.Cyan), alignment = Alignment.Center){
        Text("Hi there!", Modifier.padding(10.dp).border(2.dp, Color.Magenta).padding(10.dp))
    }
}

Render of given Example

Sailcloth answered 5/10, 2020 at 12:49 Comment(0)
P
3

"Modifier elements may be combined using then. Order is significant; modifier elements that appear first will be applied first." @here

It applies to the outer layer first with padding 10.dp, then the border with color.Magenta, and so on ("left to right"). The 80.dp padding applies last to the inner layer.

@Composable
fun test() {
    Text("Hi there!",
            Modifier.background(color = Color.Green)
                    .padding(10.dp)
                    .border(2.dp, Color.Magenta)
                    .padding(30.dp)
                    .border(2.dp, Color.Red)
                    .padding(80.dp)
    )
}

enter image description here

Promulgate answered 5/10, 2020 at 22:16 Comment(7)
You say "It applies to outer layer first". Isn't that kind of backward thinking? Wouldn't it be more logical to start with the Text itself (inner layer) and see how it is being modified with different Modifiers outward? So your way of thinking is that you go outward toward inside applying modifiers as they are defined. You can get same result by going from inside toward outside applying Modifiers in reverse (as I stated in my post). In other words for me it is more logical to start with Text (inner layer) but in that case I need to apply modifiers in reverse order to predict the outcomeLetendre
.padding(10.dp) is closest to the Text in the code but it is furtherest away in the Image. It is illogical. .padding(80.dp) is furtherest from the Text in the code but it is closest to the Text in the Image. It is illogical. But from this I conclude that Modifiers are acctualy applied in opposite order: those furtherest in the code are applied first.Letendre
If you use your approach "It applies to outer layer first" that means that as you read the Code first you see Text then you jump to somewhere away from text (outer layer) and go back to Text by applying Modifiers from outer to inner layer. That doesn't make sense to me. For me it is more logical that we construct image in same way that we read the code. First there is some Text. Then next to it is first Modifier in the code and it should also be in the image. And so we build the image in the same order as we read the code. From inside outward because code starts with Text which is insideLetendre
So when I am saying that Modifiers are applied in the opposite direction what I mean is that Modifiers that are closest to the Text in Code are further away in the resulting image Image goes in opposite direction from the code - hence Modifiers are applied in opposite direction. In the Code Magenta border is defined BEFORE red border and it is CLOSER to Text. But in the resulting Image it is the other way around: red border is CLOSER to the Text eventhough it is defined after Magenta border.Letendre
Equivalent code in SwiftUI produces opposite result. There Magenta border would be closer to the Text in both code and image. Because there each Modifier returns new Modified View. So Modifiers really are used in the direction they are defined. You start with Text View. You apply Magenta modifier which returns new View with Magenta border around Text. Then to that View you apply padding and Red border so red border becomes outer. Everything is logical and flows in the same natural direction in both code and resulting image.Letendre
@Letendre I have worked with SwiftUI and all APIs are intuitive, because it was created by sane people not googlersEdible
modifier elements that appear first will be applied first. you probably meant this modifier elements that appear first will be applied the last. because intuitively we go from inner layer to outer layer. But google destroyed that intuition with these APIsEdible

© 2022 - 2024 — McMap. All rights reserved.