When developing a Jetpack Compose UI, you may encounter the compiler error “@Composable invocations can only happen from the context of a @Composable function”.
@Composable invocations can only happen from the context of a @Composable function
This error means that a Composable function is being called from normal Kotlin code that is not currently executing in a Compose Composition.
Typical examples include calling Text() from a normal function, calling a Composable from a Button onClick callback, using remember inside an event handler, or trying to invoke LaunchedEffect after a click.
Compose APIs such as remember, LaunchedEffect, and rememberCoroutineScope are connected to the Composition and its lifecycle, so it is important to distinguish Composable execution from normal Kotlin callbacks. :contentReference[oaicite:15]{index=15}
This article explains why the error occurs and how to fix the most common cases correctly.
- What Does the Error Mean?
- Common Situations
- Cause 1: The Calling Function Is Not @Composable
- Solution 1: Add @Composable to UI Functions
- Do Not Make Every Function @Composable
- Cause 2: Calling a Composable Inside onClick
- Solution 2: Change State Instead
- Why Can’t Text() Be Called from onClick?
- Cause 3: Calling LaunchedEffect Inside onClick
- Solution 3: Use rememberCoroutineScope for Click Events
- LaunchedEffect vs rememberCoroutineScope
- Correct LaunchedEffect Example
- Correct rememberCoroutineScope Example
- Cause 4: Calling remember Inside an Event Callback
- Solution 4: Call remember in the Composable Body
- Cause 5: Confusing Normal and Composable Lambdas
- Solution 5: Annotate the Lambda Type
- Composable Lambda with a Parameter
- Cause 6: Calling a Composable from a ViewModel
- Solution 6: Let the ViewModel Expose State
- Cause 7: Calling Compose UI from a Repository or UseCase
- Cause 8: Executing a Composable from a Normal Function
- Cause 9: Calling Compose UI Outside setContent
- Cause 10: Assuming if Statements Cannot Contain Composables
- when Can Also Contain Compose UI
- Showing a Dialog After a Click
- Showing a Snackbar from an Event
- Running Work When the Screen Appears
- Running Work When a Value Changes
- Do Not Call SideEffect from onClick
- DisposableEffect Also Belongs in a Composable Context
- Common Mistake: Creating a Screen Directly from onClick
- Common Mistake: Reading Compose-Specific Values from a Normal Function
- Common Mistake: Giving the ViewModel UI Instead of State
- When Adding @Composable Is the Correct Fix
- When You Should Not Add @Composable
- Trace the Call Chain
- Remember That Compose Is Recomposition-Based
- Fastest Troubleshooting Order
- Quick Fix by Situation
- If the Error Still Occurs
- Summary
What Does the Error Mean?
Jetpack Compose uses functions annotated with @Composable to describe UI.
@Composable fun Greeting() { Text("Hello") }
Text() is itself a Composable function and must therefore be invoked from a valid Composable context.
This is invalid:
fun showGreeting() { Text("Hello") }
showGreeting() is a normal Kotlin function, so the compiler reports:
@Composable invocations can only happen from the context of a @Composable function
Basic rule: Compose UI APIs such as Text, Button, Column, remember, and LaunchedEffect must be invoked from an appropriate Composable context.
Common Situations
- Calling
Text()from a normal Kotlin function - Calling a custom Composable from a non-Composable function
- Calling a Composable inside
onClick - Calling
rememberinside an event callback - Calling
LaunchedEffectinsideonClick - Calling a Composable from a ViewModel
- Calling a Composable from a Repository or UseCase
- Passing Compose UI through a normal
() -> Unitlambda - Forgetting
@Composableon a lambda parameter - Trying to execute Compose UI outside
setContent
Cause 1: The Calling Function Is Not @Composable
This code is invalid:
fun UserName(name: String) { Text(text = name) }
Because the function builds Compose UI, declare it as Composable:
@Composable fun UserName(name: String) { Text(text = name) }
Solution 1: Add @Composable to UI Functions
@Composable fun ProfileScreen() { Text("Profile") }
A parent Composable can then invoke it:
@Composable fun AppScreen() { ProfileScreen() }
Do Not Make Every Function @Composable
A function that only performs calculations or business logic should normally remain plain Kotlin.
fun calculateTotal( price: Int, count: Int ): Int { return price * count }
Use the result from a Composable:
@Composable fun PriceScreen( price: Int, count: Int ) { val total = calculateTotal(price, count)
Text("Total: $total")
}
Design principle: Keep UI declaration in Composables and keep business logic, calculations, and data access in normal Kotlin whenever possible.
Cause 2: Calling a Composable Inside onClick
This is a very common mistake:
@Composable fun SampleScreen() { Button( onClick = { Text("Clicked") } ) { Text("Button") } }
The onClick block is a normal event callback, not a Composable content block.
Solution 2: Change State Instead
@Composable fun SampleScreen() { var clicked by remember { mutableStateOf(false) }
Column {
Button(
onClick = {
clicked = true
}
) {
Text("Button")
}
if (clicked) {
Text("Clicked")
}
}
}
Compose state changes schedule recomposition of Composables that read that state. Google’s current documentation explains that remember stores values in the Composition and that mutableStateOf creates observable state integrated with Compose. :contentReference[oaicite:16]{index=16}
Why Can’t Text() Be Called from onClick?
Compose does not use an imperative model where an event handler manually inserts another UI element.
Instead, state determines what UI should currently exist.
clicked = false → Do not show the message
clicked = true
→ Show the message
The event changes the state, and Compose updates the UI from that state.
Cause 3: Calling LaunchedEffect Inside onClick
This is also invalid:
@Composable fun SampleScreen() { Button( onClick = { LaunchedEffect(Unit) { // Suspended work } } ) { Text("Start") } }
LaunchedEffect is itself a Composable function.
Google’s current side-effect documentation explicitly notes that LaunchedEffect is a Composable and must be used from other Composable functions. :contentReference[oaicite:17]{index=17}
Solution 3: Use rememberCoroutineScope for Click Events
@Composable fun SampleScreen() { val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
// Call suspend function
}
}
) {
Text("Start")
}
}
Google recommends rememberCoroutineScope when a coroutine needs to be launched from an event handler. The returned scope is associated with the location in the Composition and is cancelled when that call leaves the Composition. :contentReference[oaicite:18]{index=18}
LaunchedEffect vs rememberCoroutineScope
Composable enters the Composition A key or state value changes → LaunchedEffect
User presses a button
An event callback runs
→ rememberCoroutineScope + launch
LaunchedEffect starts a coroutine when it enters the Composition. If its keys change, the previous coroutine is cancelled and a new one starts. The coroutine is also cancelled when the effect leaves the Composition. :contentReference[oaicite:19]{index=19}
Correct LaunchedEffect Example
@Composable fun UserScreen( userId: String ) { LaunchedEffect(userId) { loadUser(userId) }
Text("User")
}
Correct rememberCoroutineScope Example
@Composable fun SaveButton() { val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
saveData()
}
}
) {
Text("Save")
}
}
Cause 4: Calling remember Inside an Event Callback
This is incorrect:
@Composable fun SampleScreen() { Button( onClick = { val state = remember { mutableStateOf(false) } } ) { Text("Button") } }
remember stores data in the Composition and therefore needs a Composable context.
Google’s state documentation explains that values created with remember are stored during the initial Composition and returned again during recomposition. :contentReference[oaicite:20]{index=20}
Solution 4: Call remember in the Composable Body
@Composable fun SampleScreen() { var enabled by remember { mutableStateOf(false) }
Button(
onClick = {
enabled = true
}
) {
Text("Enable")
}
if (enabled) {
Text("Enabled")
}
}
Cause 5: Confusing Normal and Composable Lambdas
A normal lambda type is:
() -> Unit
A lambda intended to contain Compose UI is:
@Composable () -> Unit
Solution 5: Annotate the Lambda Type
Problematic example:
@Composable fun Container( content: () -> Unit ) { content() }
If content contains Compose UI:
@Composable fun Container( content: @Composable () -> Unit ) { content() }
Usage:
@Composable fun Screen() { Container { Text("Hello") } }
Composable Lambda with a Parameter
@Composable fun UserContainer( content: @Composable (String) -> Unit ) { content("Alice") }
Usage:
@Composable fun Screen() { UserContainer { name -> Text(name) } }
Cause 6: Calling a Composable from a ViewModel
Avoid designs such as:
class MainViewModel : ViewModel() {
fun showScreen() {
MyScreen()
}
}
A ViewModel is not a Compose rendering context.
Solution 6: Let the ViewModel Expose State
ViewModel:
class MainViewModel : ViewModel() {
private val _showMessage =
MutableStateFlow(false)
val showMessage =
_showMessage.asStateFlow()
fun onButtonClick() {
_showMessage.value = true
}
}
Composable:
@Composable fun MainScreen( viewModel: MainViewModel ) { val showMessage by viewModel.showMessage.collectAsState()
Button(
onClick = {
viewModel.onButtonClick()
}
) {
Text("Show")
}
if (showMessage) {
Text("Hello")
}
}
This keeps events and state changes outside the UI declaration while letting the Composable render the current state.
Cause 7: Calling Compose UI from a Repository or UseCase
This is also incorrect:
class UserRepository {
fun showError() {
Text("Error")
}
}
Return data or an error result instead:
class UserRepository {
suspend fun loadUser():
Result<User> {
// Load data
}
}
The UI layer can then decide what to display.
Cause 8: Executing a Composable from a Normal Function
Problematic:
fun getContent() { Greeting() }
If you really need to return Compose content, return a Composable lambda:
fun getContent(): @Composable () -> Unit = { Greeting() }
Cause 9: Calling Compose UI Outside setContent
In a Compose Activity, setContent establishes the UI Composition.
class MainActivity : ComponentActivity() {
override fun onCreate(
savedInstanceState: Bundle?
) {
super.onCreate(savedInstanceState)
setContent {
App()
}
}
}
If App() is Composable, invoke it inside the Compose content block.
Cause 10: Assuming if Statements Cannot Contain Composables
Normal control flow is valid inside Composable functions.
@Composable fun Screen( loading: Boolean ) { if (loading) { Text("Loading...") } else { Text("Completed") } }
The problem is not the if statement. The important question is whether the call site is in a Composable context.
when Can Also Contain Compose UI
@Composable fun ResultScreen( state: UiState ) { when (state) { UiState.Loading -> { Text("Loading") }
UiState.Success -> {
Text("Success")
}
UiState.Error -> {
Text("Error")
}
}
}
Showing a Dialog After a Click
Do not do this:
Button( onClick = { AlertDialog(...) } )
Change state instead:
@Composable fun DialogSample() { var showDialog by remember { mutableStateOf(false) }
Button(
onClick = {
showDialog = true
}
) {
Text("Open")
}
if (showDialog) {
AlertDialog(
onDismissRequest = {
showDialog = false
},
confirmButton = {
TextButton(
onClick = {
showDialog = false
}
) {
Text("OK")
}
},
text = {
Text("Message")
}
)
}
}
Showing a Snackbar from an Event
SnackbarHostState.showSnackbar() is a suspend function, so a composition-aware CoroutineScope is useful.
@Composable fun SnackbarSample() { val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Scaffold(
snackbarHost = {
SnackbarHost(snackbarHostState)
}
) {
Button(
onClick = {
scope.launch {
snackbarHostState
.showSnackbar("Saved")
}
}
) {
Text("Save")
}
}
}
Google’s side-effect documentation uses the same pattern: create a scope with rememberCoroutineScope and launch from the button event handler. :contentReference[oaicite:21]{index=21}
Running Work When the Screen Appears
If the operation should run because a Composable entered the Composition rather than because the user clicked a button, use LaunchedEffect.
@Composable fun ProfileScreen( viewModel: ProfileViewModel ) { LaunchedEffect(Unit) { viewModel.loadProfile() }
Text("Profile")
}
When LaunchedEffect enters the Composition it launches its coroutine, and that coroutine is cancelled when the effect leaves the Composition. :contentReference[oaicite:22]{index=22}
Running Work When a Value Changes
@Composable fun UserScreen( userId: String ) { LaunchedEffect(userId) { loadUser(userId) } }
If userId changes, the existing effect is cancelled and restarted with the new key. :contentReference[oaicite:23]{index=23}
Do Not Call SideEffect from onClick
SideEffect is also a Composable API.
This is invalid:
Button( onClick = { SideEffect { // Work } } )
Google describes SideEffect as an effect that runs after every successful recomposition, commonly for publishing Compose state to non-Compose code. :contentReference[oaicite:24]{index=24}
DisposableEffect Also Belongs in a Composable Context
@Composable fun LifecycleSample( lifecycleOwner: LifecycleOwner ) { DisposableEffect(lifecycleOwner) {
val observer =
LifecycleEventObserver { _, _ ->
// Work
}
lifecycleOwner.lifecycle
.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle
.removeObserver(observer)
}
}
}
Google documents DisposableEffect for effects that require cleanup when keys change or when the Composable leaves the Composition. :contentReference[oaicite:25]{index=25}
Common Mistake: Creating a Screen Directly from onClick
Incorrect:
Button( onClick = { DetailScreen() } )
For navigation, invoke a normal navigation action instead:
Button( onClick = { navController.navigate("detail") } )
Common Mistake: Reading Compose-Specific Values from a Normal Function
A normal function should not directly access APIs that require a Composable context.
For example, instead of:
fun getColor(): Color { return MaterialTheme.colorScheme.primary }
read the value in Compose and pass it to normal code:
@Composable fun Screen() { val color = MaterialTheme.colorScheme.primary
Content(color)
}
fun Content(color: Color) {
// Normal Kotlin logic
}
Common Mistake: Giving the ViewModel UI Instead of State
Prefer state such as:
data class UiState( val loading: Boolean = false, val message: String? = null )
and let the Composable decide what UI corresponds to that state.
When Adding @Composable Is the Correct Fix
If the function’s job is to describe UI:
fun Title() { Text("Title") }
change it to:
@Composable fun Title() { Text("Title") }
When You Should Not Add @Composable
Business logic does not need the annotation.
fun validateName( name: String ): Boolean { return name.isNotBlank() }
Keep such functions as normal Kotlin code.
Trace the Call Chain
This call chain is valid:
@Composable Screen() ↓ @Composable Content() ↓ @Composable UserCard() ↓ Text()
This one crosses into a normal callback:
@Composable Screen() ↓ onClick { ... } ↓ Text()
The second call to Text() is therefore invalid.
Remember That Compose Is Recomposition-Based
Composable functions can run again when state changes.
Google defines Composition as the description of the UI produced by executing Composables, and recomposition as re-running Composables to update that Composition when data changes. :contentReference[oaicite:26]{index=26}
That is why event handlers should normally update state instead of directly invoking additional UI functions.
Fastest Troubleshooting Order
- Check whether the function containing the error is
@Composable. - Check whether the called API is a Composable.
- Check whether the call occurs inside
onClickor another normal callback. - If it changes UI, represent the change as state.
- If it launches suspended work, choose between
LaunchedEffectandrememberCoroutineScope. - If a lambda contains Compose UI, make it
@Composable () -> Unit. - Make sure ViewModels and repositories do not directly invoke Compose UI.
Quick Fix by Situation
Text() called from a normal UI function → Add @Composable to that UI function
Text() called from onClick
→ Change state and render Text from the Composable
LaunchedEffect called from onClick
→ Use rememberCoroutineScope + launch
Composable passed to a normal lambda
→ Change the type to @Composable () -> Unit
Composable called from ViewModel
→ Update state in the ViewModel and render in Compose
Suspend work when the screen enters Composition
→ Use LaunchedEffect
If the Error Still Occurs
Check all of the following:
- The calling UI function has
@Composable. Text,Button, orColumnis not being called from normal Kotlin code.rememberis not called inside an event handler.LaunchedEffectis not called insideonClick.SideEffectandDisposableEffectare not called from normal callbacks.- Composable lambda parameters use
@Composable. - ViewModels do not directly call Compose UI.
- Repositories do not directly call Compose UI.
- UI changes are represented with state.
- Coroutines are launched with the appropriate Compose API.
Summary
Jetpack Compose “@Composable invocations can only happen from the context of a @Composable function” means that a Composable function is being invoked from a location that is not a valid Composable context.
If a function’s responsibility is UI declaration, annotate it correctly:
@Composable fun Greeting() { Text("Hello") }
However, do not directly invoke Composables from callbacks such as onClick.
Instead, change state:
onClick = { showMessage = true }
and render from the Composable:
if (showMessage) { Text("Message") }
This follows Compose’s state and recomposition model. :contentReference[oaicite:27]{index=27}
For suspended work triggered by an event, use:
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
saveData()
}
}
) {
Text("Save")
}
Google’s current documentation recommends rememberCoroutineScope for launching coroutines from event handlers. :contentReference[oaicite:28]{index=28}
For work tied to entering the Composition or to key changes, use LaunchedEffect. It starts a coroutine when it enters the Composition, restarts when its keys change, and is cancelled when it leaves the Composition. :contentReference[oaicite:29]{index=29}
When this compiler error appears, the most useful question is not simply “Where should I add @Composable?” but “Is this code describing UI, handling an event, changing state, or running a side effect?” Once that distinction is clear, the correct fix is usually straightforward.
