Interview QuestionPractical QuestionFollow-up Questions

State Hoisting in Jetpack Compose

skydovesJaewoong Eum (skydoves)||8 min read

State Hoisting in Jetpack Compose

State hoisting is a pattern in Jetpack Compose where you move state ownership from a child composable up to its caller. The child becomes stateless, receiving its current value and an event callback as parameters. This creates unidirectional data flow: state flows downward through parameters, and events flow upward through callbacks. By the end of this lesson, you will be able to:

  • Explain how state hoisting creates unidirectional data flow in composable hierarchies.
  • Identify where state should be hoisted based on which composables consume it.
  • Describe how hoisting improves reusability and testability of composables.
  • Apply the pattern to build stateless UI components that are decoupled from business logic.
  • Recognize when to use state holders versus hoisting to a ViewModel.

Unidirectional Data Flow

In a hoisted state model, the parent composable owns the state and passes it down as an immutable parameter. The child composable renders based on that parameter and notifies the parent through a lambda when the user interacts with it. The parent then updates the state, which triggers recomposition and pushes the new value back down.

@Composable
fun TemperatureConverter() {
    var celsius by remember { mutableStateOf("") }

    TemperatureInput(
        value = celsius,
        onValueChange = { celsius = it }
    )
}

@Composable
fun TemperatureInput(
    value: String,
    onValueChange: (String) -> Unit
) {
    TextField(value = value, onValueChange = onValueChange)
}

TemperatureInput has no remember call and no mutable state. It is a pure function of its parameters. The parent TemperatureConverter holds the state and decides how to respond to changes. This separation means TemperatureInput can be used in any context that provides a String and a callback, without carrying hidden state dependencies.

The Compose runtime benefits from this pattern because it can skip recomposition of the child when its parameters have not changed. If the parent holds the state and only passes stable, immutable values downward, the runtime's equality checks can avoid unnecessary recomposition of subtrees.

Determining Where to Hoist

The rule for deciding where to place state is straightforward: hoist it to the lowest common ancestor of all composables that need to read or modify it. If only one composable reads a value and no sibling needs it, the state can stay local. If two siblings both depend on the same value, the state must move up to their shared parent.

This interview continues for subscribers

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

Become a Sponsor