DisposableEffect
DisposableEffectは、コンポーザブルの表示開始(入場)に合わせてリスナーの登録などを行い、非表示(退場)時にそれらを確実にクリーンアップ(破棄・解放)するための副作用APIです。
LaunchedEffectとは異なり、コルーチン(サスペンド関数)の実行はできませんが、画面が閉じられたときやキーが変わったときに必ず後処理(onDispose)を走らせることができます。
🛠 基礎的な使い方
DisposableEffectの末尾には、必ず onDispose { ... } を記述する必要があります。
@Composable
fun MyComponent(keyData: String) {
// keyData が変更されるか、コンポーザブルが破棄されると実行される
DisposableEffect(keyData) {
// 1. コンポーザブル入場時、またはキー変更時の処理
println("登録処理を実行: $keyData")
// 2. 必ず末尾にクリーンアップ処理を記述
onDispose {
println("解放処理を実行: $keyData")
}
}
}
💡 代表的な2つのユースケース
1. ライフサイクルイベント(onResumeなど)を監視する
Androidの画面状態(Lifecycle.Event)を検知し、画面を離れる際にオブザーバーを正しく解除します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Composable fun LifecycleObserverExample( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current ) { DisposableEffect(lifecycleOwner) { // オブザーバーの作成 val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { println("画面が表示されました(onResume)") } } // ライフサイクルへの登録 lifecycleOwner.lifecycle.addObserver(observer) // 画面破棄時、または別のlifecycleOwnerに変わった時に登録を解除 onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } } |
2. BroadcastReceiver などのリスナー登録と解除
システムイベントを検知するレシーバーを登録し、アプリの画面が切り替わるタイミングで解除してメモリリークを防ぎます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Composable fun SystemAirplaneModeObserver(context: Context) { DisposableEffect(context) { val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { println("飛行機モードが変更されました") } } // フィルターを設定してレシーバーを登録 context.registerReceiver(receiver, IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)) // コンポーザブル破棄時に必ず登録解除 onDispose { context.unregisterReceiver(receiver) } } } |
⚠️ 知っておくべき仕様と注意点
onDisposeの強制: ブロックの最後にonDisposeを記述しないと、コンパイルエラーになります。- キー(Key)の再実行ルール: 引数に渡した
keyの値が変わると、古いonDisposeが実行されてクリーンアップされ、新しい値で再度初期化処理が実行されます。 - 一度だけ実行したい場合: 画面表示から消滅まで1度だけ登録・解除したい場合は、
DisposableEffect(Unit)のように定数をキーに指定します。 - 最新の状態を参照したい場合: エフェクト内で最新の引数やStateを参照しつつ、エフェクト自体は再起動させたくない場合は、
rememberUpdatedStateと組み合わせて使用します。
付ける位置はColumnとかの中
LaunchedEffect
Jetpack Composeの LaunchedEffect は、コンポーザブルのライフサイクルに合わせて安全に非同期処理(コルーチン)を実行するための副作用(Side-Effect)APIです。
通常のコンポーザブル関数は再コンポジション(画面更新)によって何度も実行されるため、ネットワーク通信や画面遷移などの処理を直接書くと意図しない無限ループや重複処理が発生します。LaunchedEffect を使うことで、これらの処理を適切に制御できます。
💡 主な効果と特徴
- コルーチンスコープの自動生成
- ライフサイクルに連動したキャンセル
- キー(Key)による実行制御
🛠 基本的な使い方(パターン別)
1. 画面表示時に「1度だけ」実行したい場合(初期化など)
画面の初回表示時(Composition時)のみ実行し、その後の再コンポジションでは実行させない場合は、キーに Unit や true などの固定値を指定します。
|
1 2 3 4 5 6 7 8 9 |
@Composable fun MyScreen(viewModel: MyViewModel) { // 画面が表示された時に1度だけデータを読み込む LaunchedEffect(Unit) { viewModel.loadInitialData() } // UIの描画処理... } |
2. 特定の状態が変化した時に「再実行」したい場合
状態(State など)をキーに渡すと、その値が変わるたびに内部のコルーチンがリスタートします。
|
1 2 3 4 5 6 7 8 9 10 |
@Composable fun SearchScreen(searchQuery: String, viewModel: MyViewModel) { // ユーザーが検索ワード(searchQuery)を入力・変更するたびにAPIを叩く LaunchedEffect(searchQuery) { if (searchQuery.isNotEmpty()) { delay(500) // デバウンス処理(連続入力を待つ) viewModel.performSearch(searchQuery) } } } |
3. 1回限りのイベント(One-shot Event)を検知して処理する場合
ViewModelからの画面遷移の指示や、スナックバーの表示要求をUI側で受け取る際によく使われます。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
@Composable fun LoginScreen(viewModel: LoginViewModel, snackbarHostState: SnackbarHostState) { // UIイベントのFlowを購読する LaunchedEffect(viewModel.uiEvent) { viewModel.uiEvent.collect { event -> when (event) { is UiEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.message) is UiEvent.NavigateToHome -> /* 画面遷移処理 */ } } } } |
⚠️ 使用する際の注意点
- 重い処理を直接書かない
- ブロック内で使う変数はキーに含める
使い分け方
LaunchedEffect と DisposableEffect の最大の違いは、「非同期処理(コルーチン)を実行したいか」それとも「クリーンアップ(後処理)が必要な処理を行いたいか」です。
一言で言えば、「やりっぱなし(自動キャンセル任せ)でいいなら LaunchedEffect」、「自分で後始末(リスナーの解除など)が必要なら DisposableEffect」と使い分けます。
📊 比較一覧
| 項目 | LaunchedEffect | DisposableEffect |
|---|---|---|
| 主な目的 | 非同期処理、サスペンド関数の実行 | リスナーの登録・解除、ライフサイクル連動 |
| コルーチン | 利用できる(ブロックがサスペンドスコープ) | 利用できない(非同期処理は書けない) |
| クリーンアップ | 自動(画面消滅時にコルーチンがキャンセル) | 必須(onDispose ブロックの記述が強制) |
🛠 2つの使い分けパターンと具体例
1. LaunchedEffect を使うべきケース(非同期・やりっぱなし)
コンポーザブルの消滅時に、実行中の処理(通信やアニメーションなど)が自動でキャンセルされればそれで十分な場合に使います。
- 例:画面表示時のAPI通信、タイマーカウント、アニメーションの開始
|
1 2 3 4 5 6 7 8 9 10 11 12 |
@Composable fun AutoFadingText() { val alpha = remember { Animatable(0f) } // 画面表示時にフェードインアニメーションを開始する(サスペンド関数) // 画面が閉じられれば、アニメーション処理は自動的にストップする LaunchedEffect(Unit) { alpha.animateTo(1f, animationSpec = tween(1000)) } Text("こんにちは", modifier = Modifier.alpha(alpha.value)) } |
2. DisposableEffect を使うべきケース(後始末が必須)
Androidシステムや外部ライブラリに対して、「登録(Subscribe)」と「解除(Unsubscribe)」をペアで行う必要がある場合に使います。
※末尾に onDispose { ... } の記述が必須(コンパイルエラーになります)。
- 例:イベントリスナーの追加/削除、BroadcastReceiverの登録/解除、センサーの監視、ライフサイクルイベントの監視
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Composable fun SystemLocationObserver(context: Context, onLocationChanged: (Location) -> Unit) { // 位置情報マネージャーの例 val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager val listener = LocationListener { location -> onLocationChanged(location) } DisposableEffect(Unit) { // 1. 画面が表示されたら(またはキーが変わったら)リスナーを登録 locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000L, 1f, listener) // 2. 画面から消える(またはキーが変わる)直前に、必ず呼び出される後始末 onDispose { locationManager.removeUpdates(listener) // これを忘れるとメモリリークする! } } } |
⚠️ よくある間違い
DisposableEffect の中で非同期処理(コルーチン)を行おうとして、自前の CoroutineScope を作って起動(launch)するコードを見かけることがありますが、これはアンチパターンです。
画面消滅時(onDispose)に手動でコルーチンをキャンセルするコードを書くくらいなら、最初から LaunchedEffect を使用してください。