0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Android】Jetpack ComposeにおけるLaunchedEffectとDisposableEffectの使い分け

0
Posted at

はじめに

Compose の副作用 API の中でも、LaunchedEffectDisposableEffect は混同されやすい存在です。

LaunchedEffectDisposableEffect はどちらも「Composable が画面に出ている/消える」タイミングに連動して何かを実行する、という点では似ています。しかし、

  • LaunchedEffectコルーチン(非同期処理) を起動するためのもの
  • DisposableEffect後片付け(クリーンアップ)が必須な同期的な副作用 を扱うためのもの

という明確な役割分担があります。この違いを理解していないと、

  • リスナーの登録を解除し忘れてメモリリークする
  • BroadcastReceiver が画面を離れても登録されたままになる
  • センサーの購読が二重登録される

といった不具合につながります。本記事ではこの2つの違いを実例ベースで整理します。

結論を先に

LaunchedEffect DisposableEffect
主目的 コルーチンの起動 登録⇄解除が対になるリソース管理
中身 suspend 関数を呼べる suspend 関数は呼べない(通常の同期処理)
終了時の処理 コルーチンが自動でキャンセルされる onDispose { }明示的に書く必要がある
典型用途 API呼び出し、Flow監視、アニメーション開始 リスナー登録、Broadcast受信登録、ライフサイクル監視
key 変更時の挙動 古いコルーチンをキャンセルし再実行 onDispose を呼んでから再実行

一言でまとめると、

「非同期処理を始めたいだけ」なら LaunchedEffect、「登録したら必ず解除しないといけないもの」なら DisposableEffect

です。

LaunchedEffect: コルーチンを起動して、あとはCompose任せ

LaunchedEffect の中では delaycollect のような 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)」がペアになっているものが数多くあります。BroadcastReceiverSensorEventListenerLocationListenerLifecycle.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 のライフサイクルに安全に副作用を紐付けるための仕組みです。目的に応じて正しく使い分けることで、リークのない堅牢な画面を作ることができます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?