Android Compose Modifiers

Composed modifiers in Jetpack Compose

Learn about composed modifiers and compare those to standard ones.

Some confusion

Some people seems a bit confused about when to use composed to write a custom Modifier in Jetpack Compose, and when to use the then extension function instead. Well, they are actually very different. Let me expand on this just a bit.

β€œStandard” modifiers

This is an example of a β€œstandard” modifier, using the then extension function πŸ‘‡

@Stable
fun Modifier.padding(all: Dp) =
    this.then(
        PaddingModifier(
            start = all,
            top = all,
            end = all,
            bottom = all,
            rtlAware = true,
            inspectorInfo = debugInspectorInfo {
                name = "padding"
                value = all
            }
        )
    )

A β€œstandard” modifier is written using the Modifier.then function over the modifier object, or a previous modifier instance. All the modifiers we use daily are written like this, making use of an extension function for ergonomics.

To write a modifier, we must pick the correct type. E.g: PaddingModifier, if our aim is to limit the children constraints by introducing some padding, LayoutModifier if we want to affect measuring and layout. ParentDataModifier, if we need to provide data to a parent during measuring and positioning. DrawModifier, if we need to draw into the Layout. There are many others.

private class PaddingModifier(
    val start: Dp = 0.dp,
    val top: Dp = 0.dp,
    val end: Dp = 0.dp,
    val bottom: Dp = 0.dp,
    val rtlAware: Boolean,
    inspectorInfo: InspectorInfo.() -> Unit
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
    init {
        require(
            (start.value >= 0f || start == Dp.Unspecified) &&
                (top.value >= 0f || top == Dp.Unspecified) &&
                (end.value >= 0f || end == Dp.Unspecified) &&
                (bottom.value >= 0f || bottom == Dp.Unspecified)
        ) {
            "Padding must be non-negative"
        }
    }

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {

        val horizontal = start.roundToPx() + end.roundToPx()
        val vertical = top.roundToPx() + bottom.roundToPx()

        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) {
            if (rtlAware) {
                placeable.placeRelative(start.roundToPx(), top.roundToPx())
            } else {
                placeable.place(start.roundToPx(), top.roundToPx())
            }
        }
    }

    override fun hashCode(): Int {
        var result = start.hashCode()
        result = 31 * result + top.hashCode()
        result = 31 * result + end.hashCode()
        result = 31 * result + bottom.hashCode()
        result = 31 * result + rtlAware.hashCode()
        return result
    }

    override fun equals(other: Any?): Boolean {
        val otherModifier = other as? PaddingModifier ?: return false
        return start == otherModifier.start &&
            top == otherModifier.top &&
            end == otherModifier.end &&
            bottom == otherModifier.bottom &&
            rtlAware == otherModifier.rtlAware
    }
}

β€œStandard” modifiers are stateless. Compose UI wraps them to hold their state while resolving / applying them. An example of this is a modifier that affects the size of the node it modifies, or even later modifiers in the chain, like padding. It needs to store the measure somewhere.

Icon(
  painter = rememberVectorPainter(image = Icons.Rounded.Menu),
  contentDescription = "Menu button",
  modifier = Modifier
    .clickable { onMenuClick() }
    .padding(16.dp)
    .background(Color.Red) // only the remaining space after applying the padding will be colored.
)

Since the modifier is stateless, all that state can be hold in an internal wrapper that the Compose UI library uses.

Composed modifiers

But sometimes we actually need to write a stateful modifier. Imagine we need to remember things from its body. How could we do it with a stateless one? We need a Composition context. Same for accessing CompositionLocals, for example. That is where you’d use a composed modifier. A good example of it is the clickable modifier πŸ‘‡

fun Modifier.clickable(
    interactionSource: MutableInteractionSource,
    indication: Indication?,
    enabled: Boolean = true,
    onClickLabel: String? = null,
    role: Role? = null,
    onClick: () -> Unit
) = composed(
    factory = { ... },
    inspectorInfo = debugInspectorInfo {
        name = "clickable"
        properties["enabled"] = enabled
        properties["onClickLabel"] = onClickLabel
        properties["role"] = role
        properties["onClick"] = onClick
        properties["indication"] = indication
        properties["interactionSource"] = interactionSource
    }
)

These composed modifiers are stateful modifiers that get composed right before getting assigned to the LayoutNode, which is the representation of a UI node used by Compose UI. They provide a @Composable factory function for creating them in the context of a Composition, so they can call Composable functions like remember when created.

fun Modifier.clickable(
    interactionSource: MutableInteractionSource,
    indication: Indication?,
    enabled: Boolean = true,
    onClickLabel: String? = null,
    role: Role? = null,
    onClick: () -> Unit
) = composed(
    factory = {
        val onClickState = rememberUpdatedState(onClick)
        val pressedInteraction = remember { mutableStateOf<PressInteraction.Press?>(null) }
        if (enabled) { ... }
        val isRootInScrollableContainer = isComposeRootInScrollableContainer()
        val isClickableInScrollableContainer = remember { mutableStateOf(true) }
        val delayPressInteraction = rememberUpdatedState { ... }
        val gesture = Modifier.pointerInput(interactionSource, enabled) { ... }
        Modifier
            .then(
                remember { ... }
            )
            .genericClickableWithoutGesture(
                gestureModifiers = gesture,
                interactionSource = interactionSource,
                indication = indication,
                enabled = enabled,
                onClickLabel = onClickLabel,
                role = role,
                onLongClickLabel = null,
                onLongClick = null,
                onClick = onClick
            )
    },
    inspectorInfo = debugInspectorInfo { ... }
)

This is one of the topics covered in detail in a brand new chapter about Compose UI available in the Jetpack Compose Internals book πŸ₯³

If you want to learn much more about the Compose UI library, read Jetpack Compose Internals πŸ“– Here is the full table of contents πŸ‘‡

Jetpack Compose Internals: Compose UI

  • Integrating UI with the Compose runtime
  • Mapping scheduled changes to actual changes to the tree
  • Composition from the point of view of Compose UI
  • Subcomposition from the point of view of Compose UI
  • Reflecting changes in the UI
  • Different types of Appliers
  • Materializing a new LayoutNode
  • Materializing a change to remove nodes
  • Materializing a change to move nodes
  • Materializing a change to clear all the nodes
  • setContent as the integration point to close the circle
  • Measuring in Compose UI
  • Measuring policies
  • Intrinsic measurements
  • Layout Constraints
  • Modeling modifier chains
  • Setting modifiers to the LayoutNode
  • How LayoutNode ingests new modifiers
  • Drawing the node tree
  • Semantics in Jetpack Compose
  • Notifying about semantic changes
  • Merged and unmerged semantic trees

Since the book is approaching to a final release, I’ll be raising the price just a bit with every new chapter published, so grab it cheaper while you can! Every purchase will be really appreciated, and boost my motivation to keep writing, even more! πŸ™

Thanks for reading! πŸ™Œ


I also share thoughts and ideas on Twitter quite regularly. See you there!