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:呼び出し元に@Composableが付いていない
- 解決方法1:UIを描画する関数に@Composableを付ける
- ただし何でも@Composableにすればよいわけではない
- 原因2:onClickの中でComposableを呼んでいる
- 解決方法2:状態を変更してUIを再コンポーズさせる
- なぜonClick内でText()を呼べないのか
- 原因3:onClick内でLaunchedEffectを呼んでいる
- 解決方法3:クリックイベントではrememberCoroutineScopeを使う
- LaunchedEffectとrememberCoroutineScopeの使い分け
- LaunchedEffectの正しい例
- rememberCoroutineScopeの正しい例
- 原因4:rememberを通常のコールバック内で呼んでいる
- 解決方法4:rememberはComposable本体で呼ぶ
- 原因5:通常ラムダとComposableラムダを混同している
- 解決方法5:Composableラムダに@Composableを付ける
- 引数付きComposableラムダ
- 原因6:ViewModelからComposableを呼んでいる
- 解決方法6:ViewModelはStateを公開し、Composableが描画する
- 原因7:RepositoryやUseCaseからComposableを呼んでいる
- 原因8:Composableを通常関数の戻り値として実行している
- 原因9:setContentの外側でComposableを呼んでいる
- 原因10:条件分岐そのものが問題だと思っている
- whenでもComposableを呼べる
- イベントでDialogを表示したい場合
- イベントでSnackbarを表示したい場合
- 画面表示時に一度だけ処理したい場合
- 値が変わったときに処理したい場合
- SideEffectもonClick内では呼ばない
- DisposableEffectもComposable内で使う
- よくある誤り1:クリック時に画面を直接生成する
- よくある誤り2:Composableから値を取得しようとする
- よくある誤り3:ComposableをViewModelへ渡す
- @Composableを付ければエラーが直るケース
- @Composableを付けないほうがよいケース
- Composableの呼び出し関係を確認する
- Composeは再コンポーズされることを前提にする
- 最短で原因を特定する手順
- ケース別の解決方法
- それでも解決しない場合
- まとめ
エラーの意味
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を変更する設計が基本になります。
最短で原因を特定する手順
- エラーになっている関数が
@Composableか確認する - 呼び出している関数がComposable APIか確認する
onClickや通常のコールバック内ではないか確認する- UI変更ならStateへ置き換える
- suspend処理なら
LaunchedEffectかrememberCoroutineScopeを選ぶ - ラムダ引数なら
@Composable () -> Unitが必要か確認する - 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の宣言なのか、イベント処理なのか、状態変更なのか、副作用なのか」を切り分けると、正しい修正方法を選びやすくなります。
