When building Android apps with Jetpack Compose, state management is one of the most important pieces to get right. If you don’t handle state properly, your UI can become messy, tightly coupled, and hard to scale. That’s where State Hoisting in Jetpack Compose comes in.
In this post, we’ll break down what state hoisting is, why it matters, and how you can apply best practices to make your Compose apps scalable, maintainable, and easy to debug.
What Is State Hoisting in Jetpack Compose?
In simple terms, state hoisting is the process of moving state up from a child composable into its parent. Instead of a UI component directly owning and mutating its state, the parent holds the state and passes it down, while the child only receives data and exposes events.
This separation ensures:
- Reusability: Components stay stateless and reusable.
- Single Source of Truth: State is managed in one place, reducing bugs.
- Scalability: Complex UIs are easier to extend and test.
A Basic Example of State Hoisting
Let’s say you have a simple text field. Without state hoisting, the child manages its own state like this:
@Composable
fun SimpleTextField() {
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it }
)
}This works fine for small apps, but the parent composable has no control over the value. It becomes difficult to coordinate multiple composables.
Now let’s apply state hoisting:
@Composable
fun SimpleTextField(
text: String,
onTextChange: (String) -> Unit
) {
TextField(
value = text,
onValueChange = onTextChange
)
}And in the parent:
@Composable
fun ParentComposable() {
var text by remember { mutableStateOf("") }
SimpleTextField(
text = text,
onTextChange = { text = it }
)
}Here’s what changed
- The parent owns the state (
text). - The child only displays the state and sends updates back via
onTextChange.
This is the core idea of State Hoisting in Jetpack Compose.
Why State Hoisting Matters for Scalable Apps
As your app grows, different UI elements will need to communicate. If each composable owns its own state, you’ll end up duplicating data or creating inconsistencies.
By hoisting state:
- You centralize control, making it easier to debug.
- You avoid unexpected side effects caused by hidden internal state.
- You enable testing, since state management is separated from UI rendering.
Best Practices for State Hoisting in Jetpack Compose
1. Keep Composables Stateless When Possible
A good rule of thumb: UI elements should be stateless and only care about how data is displayed. The parent decides what data to provide.
Example: A button shouldn’t decide what happens when it’s clicked — it should simply expose an onClick callback.
2. Use remember Wisely in Parents
State is usually managed at the parent level using remember or rememberSaveable.
- Use
rememberwhen state only needs to survive recomposition. - Use
rememberSaveablewhen you want state to survive configuration changes (like screen rotations).
var text by rememberSaveable { mutableStateOf("") }3. Follow the Unidirectional Data Flow Pattern
Compose encourages Unidirectional Data Flow (UDF):
- Parent owns state.
- State is passed down to child.
- Child emits events back to parent.
This clear flow makes apps predictable and avoids infinite loops or messy side effects.
4. Keep State Close to Where It’s Used, But Not Too Close
Don’t hoist all state to the top-level of your app. That creates unnecessary complexity. Instead, hoist it just far enough up so that all dependent composables can access it.
For example, if only one screen needs a piece of state, keep it inside that screen’s parent composable rather than in the MainActivity.
5. Use ViewModels for Shared State Across Screens
For larger apps, when multiple screens or composables need the same state, use a ViewModel.
class LoginViewModel : ViewModel() {
var username by mutableStateOf("")
private set
fun updateUsername(newValue: String) {
username = newValue
}
}Then in your composable:
@Composable
fun LoginScreen(viewModel: LoginViewModel = viewModel()) {
SimpleTextField(
text = viewModel.username,
onTextChange = { viewModel.updateUsername(it) }
)
}This pattern keeps your UI clean and separates business logic from presentation.
Common Mistakes to Avoid
- Keeping state inside deeply nested children: This makes it impossible to share or control at higher levels.
- Over-hoisting: Don’t hoist state unnecessarily if no other composable needs it.
- Mixing UI logic with business logic: Keep state handling in ViewModels where appropriate.
Conclusion
State Hoisting in Jetpack Compose is more than just a coding pattern — it’s the backbone of building scalable, maintainable apps. By lifting state up, following unidirectional data flow, and keeping components stateless, you set yourself up for long-term success.
To summarize:
- Keep state in the parent, not the child.
- Pass data down, send events up.
- Use ViewModels for shared or complex state.
By applying these best practices, you’ll build apps that are not only functional today but also easy to scale tomorrow.
