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.