はじめに
Compose の副作用 API の中でも、LaunchedEffect と DisposableEffect は混同されやすい存在です。
LaunchedEffect と DisposableEffect はどちらも「Composable が画面に出ている/消える」タイミングに連動して何かを実行する、という点では似ています。しかし、
-
LaunchedEffectは コルーチン(非同期処理) を起動するためのもの -
DisposableEffectは 後片付け(クリーンアップ)が必須な同期的な副作用 を扱うためのもの
という明確な役割分担があります。この違いを理解していないと、
- リスナーの登録を解除し忘れてメモリリークする
- BroadcastReceiver が画面を離れても登録されたままになる
- センサーの購読が二重登録される
といった不具合につながります。本記事ではこの2つの違いを実例ベースで整理します。
結論を先に
LaunchedEffect |
DisposableEffect |
|
|---|---|---|
| 主目的 | コルーチンの起動 | 登録⇄解除が対になるリソース管理 |
| 中身 | suspend 関数を呼べる | suspend 関数は呼べない(通常の同期処理) |
| 終了時の処理 | コルーチンが自動でキャンセルされる |
onDispose { } に明示的に書く必要がある |
| 典型用途 | API呼び出し、Flow監視、アニメーション開始 | リスナー登録、Broadcast受信登録、ライフサイクル監視 |
| key 変更時の挙動 | 古いコルーチンをキャンセルし再実行 |
onDispose を呼んでから再実行 |
一言でまとめると、
「非同期処理を始めたいだけ」なら
LaunchedEffect、「登録したら必ず解除しないといけないもの」ならDisposableEffect
です。
LaunchedEffect: コルーチンを起動して、あとはCompose任せ
LaunchedEffect の中では delay や collect のような suspend 関数を呼び出せます。コンポジションから離脱したり key が変わったりすると、Compose が起動中のコルーチンを自動的にキャンセルしてくれるため、後片付け用のコードを自分で書く必要がありません。
@Composable
fun MessageScreen(chatId: String, viewModel: ChatViewModel) {
LaunchedEffect(chatId) {
viewModel.messagesFlow(chatId).collect { messages ->
viewModel.updateMessages(messages)
}
}
...
}
collect はコルーチンがキャンセルされた時点で自動的に止まるので、「監視をやめる」という後片付けを意識する必要がありません。これが LaunchedEffect の得意分野です。
DisposableEffect: 「登録したら必ず解除する」を保証する
一方、Android の API には suspend 関数ではなく、コールバック方式で「登録(register)」と「解除(unregister)」がペアになっているものが数多くあります。BroadcastReceiver、SensorEventListener、LocationListener、Lifecycle.addObserver などがその代表です。
これらは非同期処理(コルーチン)ではないため LaunchedEffect の恩恵(自動キャンセル)を受けられません。登録した側が責任を持って解除しないとリークします。 そのために用意されているのが DisposableEffect です。
実例: BroadcastReceiver の登録/解除
@Composable
fun NetworkStatusObserver(context: Context, onStatusChanged: (Boolean) -> Unit) {
DisposableEffect(context) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val isConnected = intent.getBooleanExtra("connected", false)
onStatusChanged(isConnected)
}
}
val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
context.registerReceiver(receiver, filter)
onDispose {
context.unregisterReceiver(receiver)
}
}
}
DisposableEffect のブロックの最後には、必ず onDispose { } を書く必要があります(これはコンパイル時に強制されます)。ここに書いた処理は、
-
key(この例ではcontext)が変わったとき - Composable がコンポジションから離脱したとき
に必ず呼ばれます。これにより「登録したら必ず解除される」ことが保証されます。
実例: LifecycleObserver を使った画面のフォアグラウンド検知
@Composable
fun rememberIsAppForeground(lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): State<Boolean> {
val isForeground = remember { mutableStateOf(true) }
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> isForeground.value = true
Lifecycle.Event.ON_PAUSE -> isForeground.value = false
else -> Unit
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
return isForeground
}
addObserver に対して removeObserver が確実にペアで呼ばれる構造になっている点がポイントです。
ありがちなアンチパターン
アンチパターン1: 解除処理が必要なリスナー登録を LaunchedEffect で書いてしまう
@Composable
fun BadSensorObserver(sensorManager: SensorManager, listener: SensorEventListener) {
LaunchedEffect(Unit) {
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
// ここで解除するタイミングがない!
}
}
LaunchedEffect にはキャンセル時に呼ばれる仕組み(onDispose 相当)がありません。コルーチンがキャンセルされても registerListener はキャンセルの影響を受けないただの同期呼び出しなので、解除されないまま放置されます。こうしたペア処理は DisposableEffect の役目です。
アンチパターン2: suspend関数を呼びたいだけなのに DisposableEffect を使う
@Composable
fun BadDataLoader(viewModel: SomeViewModel) {
DisposableEffect(Unit) {
// suspend関数を直接呼べないため、結局 launchするか GlobalScope を使う羽目になる
GlobalScope.launch { viewModel.load() }
onDispose { }
}
}
DisposableEffect のブロックは通常の同期コードとして実行されるため、その場で suspend 関数を呼ぶことはできません。GlobalScope のようなコンポジションに紐づかないスコープを使うと、画面を離れても処理が生き続けてしまいます。単に非同期処理を開始したいだけなら素直に LaunchedEffect を使うべきです。
まとめ
-
LaunchedEffectは コルーチンを起動するためのAPIで、キャンセルはCompose任せにできる -
DisposableEffectは 登録と解除が対になる同期的な副作用を扱うためのAPIで、onDisposeに解除処理を明示的に書く必要がある - 「非同期処理を始めたいだけ」か「後片付けが必要なリソースを扱っているか」で判断すると迷いにくい
どちらも Compose のライフサイクルに安全に副作用を紐付けるための仕組みです。目的に応じて正しく使い分けることで、リークのない堅牢な画面を作ることができます。