State hoisting helps keep your app organized by moving state out of UI parts. This makes your code easier to read and test.
State hoisting pattern in Android Kotlin
class MyViewModel : ViewModel() { private val _count = MutableStateFlow(0) val count: StateFlow<Int> = _count fun increment() { _count.value += 1 } } @Composable fun Counter(count: Int, onIncrement: () -> Unit) { Button(onClick = onIncrement) { Text("Count: $count") } }
State is stored outside the UI in a ViewModel or similar class.
The UI component receives state and events as parameters.
class CounterViewModel : ViewModel() { private val _count = MutableStateFlow(0) val count: StateFlow<Int> = _count fun increment() { _count.value += 1 } }
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) {
Text("Count: $count")
}
}@Composable
fun CounterScreen(viewModel: CounterViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
val count by viewModel.count.collectAsState()
Counter(count = count, onIncrement = { viewModel.increment() })
}This app shows a button with a count. When you tap the button, the count increases. The count state is kept in the ViewModel, not inside the button UI.
import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow class CounterViewModel : ViewModel() { private val _count = MutableStateFlow(0) val count: StateFlow<Int> = _count fun increment() { _count.value += 1 } } @Composable fun Counter(count: Int, onIncrement: () -> Unit) { Button(onClick = onIncrement) { Text("Count: $count") } } @Composable fun CounterScreen(viewModel: CounterViewModel = viewModel()) { val count by viewModel.count.collectAsState() Counter(count = count, onIncrement = { viewModel.increment() }) } @Composable fun App() { CounterScreen() }
State hoisting improves testability by separating UI from logic.
Always pass state and event handlers as parameters to UI components.
Use ViewModel or similar classes to hold state outside UI.
State hoisting means moving state out of UI components.
UI components become simpler and reusable by receiving state and events as parameters.
ViewModel is a common place to hold state in Android apps.
Practice
state hoisting in Android Kotlin UI development?Solution
Step 1: Understand state hoisting concept
State hoisting means moving state out of UI components so they don't manage their own state internally.Step 2: Identify the benefit of moving state out
This makes UI components simpler, easier to reuse, and testable by passing state and events as parameters.Final Answer:
To move state out of UI components to make them simpler and reusable -> Option DQuick Check:
State hoisting = move state out [OK]
- Thinking state should stay inside UI components
- Confusing state hoisting with global variables
- Believing ViewModel is not related to state hoisting
Solution
Step 1: Identify state hoisting signature
State hoisting means the composable receives state as a parameter instead of owning it internally.Step 2: Check which function receives state as parameter
fun Greeting(name: String) { Text("Hello $name") } passesname: Stringas parameter, which is correct for hoisting state.Final Answer:
fun Greeting(name: String) { Text("Hello $name") } -> Option CQuick Check:
State passed as parameter = fun Greeting(name: String) { Text("Hello $name") } [OK]
- Creating state inside composable instead of passing it
- Not passing state at all
- Using remember inside UI component for hoisted state
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Clicked $count times")
}Solution
Step 1: Understand initial state value
The variablecountstarts at 0 usingrememberandmutableStateOf.Step 2: Analyze button click effect
When the button is clicked once,count++increments count to 1, triggering recomposition.Final Answer:
Clicked 1 times -> Option AQuick Check:
Initial 0 + 1 click = 1 [OK]
- Thinking count stays 0 after click
- Ignoring recomposition after state change
- Expecting syntax or runtime error
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
CounterDisplay(count)
}
@Composable
fun CounterDisplay(count: Int) {
Button(onClick = { count++ }) {
Text("Count: $count")
}
}Solution
Step 1: Analyze state ownership and mutation
Statecountis owned inCounterand passed as immutableInttoCounterDisplay.Step 2: Understand why
Incrementingcount++inCounterDisplayfailscountinsideCounterDisplayonly changes local copy, not the original state inCounter.Final Answer:
count++ inside CounterDisplay does not update state in Counter -> Option AQuick Check:
Immutable parameter can't update hoisted state [OK]
- Trying to mutate immutable parameter
- Using remember in child composable incorrectly
- Not passing event callback to update state
CounterDisplay can update the count?
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
CounterDisplay(count)
}
@Composable
fun CounterDisplay(count: Int) {
Button(onClick = { count++ }) {
Text("Count: $count")
}
}Solution
Step 1: Recognize state hoisting requires event callbacks
To update hoisted state, child composable must receive a function to trigger state changes in the owner.Step 2: Modify CounterDisplay to accept update lambda
Pass a lambda likeonCountChange: () -> UnittoCounterDisplayand call it insideonClick.Final Answer:
Pass a lambda to CounterDisplay to update count in Counter -> Option BQuick Check:
State hoisting needs event callback to update state [OK]
- Trying to mutate parameter directly
- Creating new state inside child composable
- Removing state from owner composable
Ready to prove it?
Pass the test and this pattern moves unknown
