RizTech Academy logo
RizTech Academy
Jetpack Compose FundamentalsLesson 4 of 630 min

Modifiers, and why order matters

Every composable you have written so far takes a modifier, and so does almost every composable in the Compose library. A modifier is how you give something a size, padding, a background, a border, a shape or a click handler. It looks like a list of settings. It is not: it is a chain, applied in order, and swapping two links changes what the user sees and where they can tap. This lesson runs the classic order mistakes on the emulator so you can see each one.

A modifier is a chain of wrappers

Text(
    text = "background, then padding",
    modifier = Modifier
        .background(Color(0xFFFFE082))
        .padding(16.dp),
)

Start with the empty Modifier, add a yellow background, then 16 dp of padding. The way to hold this in your head is: each modifier applies to everything written after it. background comes first, so it paints behind the padding and the text. padding comes second, so it only pushes the text inwards. Think of each one as a box wrapped round the rest of the chain, the first one outermost.

Each call returns a new Modifier; nothing is changed in place. Modifier on its own is the empty chain you start from.

Padding before and after background

Put two texts in a Column on the emulator:

Text(
    text = "background, then padding",
    modifier = Modifier
        .background(Color(0xFFFFE082))
        .padding(16.dp),
)
Text(
    text = "padding, then background",
    modifier = Modifier
        .padding(16.dp)
        .background(Color(0xFFFFE082)),
)

The first shows a generous yellow block with the text sitting 16 dp inside it: the padding is inside the background, so it is painted yellow. The second shows a thin yellow strip hugging the letters, set 16 dp in from where the first one starts: the padding came first, so it is empty space outside the background.

Compose has no margin. Padding before a background or border is the margin; padding after it is the inner padding. One modifier, two meanings, decided entirely by position.

Padding before and after clickable

Order matters most where the user cannot see it. Two bordered texts, both meant to be tappable:

Text(
    text = "clickable, then padding",
    modifier = Modifier
        .border(1.dp, Color.Gray)
        .clickable { Log.d("Modifiers", "clicked: clickable, then padding") }
        .padding(24.dp),
)
Text(
    text = "padding, then clickable",
    modifier = Modifier
        .border(1.dp, Color.Gray)
        .padding(24.dp)
        .clickable { Log.d("Modifiers", "clicked: padding, then clickable") },
)

On screen they look identical: two grey boxes of the same size. On the emulator, the accessibility tree reported their tappable areas as:

Version Tappable area
clickable, then padding The whole box, about 222 × 72 dp
padding, then clickable Only the text, about 174 × 48 dp

Tapping inside the border near the top-left corner logged clicked: clickable, then padding for the first box and nothing at all for the second. The second box has a 24 dp dead zone all round that looks tappable and is not. Users on a cheap phone, in a moving bus, with a thumb, miss small targets constantly, and they blame the app. Material's guidance is a touch target of at least 48 × 48 dp; the accessibility lesson in module 4 comes back to it. The same rule applies to the ripple: it fills only the clickable area, so with padding first, the ripple is a tight rectangle round the text.

Put clickable before the padding that should be part of the tap area.

Clip before background

Text(
    text = "clip, then background",
    modifier = Modifier
        .clip(RoundedCornerShape(12.dp))
        .background(Color(0xFFA5D6A7))
        .padding(16.dp),
)
Text(
    text = "background, then clip",
    modifier = Modifier
        .background(Color(0xFFA5D6A7))
        .clip(RoundedCornerShape(12.dp))
        .padding(16.dp),
)

The first has rounded green corners. The second is a plain green rectangle: the background was painted before the clip, so there was nothing for the clip to cut. Clip affects what is drawn after it in the chain, so clip first, then paint. (background also takes a shape directly, background(color, shape), which is shorter when you only need the colour rounded. clip is for when the content, such as an image or a ripple, must be cut too.)

Size: fixed, fill and weight

By default a composable is as big as its content needs. Three ways to change that:

  • size(48.dp), width(…), height(…) set a fixed size. Use them for icons and avatars, not for text containers: a fixed height that fits "Onion" in English clips a longer Marathi label or a user's larger font setting.
  • fillMaxWidth() takes all the width the parent offers. fillMaxWidth(0.5f) takes half.
  • weight(1f) exists only inside a Row or Column. It shares out the space left after the unweighted children are measured.
Row(modifier = Modifier.fillMaxWidth()) {
    Text("Onion", modifier = Modifier.weight(1f).background(Color(0xFFE0E0E0)))
    Text("₹2,450", modifier = Modifier.background(Color(0xFFB3E5FC)))
}

On the emulator the price takes exactly its own width at the right edge and "Onion" stretches to fill everything else. That is the standard "label on the left, value on the right" row, and you will write it constantly. Try weight outside a Row or Column and it does not compile: it is defined on RowScope and ColumnScope, which are only available inside their lambdas.

The trap: the parameter you forgot to use

Every reusable composable takes modifier: Modifier = Modifier and should apply it to its outermost element. Here is the mistake that breaks it:

@Composable
fun BrokenPriceCard(price: CommodityPrice, modifier: Modifier = Modifier) {
    Card(modifier = Modifier.padding(8.dp)) {
        Text(text = price.commodity, modifier = Modifier.padding(16.dp))
    }
}

It compiles, and the build prints no warning about it. The only sign is that Android Studio shows the unused modifier parameter greyed out in the editor, and you will not notice it. A caller writes BrokenPriceCard(price, Modifier.fillMaxWidth()) and the card stays narrow, because the card used a fresh Modifier (capital M) and threw the caller's away. The fix is one letter, and the convention for where it goes:

Card(modifier = modifier.padding(8.dp)) {

Start the root's chain from the modifier parameter, lower-case. Your own additions go after it, so the caller's choices (size, outer padding) apply first. Inside the composable, children start from Modifier, capital, because the caller's modifier belongs to the root only. PriceCard does exactly this: Card(modifier = modifier) on the outside, Modifier.padding(16.dp) on the inner Column.

The modifiers you will use most

Modifier What it does Notes
padding(16.dp), padding(horizontal = …, vertical = …) Space around what follows Before a background or border it acts as margin
background(color), background(color, shape) Paints behind what follows
border(1.dp, color), border(1.dp, color, shape) Draws a border round what follows
clip(shape) Cuts drawing of what follows to a shape Put before background
clickable { } Makes it tappable, adds a ripple and accessibility semantics Put before inner padding
size(…), width(…), height(…) Fixed size Avoid on containers of text
heightIn(min = 48.dp) Minimum size that can still grow Safe for text and touch targets
fillMaxWidth(), fillMaxHeight(), fillMaxSize() Take the space the parent offers Optional fraction, e.g. 0.5f
weight(1f) Share the leftover space Inside Row/Column only
wrapContentWidth(Alignment.End) Shrink to content and align within the space
aspectRatio(16f / 9f) Fix the ratio of width to height Images, video
offset(x = 4.dp) Move the drawing without changing layout Neighbours do not move
alpha(0.5f) Transparency
verticalScroll(rememberScrollState()) Lets a Column scroll Lesson 6
testTag("price-card") Lets UI tests find the element Testing module

Check your work

What "each modifier applies to everything written after it" means for padding then background. The padding is outside the background, so it acts as a margin.

Why Compose has no margin modifier. Padding placed before a background or border already is the margin.

What happened when you tapped the padded corner of the "padding, then clickable" box. Nothing; the tappable area is only the text inside the padding.

Why background then clip shows square corners. The background was drawn before the clip, so the clip had nothing to cut.

Where weight can be used. Inside a Row or Column.

The bug in BrokenPriceCard. It ignores its modifier parameter, so the caller's fillMaxWidth() has no effect.

Where the modifier parameter goes. At the start of the outermost element's chain.

Practice 3. PriceCard(price = …, modifier = Modifier.clickable { Log.d("Modifiers", price.market) }). Because PriceCard applies its modifier to the Card, the whole card is the tap area.

Practice 4. "Lasalgaon" and "Onion" share the leftover width equally; the price keeps its own width. Weights divide the space left after unweighted children, in proportion to the numbers.

Practice 6. One correct answer:

@Composable
fun MarketChip(name: String, modifier: Modifier = Modifier) {
    val shape = RoundedCornerShape(50)
    Box(
        modifier = modifier
            .heightIn(min = 48.dp)
            .clip(shape)
            .border(1.dp, MaterialTheme.colorScheme.outline, shape)
            .clickable { Log.d("Modifiers", "chip: $name") }
            .padding(horizontal = 16.dp),
        contentAlignment = Alignment.Center,
    ) {
        Text(text = name)
    }
}

heightIn first, so the minimum covers everything; clip before anything drawn, so the ripple is rounded; border with the same shape; clickable before padding, so the padding is tappable. Box stacks its content and centres it; module 4 covers it properly.

Practice

  1. Run the padding-and-background pair on the emulator and describe the difference in one sentence.
  2. Run the two clickable boxes, add a Log.d to each, and tap the corners. Confirm which one logs.
  3. Give PriceCard a clickable that logs the market, applied through the caller's modifier only: change the call site, not PriceCard. Check the whole card ripples.
  4. Build the "Onion ... ₹2,450" row, then add a third Text("Lasalgaon") with weight(1f) too. Predict how the space divides before running it.
  5. Break it on purpose: put Modifier.weight(1f) on a Text directly inside setContent. Read the compiler's error and say which scope it is missing.
  6. Harder: write MarketChip(name: String, modifier: Modifier = Modifier) that draws the market name in a rounded, bordered, tappable chip with a 48 dp minimum height, where the whole chip, including its padding, is tappable and the ripple is rounded. Get the order of clip, border, clickable, heightIn and padding right, and explain each position.

Official documentation

  • Compose modifiers — The official guide, including why order matters and scope-specific modifiers.
  • List of Compose modifiers — Every built-in modifier, grouped by purpose, for when the table above runs out.
  • Compose layout basics — How measurement works, which explains fillMaxWidth and weight; module 4 covers it.

Next: previews, and seeing a change without running the app.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.

About the internship