PC パソコン

Jetpack Compose「@Composable invocations can only happen from the context of a @Composable function」の解決方法

Jetpack ComposeでUIを実装していると、「@Composable invocations can only happen from the context of a @Composable function」というエラーが表示されることがあります。

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

このエラーは、@Composable関数を、ComposeのCompositionとして実行できない通常のKotlin関数やコールバックから直接呼び出している場合に発生します。

たとえば、通常関数からText()を呼んだり、ButtonのonClick内でText()やLaunchedEffect()を直接呼んだりすると発生します。

Jetpack Composeでは、rememberがComposition内に値を保持し、LaunchedEffectやrememberCoroutineScopeなどもCompositionのライフサイクルを利用します。そのため、Composable APIを呼び出せる場所と通常のKotlinコードを実行する場所を区別する必要があります。 :contentReference[oaicite:0]{index=0}

この記事では、このエラーが発生する原因、代表的な間違い、イベントハンドラでの正しい処理方法、rememberCoroutineScopeやLaunchedEffectの使い分けまで解説します。

  1. エラーの意味
  2. 代表的な発生状況
  3. 原因1:呼び出し元に@Composableが付いていない
  4. 解決方法1:UIを描画する関数に@Composableを付ける
  5. ただし何でも@Composableにすればよいわけではない
  6. 原因2:onClickの中でComposableを呼んでいる
  7. 解決方法2:状態を変更してUIを再コンポーズさせる
  8. なぜonClick内でText()を呼べないのか
  9. 原因3:onClick内でLaunchedEffectを呼んでいる
  10. 解決方法3:クリックイベントではrememberCoroutineScopeを使う
  11. LaunchedEffectとrememberCoroutineScopeの使い分け
  12. LaunchedEffectの正しい例
  13. rememberCoroutineScopeの正しい例
  14. 原因4:rememberを通常のコールバック内で呼んでいる
  15. 解決方法4:rememberはComposable本体で呼ぶ
  16. 原因5:通常ラムダとComposableラムダを混同している
  17. 解決方法5:Composableラムダに@Composableを付ける
  18. 引数付きComposableラムダ
  19. 原因6:ViewModelからComposableを呼んでいる
  20. 解決方法6:ViewModelはStateを公開し、Composableが描画する
  21. 原因7:RepositoryやUseCaseからComposableを呼んでいる
  22. 原因8:Composableを通常関数の戻り値として実行している
  23. 原因9:setContentの外側でComposableを呼んでいる
  24. 原因10:条件分岐そのものが問題だと思っている
  25. whenでもComposableを呼べる
  26. イベントでDialogを表示したい場合
  27. イベントでSnackbarを表示したい場合
  28. 画面表示時に一度だけ処理したい場合
  29. 値が変わったときに処理したい場合
  30. SideEffectもonClick内では呼ばない
  31. DisposableEffectもComposable内で使う
  32. よくある誤り1:クリック時に画面を直接生成する
  33. よくある誤り2:Composableから値を取得しようとする
  34. よくある誤り3:ComposableをViewModelへ渡す
  35. @Composableを付ければエラーが直るケース
  36. @Composableを付けないほうがよいケース
  37. Composableの呼び出し関係を確認する
  38. Composeは再コンポーズされることを前提にする
  39. 最短で原因を特定する手順
  40. ケース別の解決方法
  41. それでも解決しない場合
  42. まとめ

エラーの意味

Jetpack Composeでは、UIを構築する関数に@Composableを付けます。

@Composable fun Greeting() { Text("Hello") }

Text()自体もComposable関数なので、Compositionの中で呼び出す必要があります。

次のコードは正しくありません。

fun showGreeting() { Text("Hello") }

showGreeting()は通常のKotlin関数であり、Composableコンテキストではありません。

そのため、

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

というコンパイルエラーになります。

基本ルール:Text()、Button()、Column()、remember()、LaunchedEffect()などのComposable APIは、Composableとして実行される場所から呼び出します。

代表的な発生状況

  • 通常のKotlin関数からText()を呼んだ
  • 通常関数から自作の@Composable関数を呼んだ
  • ButtonのonClick内でComposableを呼んだ
  • rememberをイベントコールバック内で呼んだ
  • LaunchedEffectをonClick内で呼んだ
  • ViewModelからComposable関数を直接呼ぼうとした
  • RepositoryやUseCaseからComposableを呼ぼうとした
  • 通常のラムダ型() -> UnitへComposable関数を渡そうとした
  • Composableラムダに@Composableを付け忘れた
  • setContentの外側でComposable UIを直接実行しようとした

原因1:呼び出し元に@Composableが付いていない

もっとも基本的な原因です。

エラーになる例:

fun UserName(name: String) { Text(text = name) }

Text()はComposableなので、呼び出し元もComposableとして定義します。

@Composable fun UserName(name: String) { Text(text = name) }

解決方法1:UIを描画する関数に@Composableを付ける

その関数の目的がCompose UIを構築することであれば、@Composableを付けます。

@Composable fun ProfileScreen() { Text("Profile") }

自作Composableを呼ぶ場合も同じです。

@Composable fun AppScreen() { ProfileScreen() }

ただし何でも@Composableにすればよいわけではない

エラーが出るたびにすべての関数へ@Composableを付けるのはおすすめできません。

たとえばデータ計算だけを行う関数は通常関数のままで問題ありません。

fun calculateTotal(price: Int, count: Int): Int { return price * count }

そしてComposableから呼びます。

@Composable fun PriceScreen(price: Int, count: Int) { val total = calculateTotal(price, count)
Text(text = "Total: $total")

}

設計の基本:UIを記述する処理はComposable、計算・データ取得・ビジネスロジックは可能な限り通常のKotlinコードとして分離すると整理しやすくなります。

原因2:onClickの中でComposableを呼んでいる

非常によくあるケースです。

次のコードはエラーになります。

@Composable fun SampleScreen() { Button( onClick = { Text("Clicked") } ) { Text("Button") } }

ButtonのonClickは通常のイベントコールバックです。

クリックされた瞬間にComposableを直接追加する場所ではありません。

解決方法2:状態を変更してUIを再コンポーズさせる

クリック後にTextを表示したいなら、状態を変更します。

@Composable fun SampleScreen() { var clicked by remember { mutableStateOf(false) }
Column {
    Button(
        onClick = {
            clicked = true
        }
    ) {
        Text("Button")
    }

    if (clicked) {
        Text("Clicked")
    }
}

}

ComposeではStateが変化すると、そのStateを読み取っているComposableの再コンポーズがスケジュールされます。公式ドキュメントでも、rememberはComposition内に値を保持し、mutableStateOfはComposeと連携する監視可能なStateを作成すると説明されています。 :contentReference[oaicite:1]{index=1}

なぜonClick内でText()を呼べないのか

Composeは、命令的に「今ここへTextを追加する」というUIモデルではありません。

代わりに、現在のStateから「UIはどう見えるべきか」をComposableで宣言します。

状態: clicked = false ↓ Text("Clicked")を表示しない

状態:
clicked = true
↓
Text("Clicked")を表示する

イベントはStateを変更し、ComposeがそのStateをもとにUIを再構築します。

原因3:onClick内でLaunchedEffectを呼んでいる

次もエラーになります。

@Composable fun SampleScreen() { Button( onClick = { LaunchedEffect(Unit) { // suspend処理 } } ) { Text("Start") } }

LaunchedEffect自体がComposable関数だからです。

Android公式ドキュメントでも、LaunchedEffectはComposableであり、他のComposable関数内からのみ利用できると説明されています。 :contentReference[oaicite:2]{index=2}

解決方法3:クリックイベントではrememberCoroutineScopeを使う

クリックを契機としてCoroutineを開始したい場合は、Composable側でrememberCoroutineScope()を取得しておきます。

@Composable fun SampleScreen() { val scope = rememberCoroutineScope()
Button(
    onClick = {
        scope.launch {
            // suspend関数を呼ぶ
        }
    }
) {
    Text("Start")
}

}

公式ドキュメントでも、イベントハンドラからCoroutineを開始するときはrememberCoroutineScopeを利用する例が示されています。返されるCoroutineScopeは、そのComposableのComposition上の位置に関連付けられ、Compositionから外れるとキャンセルされます。 :contentReference[oaicite:3]{index=3}

LaunchedEffectとrememberCoroutineScopeの使い分け

大まかには次のように考えると分かりやすいです。

Composableが表示された Stateやkeyが変化した ↓ LaunchedEffect

ユーザーがButtonを押した
イベントが発生した
↓
rememberCoroutineScope + launch

LaunchedEffectはCompositionへ入ったときにCoroutineを起動し、指定したkeyが変わると既存Coroutineをキャンセルして再起動します。Compositionから外れるとCoroutineもキャンセルされます。 :contentReference[oaicite:4]{index=4}

LaunchedEffectの正しい例

@Composable fun UserScreen(userId: String) { LaunchedEffect(userId) { // userIdが変わったら再実行 loadUser(userId) }
Text("User")

}

rememberCoroutineScopeの正しい例

@Composable fun SaveButton() { val scope = rememberCoroutineScope()
Button(
    onClick = {
        scope.launch {
            saveData()
        }
    }
) {
    Text("Save")
}

}

原因4:rememberを通常のコールバック内で呼んでいる

次のようなコードも間違いです。

@Composable fun SampleScreen() { Button( onClick = { val state = remember { mutableStateOf(false) } } ) { Text("Button") } }

rememberはComposition内に値を保存するComposable APIなので、イベント発生時に新しく呼ぶことはできません。

Android公式ドキュメントでは、rememberで作成された値は初回Composition時に保存され、その後の再コンポーズで同じ値が返されると説明されています。 :contentReference[oaicite:5]{index=5}

解決方法4:rememberはComposable本体で呼ぶ

@Composable fun SampleScreen() { var enabled by remember { mutableStateOf(false) }
Button(
    onClick = {
        enabled = true
    }
) {
    Text("Enable")
}

if (enabled) {
    Text("Enabled")
}

}

原因5:通常ラムダとComposableラムダを混同している

通常のラムダ型は、

() -> Unit

です。

Composableを受け取るラムダは、

@Composable () -> Unit

と定義する必要があります。

解決方法5:Composableラムダに@Composableを付ける

エラーになる例:

@Composable fun Container( content: () -> Unit ) { content() }

contentへComposable UIを渡したいなら、次のようにします。

@Composable fun Container( content: @Composable () -> Unit ) { content() }

呼び出し:

@Composable fun Screen() { Container { Text("Hello") } }

引数付きComposableラムダ

@Composable fun UserContainer( content: @Composable (String) -> Unit ) { content("Alice") }

利用側:

@Composable fun Screen() { UserContainer { name -> Text(name) } }

原因6:ViewModelからComposableを呼んでいる

次のような設計は避けます。

class MainViewModel : ViewModel() {
fun showScreen() {
    MyScreen()
}

}

ViewModelはCompose UIを直接描画する場所ではありません。

解決方法6:ViewModelはStateを公開し、Composableが描画する

たとえば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")
}

}

このように、ViewModelがStateやイベント処理を担当し、ComposableがStateを読み取ってUIを構築する形にすると、Composeの単方向データフローと整合しやすくなります。

原因7:RepositoryやUseCaseからComposableを呼んでいる

RepositoryやUseCaseもUIレイヤーではありません。

class UserRepository {
fun showError() {
    Text("Error")
}

}

これは避けます。

Repositoryは結果やエラー情報を返します。

class UserRepository {
suspend fun loadUser(): Result<User> {
    // データ取得
}

}

そしてViewModelやComposableが結果に応じたUIを表示します。

原因8:Composableを通常関数の戻り値として実行している

次のような構造も問題になります。

fun getContent() { Greeting() }

Composableを返したい場合はComposableラムダとして返す方法があります。

fun getContent(): @Composable () -> Unit = { Greeting() }

または呼び出し側のComposableで条件分岐するほうが単純な場合もあります。

原因9:setContentの外側でComposableを呼んでいる

ActivityでCompose UIを開始する場合はsetContent内がCompositionの入口になります。

正しい例:

class MainActivity : ComponentActivity() {
override fun onCreate(
    savedInstanceState: Bundle?
) {
    super.onCreate(savedInstanceState)

    setContent {
        App()
    }
}

}

App()が@Composableなら、setContent内から呼び出します。

原因10:条件分岐そのものが問題だと思っている

Composable内の通常のifからComposableを呼ぶことは問題ありません。

@Composable fun Screen( loading: Boolean ) { if (loading) { Text("Loading...") } else { Text("Completed") } }

問題なのはifではなく、呼び出し元がComposableコンテキストかどうかです。

whenでもComposableを呼べる

@Composable fun ResultScreen( state: UiState ) { when (state) { UiState.Loading -> { Text("Loading") }
    UiState.Success -> {
        Text("Success")
    }

    UiState.Error -> {
        Text("Error")
    }
}

}

イベントでDialogを表示したい場合

次のようにはしません。

Button( onClick = { AlertDialog(...) } )

Stateを変更します。

@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")
        }
    )
}

}

イベントでSnackbarを表示したい場合

SnackbarHostState.showSnackbar()はsuspend関数なので、rememberCoroutineScopeを使用できます。

@Composable fun SnackbarSample() { val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()

Scaffold(
    snackbarHost = {
        SnackbarHost(snackbarHostState)
    }
) {
    Button(
        onClick = {
            scope.launch {
                snackbarHostState
                    .showSnackbar("Saved")
            }
        }
    ) {
        Text("Save")
    }
}

}

Android公式ドキュメントでも、ButtonのonClickからrememberCoroutineScopeで取得したScopeを使い、Snackbarを表示する例が示されています。 :contentReference[oaicite:6]{index=6}

画面表示時に一度だけ処理したい場合

イベントではなく、そのComposableがCompositionへ入ったときに処理したいならLaunchedEffectを使います。

@Composable fun ProfileScreen( viewModel: ProfileViewModel ) { LaunchedEffect(Unit) { viewModel.loadProfile() }
Text("Profile")

}

LaunchedEffectはCompositionへ入るとCoroutineを起動し、Compositionから外れるとそのCoroutineがキャンセルされます。 :contentReference[oaicite:7]{index=7}

値が変わったときに処理したい場合

@Composable fun UserScreen( userId: String ) { LaunchedEffect(userId) { loadUser(userId) } }

userIdが変わると、以前のEffectがキャンセルされ、新しいkeyで再実行されます。 :contentReference[oaicite:8]{index=8}

SideEffectもonClick内では呼ばない

SideEffectもComposable APIです。

次は誤りです。

Button( onClick = { SideEffect { // 処理 } } )

SideEffectは成功した再コンポーズ後にCompose StateをCompose外のオブジェクトへ反映する用途などに使用します。公式ドキュメントでも、成功した再コンポーズ後に実行されるEffectとして説明されています。 :contentReference[oaicite:9]{index=9}

DisposableEffectもComposable内で使う

Listener登録・解除など、Composableのライフサイクルに合わせてクリーンアップが必要ならDisposableEffectを利用できます。

@Composable fun LifecycleSample( lifecycleOwner: LifecycleOwner ) { DisposableEffect(lifecycleOwner) {
    val observer =
        LifecycleEventObserver { _, _ ->
            // 処理
        }

    lifecycleOwner.lifecycle
        .addObserver(observer)

    onDispose {
        lifecycleOwner.lifecycle
            .removeObserver(observer)
    }
}

}

公式ドキュメントでは、DisposableEffectはkeyが変わった場合やCompositionから外れた場合にクリーンアップを行うEffectとして説明されています。 :contentReference[oaicite:10]{index=10}

よくある誤り1:クリック時に画面を直接生成する

誤り:

Button( onClick = { DetailScreen() } )

画面遷移ならNavigationを利用するか、Stateを変更します。

Button( onClick = { navController.navigate("detail") } )

navigate()は通常関数なのでイベントハンドラから呼べます。

よくある誤り2:Composableから値を取得しようとする

Composableは単なる値取得関数として扱わないほうが安全です。

たとえば、通常関数からテーマの値を取得する目的で、

fun getColor(): Color { return MaterialTheme.colorScheme.primary }

とすると、MaterialTheme.colorSchemeへのアクセスがComposableコンテキストを要求する場合にエラーになります。

Composable側で取得して通常関数へ渡します。

@Composable fun Screen() { val color = MaterialTheme.colorScheme.primary
Content(color)

}

fun Content(color: Color) {
// 通常処理
}

よくある誤り3:ComposableをViewModelへ渡す

ViewModelにUIそのものを保持させるより、表示に必要なデータを保持させます。

data class UiState( val loading: Boolean = false, val message: String? = null )

ComposableはそのStateを読み取ります。

@Composableを付ければエラーが直るケース

次の関数の役割がUI描画であれば、@Composableを付けるのが正しい解決です。

fun Title() { Text("Title") }

修正:

@Composable fun Title() { Text("Title") }

@Composableを付けないほうがよいケース

イベント処理やビジネスロジックまでComposableにする必要はありません。

fun validateName( name: String ): Boolean { return name.isNotBlank() }

このような関数は通常関数として利用します。

Composableの呼び出し関係を確認する

エラー箇所から呼び出し元をさかのぼってください。

@Composable Screen() ↓ @Composable Content() ↓ @Composable UserCard() ↓ Text()

この流れなら問題ありません。

一方、途中で、

@Composable Screen() ↓ onClick { ... } ↓ Text()

のように通常コールバックへ入ると、その中はComposableコンテキストではありません。

Composeは再コンポーズされることを前提にする

Composable関数はStateの変化によって再実行されることがあります。

Android公式ドキュメントでは、CompositionはComposableを実行して構築されるUIの記述であり、State変化に応じてComposableを再実行することをRecompositionと説明しています。 :contentReference[oaicite:11]{index=11}

そのため、UI表示を変更したい場合は「イベント内でUI関数を直接呼ぶ」のではなく、Stateを変更する設計が基本になります。

最短で原因を特定する手順

  1. エラーになっている関数が@Composableか確認する
  2. 呼び出している関数がComposable APIか確認する
  3. onClickや通常のコールバック内ではないか確認する
  4. UI変更ならStateへ置き換える
  5. suspend処理ならLaunchedEffectかrememberCoroutineScopeを選ぶ
  6. ラムダ引数なら@Composable () -> Unitが必要か確認する
  7. ViewModelやRepositoryからComposableを呼んでいないか確認する

ケース別の解決方法

通常関数からText()を呼ぶ → UI関数なら@Composableを付ける

onClickからText()を呼ぶ
→ Stateを変更してComposable側で表示する

onClickからLaunchedEffectを呼ぶ
→ rememberCoroutineScope + launchを使う

通常ラムダへComposableを渡す
→ @Composable () -> Unitにする

ViewModelからComposableを呼ぶ
→ ViewModelはStateを更新し、UI側で描画する

画面表示時にsuspend処理を実行
→ LaunchedEffectを使う

それでも解決しない場合

エラーが続く場合は、次を確認してください。

  • 呼び出し元に@Composableが付いているか
  • Text、Button、Columnなどを通常関数から呼んでいないか
  • rememberをイベントハンドラ内で呼んでいないか
  • LaunchedEffectをonClick内で呼んでいないか
  • SideEffectやDisposableEffectを通常コールバックから呼んでいないか
  • Composableラムダの型に@Composableが付いているか
  • ViewModelからUIを直接呼んでいないか
  • RepositoryからUIを直接呼んでいないか
  • クリック後のUI変更をStateで表現しているか
  • Coroutineの起動方法が適切か

まとめ

Jetpack Composeの「@Composable invocations can only happen from the context of a @Composable function」は、Composable関数をComposableとして実行できない場所から呼び出したときに発生します。

通常関数からUIを描画する場合は、UIを担当する関数へ@Composableを付けます。

@Composable fun Greeting() { Text("Hello") }

ただし、onClickのようなイベントコールバック内ではComposableを直接呼びません。

onClick = { showMessage = true }

のようにStateを変更し、Composable側で、

if (showMessage) { Text("Message") }

と表示します。ComposeではStateの変化によって再コンポーズが行われるため、この方法が基本です。 :contentReference[oaicite:12]{index=12}

クリックイベントからsuspend関数を実行する場合は、

val scope = rememberCoroutineScope()

Button(
onClick = {
scope.launch {
saveData()
}
}
) {
Text("Save")
}

のようにします。Android公式ドキュメントでも、イベントハンドラからCoroutineを起動する用途としてrememberCoroutineScopeが案内されています。 :contentReference[oaicite:13]{index=13}

一方、Composableの表示やkeyの変化に連動して処理を実行する場合はLaunchedEffectを使用します。LaunchedEffectはCompositionへ入ったときにCoroutineを開始し、keyが変わると再起動され、Compositionから外れるとキャンセルされます。 :contentReference[oaicite:14]{index=14}

このエラーを見つけたら、単に@Composableを追加するだけではなく、「この処理はUIの宣言なのか、イベント処理なのか、状態変更なのか、副作用なのか」を切り分けると、正しい修正方法を選びやすくなります。