ViewModel in Compose

Every composable in the RecipeBox app so far has kept its state with remember — fine for a text field's current draft, a checkbox's checked flag, or a dialog's visibility. But the recipe list itself, the logged-in user, the in-progress "Add Recipe" form — that's real application state, and it needs to survive things a plain composable's memory never does: the screen rotating, the system trimming memory in the background, or the user navigating to a detail screen and back. This lesson introduces ViewModel in Compose — the same architecture component you may already know from View-based Android, now obtained through a composable function instead of by viewModels().

Where this fits: If you've been through Android (Kotlin), ViewModel itself is not new — what's new here is how a composable gets one, and how it fits into Compose's recomposition model.

remember Belongs to the Composition — ViewModel Belongs to the Screen

remember ties a value to the composition — the tree of composables currently on screen. When a composable leaves the composition (the user navigates away, or a configuration change tears down and rebuilds the whole Activity), anything held in remember is gone, full stop. That's exactly right for transient UI state like "is this dropdown expanded," but it's the wrong tool for RecipeBox's recipe list — you don't want the list to flash away and reload every time the phone rotates.

ViewModel is scoped differently: to a ViewModelStoreOwner — typically the hosting Activity, or a specific destination in the navigation graph — which survives configuration changes even though the composable functions observing it are torn down and re-run constantly. The system keeps the exact same ViewModel instance alive across a rotation and simply reconnects the recomposed UI to it.

rememberViewModel
ScopeTied to the composition (this composable's slot in the UI tree)Tied to a ViewModelStoreOwner (Activity, NavBackStackEntry, ...)
Survives rotation?No — lost when the Activity is recreatedYes — the same instance is handed back
Survives navigating away and back?No, if the composable leaves the compositionDepends on scope — can outlive a single screen
Good forTransient UI-only state (expanded/collapsed, scroll position, draft text)Screen-level state, business logic, data loading

Obtaining a ViewModel: the viewModel() Composable Function

Compose provides a composable function, viewModel(), from the androidx.lifecycle:lifecycle-viewmodel-compose artifact. Called inside a composable, it looks up (or, the first time, creates) a ViewModel instance scoped to the current NavBackStackEntry if you're using Navigation Compose, or to the nearest ViewModelStoreOwner otherwise — usually the hosting Activity:

@Composable
fun RecipeListScreen(
    viewModel: RecipeListViewModel = viewModel()
) {
    // viewModel is the SAME instance across recompositions and rotations
}

Passing viewModel() as a default parameter value, as shown above, is the idiomatic pattern — it lets Compose supply the real ViewModel in production while still letting a test or a @Preview pass in a fake one explicitly, without changing the function's body at all.

Worked Example: RecipeListViewModel

RecipeBox's recipe list screen needs a place to hold the currently loaded recipes and a function to (re)load them. That's exactly what a ViewModel is for:

class RecipeListViewModel(
    private val repository: RecipeRepository
) : ViewModel() {

    var recipes by mutableStateOf<List<Recipe>>(emptyList())
        private set

    var isLoading by mutableStateOf(false)
        private set

    init {
        loadRecipes()
    }

    fun loadRecipes() {
        viewModelScope.launch {
            isLoading = true
            recipes = repository.getAllRecipes()
            isLoading = false
        }
    }
}

mutableStateOf here is Compose's own observable state holder — the same kind remember { mutableStateOf(...) } wraps inside a composable — but nothing here stops the ViewModel from holding it directly, without remember, because the ViewModel itself already has the right lifetime. The private set keeps writes confined to the ViewModel, while any composable can still read recipes and isLoading. viewModelScope is a coroutine scope tied to the ViewModel's own lifetime — it's automatically cancelled when the ViewModel is cleared, so there's no manual cleanup to write.

Consuming the ViewModel from a Composable

With the ViewModel defined, the recipe list screen simply reads its exposed properties — Compose takes care of recomposing this composable whenever recipes or isLoading changes, exactly like it would for any other mutableStateOf:

@Composable
fun RecipeListScreen(
    viewModel: RecipeListViewModel = viewModel(),
    onRecipeClick: (Int) -> Unit
) {
    if (viewModel.isLoading) {
        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            CircularProgressIndicator()
        }
    } else {
        LazyColumn {
            items(viewModel.recipes) { recipe ->
                RecipeRow(recipe = recipe, onClick = { onRecipeClick(recipe.id) })
            }
        }
    }
}

Notice that RecipeListScreen never constructs RecipeListViewModel itself, never calls loadRecipes() from a click handler here (the init block already triggered the first load), and never touches a coroutine directly. All of that — data loading, loading-state tracking, business logic — lives in the ViewModel, and the composable's only job is to describe what the UI looks like for whatever state the ViewModel currently exposes. That separation of "what the screen looks like" from "where the data comes from and how it changes" is the seed of the next lesson.

Note: This is correct, real Compose/Kotlin code — but this website's own project doesn't run a live Android Studio build, so you won't see it actually render on this page.
Next up: Unidirectional Data Flow formalizes the pattern you just saw — state flowing down from the ViewModel, events flowing back up — and shows why it matters once a screen has more than a read-only list.