Mr.AndroidShin / Dev Tools

Debugging log · Compose

"@Composable invocations can only happen from the context of a @Composable function"

Early on with Jetpack Compose I tried to show a dialog straight from a button's onClick and the compiler stopped me cold. The message is long and, the first time, a little cryptic — but it's pointing at the single most important mental shift Compose asks of you: you don't call UI in response to events, you change state and let the UI recompose.

The short version: a @Composable function can only be called from another @Composable function (or a composable lambda). You're calling one from a place that isn't composable — an onClick handler, a coroutine, a ViewModel, or a plain helper function. The fix is almost never to force the call through; it's to drive the UI with state instead.

The error

@Composable invocations can only happen from the context of
a @Composable function

It's a compile-time error, which is good news — the Compose compiler is catching a structural mistake before it can ship. The trick is recognizing which of a few common shapes you've hit.

Why Compose enforces this

A @Composable function isn't a normal function. The Compose compiler rewrites it to receive a hidden Composer parameter that tracks where you are in the UI tree, so it can remember state and recompose only what changed. That machinery only exists inside a composable calling context. Call one from ordinary code and there's no Composer to thread through — hence the error. In short, @Composable is a calling convention, not just a label.

Cause 1 — calling a composable from onClick

This is the classic. You can't invoke AlertDialog(...) from inside onClick, because the click lambda runs outside composition:

// ❌ Wrong
Button(onClick = {
    AlertDialog(...)   // not a composable context
}) { Text("Delete") }

Instead, flip a state flag in onClick and render the dialog in the composition based on that state:

// ✅ Right
var showDialog by remember { mutableStateOf(false) }

Button(onClick = { showDialog = true }) { Text("Delete") }

if (showDialog) {
    AlertDialog(
        onDismissRequest = { showDialog = false },
        /* ... */
    )
}

The event handler mutates state; the composition reacts. That inversion is the whole model.

Cause 2 — calling a composable from a coroutine or effect body

Launching a coroutine and calling composables inside it hits the same wall — the coroutine body isn't composable. Do the async work, store the result in state, and read that state in the composition:

// ❌ scope.launch { SomeComposable() }

// ✅
var data by remember { mutableStateOf(null) }
LaunchedEffect(Unit) { data = repository.load() }
data?.let { ItemView(it) }   // composable read happens in composition

Cause 3 — a helper function that forgot its annotation

If you factored some UI into a helper but didn't annotate it, any composable call inside it fails. The fix is one word:

// ❌ fun Header() { Text("Title") }
@Composable
fun Header() { Text("Title") }

Cause 4 — trying to build UI in a ViewModel or plain class

Sometimes the error is telling you the code is in the wrong layer entirely. Composables belong in the UI/composition layer; a ViewModel should expose state (via StateFlow or Compose state), and the composable observes it. If you find yourself wanting to call a composable from a ViewModel, that's the signal to move the rendering into the composable and let it read the ViewModel's state.

A quick rule of thumb: if you're inside a lambda that reacts to something (onClick, onValueChange, a coroutine), you're outside composition — set state there. If you're in the body of an @Composable, you're inside composition — read state and call composables freely.

Snippets are minimal for clarity — adapt the state hoisting to where the data actually lives in your app.