Skip to content

Jetpack Compose ViewModel — Complete Guide

DodaTech Updated 2026-06-24 2 min read

In this tutorial, you'll learn about Jetpack Compose ViewModel. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

The Problem

You create a new ViewModel on every recomposition, or you access ViewModel state in a LaunchedEffect and get stale data.

Wrong Approach ❌

@Composable
fun BadScreen() {
    // ViewModel created on every recomposition — WRONG
    val viewModel = MyViewModel()
    Text(viewModel.uiState.value)
}
class MyViewModel : ViewModel() {
    private val _state = MutableStateFlow("")
    val state: StateFlow<String> = _state.asStateFlow()
}

@Composable
fun StaleAccess(viewModel: MyViewModel) {
    LaunchedEffect(Unit) {
        // Captures viewModel.state.value at launch time — stale!
        val current = viewModel.state.value
    }
}

Output: ViewModel state resets on every recomposition. Stale captured values.

Right Approach ✅

@Composable
fun GoodScreen(
    viewModel: MyViewModel = hiltViewModel() // Scoped to composable lifecycle
) {
    val state by viewModel.state.collectAsState()
    Text(state)

    LaunchedEffect(Unit) {
        // Collect flow reactively
        viewModel.state.collect { latest ->
            // React to each emission
        }
    }
}

@HiltViewModel
class MyViewModel @Inject constructor(
    private val repo: MyRepository
) : ViewModel() {
    val state: StateFlow<UiState> = repo.data
        .map { UiState.Success(it) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), UiState.Loading)
}

Output: ViewModel survives recomposition and config changes. State is reactive.

Prevention

  • Use hiltViewModel() or viewModel() from lifecycle-viewmodel-compose.
  • Never instantiate ViewModel manually — always use the delegate.
  • Use collectAsState() for UI-bound state collection.
  • Use stateIn() with WhileSubscribed(5000) to avoid restarting upstream flows.

Common Mistakes with compose viewmodel

  1. Forgetting deriving (Show, Eq) on custom data types needed for debugging
  2. Placing the wildcard pattern first in case expressions, making all subsequent patterns unreachable
  3. Using head and tail instead of pattern matching, causing runtime errors on empty lists

These mistakes appear frequently in real-world Android code. DodaTech's contributors have identified these patterns through analysis of open-source projects and production systems.

Practice Exercise

Write a pure function that safely divides two integers using Maybe, then test it with edge cases like division by zero and negative numbers.

This exercise reinforces the concepts covered in this guide. Try implementing it before checking online solutions.

FAQ

### What is the correct scope for a ViewModel in Compose?

Use hiltViewModel() for Hilt-injected ViewModels. The ViewModel is scoped to the closest NavBackStackEntry, Activity, or Fragment.

### How do I share a ViewModel between composables?

Scope the ViewModel to a shared parent navigation destination or use a shared NavGraphViewModel. For app-wide state, use a Singleton state holder.

### Why does my ViewModel state disappear when I navigate away?

The ViewModel is scoped to the navigation back stack entry. When you pop the entry, the ViewModel is cleared. Use a shared ViewModel or save state in a Repository.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro