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.

Aakanksha Shivani
3 min readApr 6, 2025

🧠 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 and mutableStateOf 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!

--

--

Aakanksha Shivani
Aakanksha Shivani

No responses yet