Question #385MediumNative Android

Explain state hoisting in Jetpack Compose?

#jetpack-compose#state-hoisting#architecture

Answer

Overview

State hoisting = Moving state to a composable's caller to make it stateless and reusable.


Before Hoisting (Stateful)

kotlin
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    
    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}
// ❌ Hard to test, not reusable

After Hoisting (Stateless)

kotlin
@Composable
fun Counter(
    count: Int,
    onIncrement: () -> Unit
) {
    Button(onClick = onIncrement) {
        Text("Count: $count")
    }
}

// Caller manages state
@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }
    
    Counter(
        count = count,
        onIncrement = { count++ }
    )
}
// ✅ Testable, reusable

Benefits

1. Single Source of Truth

kotlin
@Composable
fun App() {
    var count by remember { mutableStateOf(0) }
    
    Column {
        Text("Total: $count")
        Counter(count, { count++ })
        Counter(count, { count++ })
    }
}

2. Testability

kotlin
@Test
fun counterDisplaysCorrectValue() {
    composeTestRule.setContent {
        Counter(count = 5, onIncrement = {})
    }
    composeTestRule.onNodeWithText("Count: 5").assertExists()
}

3. Reusability

kotlin
@Composable
fun MultipleCounters() {
    var count1 by remember { mutableStateOf(0) }
    var count2 by remember { mutableStateOf(0) }
    
    Counter(count1, { count1++ })
    Counter(count2, { count2++ })
}

Real Example: TextField

Stateful (Not Hoisted)

kotlin
@Composable
fun SearchBar() {
    var query by remember { mutableStateOf("") }
    
    TextField(
        value = query,
        onValueChange = { query = it }
    )
}

Stateless (Hoisted)

kotlin
@Composable
fun SearchBar(
    query: String,
    onQueryChange: (String) -> Unit
) {
    TextField(
        value = query,
        onValueChange = onQueryChange
    )
}

@Composable
fun SearchScreen() {
    var query by rememberSaveable { mutableStateOf("") }
    
    SearchBar(
        query = query,
        onQueryChange = { query = it }
    )
    
    // Can use query elsewhere
    Text("Searching for: $query")
}

Pattern

kotlin
// State holder
@Composable
fun FeatureScreen() {
    var state by remember { mutableStateOf(initialState) }
    
    StatelessComponent(
        state = state,
        onEvent = { event ->
            state = updateState(state, event)
        }
    )
}

// Stateless component
@Composable
fun StatelessComponent(
    state: State,
    onEvent: (Event) -> Unit
) {
    // Just displays and emits events
}

Best Practice: Hoist state as high as needed, but no higher. Keep state as low as possible in the hierarchy.