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.