Interview QuestionPractical QuestionFollow-up Questions

Custom Modifiers in Jetpack Compose

skydovesJaewoong Eum (skydoves)||8 min read

Custom Modifiers in Jetpack Compose

Jetpack Compose provides multiple ways to create custom modifiers, each offering different trade-offs between simplicity, flexibility, and performance. Selecting the right approach depends on whether you need access to Compose state, how often the modifier re-evaluates, and whether you require low level control over drawing or layout. By the end of this lesson, you will be able to:

  • Distinguish between modifier factories, composable modifier factories, and Modifier.Node based implementations.
  • Explain why modifier factories are hoistable and allocation free during recomposition.
  • Describe when a composable modifier factory is necessary and what limitations it introduces.
  • Break down the three components of a Modifier.Node implementation: factory, element, and node.
  • Choose the appropriate modifier approach for a given use case.

Modifier Factories

A modifier factory is a plain extension function on Modifier that chains existing modifiers together. It does not involve Compose state, animations, or composition locals. Because it is pure Kotlin with no composable context, it allocates nothing during recomposition and can be hoisted freely.

fun Modifier.myBackground(color: Color): Modifier =
  this
    .padding(16.dp)
    .clip(RoundedCornerShape(8.dp))
    .background(color)

Usage is straightforward:

Box(modifier = Modifier.myBackground(Color.Cyan))

This pattern works best for styling shortcuts or layout presets where the modifier configuration is static or derived entirely from parameters passed in at the call site.

Composable Modifier Factories

When you need to read from Compose state, animate values, or access CompositionLocal values inside a modifier, you mark the factory function with @Composable. This gives you access to the composition scope but comes with a cost: the function cannot be hoisted outside composition and always re-evaluates during recomposition.

@Composable
fun Modifier.fade(enabled: Boolean): Modifier {
  val alpha by animateFloatAsState(
    if (enabled) 0.5f else 1f
  )
  return this.then(
    Modifier.graphicsLayer { this.alpha = alpha }
  )
}

Usage inside a composable:

Box(modifier = Modifier.fade(enabled = isDimmed))

Because the function participates in composition, any state read inside it triggers recomposition of the call site. Use this approach when the modifier depends on animated or observable values that change over time.

Modifier.Node

Modifier.Node is the low level API that gives full control over rendering, layout, gesture handling, and state delegation. It consists of three parts:

  1. Factory function that returns the element.
  2. ModifierNodeElement that creates and updates the node.
  3. Modifier.Node subclass that implements the behavior.
fun Modifier.circle(color: Color) =
  this then CircleElement(color)

private data class CircleElement(
  val color: Color
) : ModifierNodeElement<CircleNode>() {
  override fun create() = CircleNode(color)
  override fun update(node: CircleNode) {
    node.color = color
  }
}

private class CircleNode(
  var color: Color
) : DrawModifierNode, Modifier.Node() {
  override fun ContentDrawScope.draw() {
    drawCircle(color = color)
  }
}

This interview continues for subscribers

Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.

Become a Sponsor