State in Jetpack Compose Made Simple: ViewModel, Hoisting, and Real-World Patterns
Jetpack Compose introduces a declarative way to build UI, and with that comes a crucial concept: state. Understanding how to manage state correctly will empower you to build responsive, dynamic, and bug-free user interfaces. This comprehensive guide will take you step by step — from basic state usage remember
to advanced patterns like ViewModel-based state hoisting and UI state modeling.
🧠 What is State?
In Jetpack Compose, state refers to any data that can change over time and directly affects what gets rendered on screen.
Examples include:
- The text a user types into a
TextField
- Whether a button is enabled
- Whether a dialog is visible
💡 State in a Declarative World
In declarative UIs like Compose, you don’t tell the framework how to update the UI. You just tell it what the UI should look like for a given state.
When the state changes, Compose automatically recomposes the affected parts of the UI.
var counter by remember { mutableStateOf(0) }
Every time counter
changes, the part of the UI that depends on it will be redrawn.
👉 For more on remember
and related concepts, check out my blog:
“Understanding State Management in Jetpack Compose: A Deep Dive with Code Examples.”
🚫 Why Not Use viewModel()
Directly in Every Composable?
A common mistake beginners make is calling viewModel()
inside multiple composables. This can:
- Lead to unexpected behavior
- Create multiple ViewModel instances
- Break testability and reusability
✅ Best Practice: Initialize the ViewModel once at the root level of the screen, then pass it (or its state) down to child composables.
✅ A Realistic Example with ViewModel and State Hoisting
Let’s build a screen where a user can enter their name and see it displayed.
Step 1: ViewModel
class UserInputViewModel : ViewModel() {
var name by mutableStateOf("")
private set
fun onNameChange(newName: String) {
name = newName
}
}
Step 2: Root Composable (Screen Entry Point)
@Composable
fun UserInputScreen(viewModel: UserInputViewModel = viewModel()) {
val name = viewModel.name
UserInputForm(name = name, onNameChange = viewModel::onNameChange)
}
Step 3: Stateless Child Composable
@Composable
fun UserInputForm(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = "Enter your name:")
Spacer(modifier = Modifier.height(8.dp))
TextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
Spacer(modifier = Modifier.height(16.dp))
Text(text = "Hello, $name!")
}
}
This architecture:
- Keeps the UI reactive
- Keeps business logic in the ViewModel
- Makes
UserInputForm
reusable and testable
🎭 Modeling Complex UI States with Sealed Classes
As your screen logic grows, so will your state. You might want to handle loading, error, and success states.
Use a sealed class
for clean modeling:
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<String>) : UiState()
data class Error(val message: String) : UiState()
}
ViewModel Example:
class DataViewModel : ViewModel() {
var uiState by mutableStateOf<UiState>(UiState.Loading)
private set
fun fetchData() {
viewModelScope.launch {
try {
val result = someRepository.getData()
uiState = UiState.Success(result)
} catch (e: Exception) {
uiState = UiState.Error("Something went wrong")
}
}
}
}
UI Layer Example:
@Composable
fun MyScreen(viewModel: DataViewModel = viewModel()) {
when (val state = viewModel.uiState) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> LazyColumn {
items(state.data) { item -> Text(item) }
}
is UiState.Error -> Text(text = state.message)
}
}
📌 Best Practices Recap
- ✅ Use
remember
andmutableStateOf
for a short-lived state - ✅ Use
ViewModel
for screen-level or business logic state - ✅ Hoist state to make components reusable and testable
- ✅ Model complex states explicitly using sealed classes
- ✅ Pass ViewModel/state from root composable — don’t use
viewModel()
everywhere
🎯 Final Thoughts
State is the soul of Jetpack Compose. When you understand and structure it well:
- Your UI becomes smooth and reliable
- Your architecture becomes scalable
- Your code becomes easy to debug and maintain
This guide is your foundation for mastering Compose UI.
Go ahead and start building composables that react intelligently to user input.
If you enjoyed this guide, follow me for more Compose and Kotlin content. Happy Composing!
🙌 If this helped you, feel free to leave a few claps — it supports my work and helps others find it too!