DisposableEffect は、明示的な初期化とその後のリソースクリーニングが必要な操作のために設計された、Jetpack Compose の composable 関数です。他の side-effect API とは異なり、DisposableEffect は onDispose ブロックを提供し、コンポーネントがコンポジションを退出する際やキーが変わった際に実行されることが保証されます。これにより、ネイティブサブスクリプション、センサーリスナー、ハードウェアリソースの作業に不可欠です。Android Developers Documentation (2025) によると、DisposableEffect は Activity ライフサイクルの onStart/onStop に似た setup/teardown ペアが必要なあらゆるシナリオで使用することが推奨されています。
まとめ
DisposableEffect は Jetpack Compose におけるリソース管理のための重要なツールです。その主な特徴は、composable コンポーネントのライフサイクルが終了したときに onDispose ブロックが保証されて呼び出されることです。この行動は Android 開発において重要で、システムサービスへの閉じられないサブスクリプションはメモリーリークやアプリケーションのクラッシュの原因となりうるためです。
非同期コルーチンコンテキストで動作する LaunchedEffect とは異なり、DisposableEffect は同期的に実行されます。これは、その中で suspend 関数を呼び出せないことを意味します。同期的な実行は予測可能性を保証します: 初期化コードが最初のレンダリングの前に実行され、クリーンアップコードがコンポーネントがメモリから削除される前に実行されることを確信できます。
Jetpack Compose ドキュメンテーション (2025) によると、DisposableEffect は 4 つの主なシナリオで使用すべきです: (1) システムサービスへのサブスクライブ (センサー、LocationManager)、(2) BroadcastReceiver の登録、(3) コルーチンをサポートしない callback ベースのライブラリへの対応、(4) AndroidView を介して Compose コンポーネントをレガシー View システムに結合すること。
class SensorManager(private val context: Context) {
fun startListening(callback: (Float) -> Unit) { /* register */ }
fun stopListening() { /* cancel */ }
}
@Composable
fun SensorDisplay() {
val sensorManager = remember { SensorManager(context) }
var value by remember { mutableStateOf(0f) }
DisposableEffect(Unit) {
sensorManager.startListening { value = it }
onDispose { sensorManager.stopListening() }
}
Text("センサー: $value")
}
DisposableEffect の内部メカニズムは、コンポジションライフサイクルのフェーズに基づいています。composable コンポーネントがコンポジションに入ると、DisposableEffect は渡されたコードブロックを実行します。このブロックは onDispose ラムダを含む DisposableEffectResult オブジェクトを返します。コンポジションはこの結果を保存し、コンポーネントがコンポジションを退出する際に onDispose を呼び出します — 理由に関わらず (ナビゲーション、親状態の変更、LazyColumn からの削除)。
DisposableEffect における キーの仕組みは LaunchedEffect と同様に動作します: いずれかのキーが変わると、まず onDispose が旧状態に対して実行され、その後に初期化ブロックが新しいキーで再実行されます。これにより、パラメータが変わった際にリソースを再構成できます。例えば、キーがソケット URL の場合、それが変わると旧ソケットが閉じられ、新しいソケットが開かれます。
重要: onDispose ブロックは DisposableEffect の必須要素です。ブロック内で onDispose を呼び出さないと、コードはコンパイルされません。このコンパイラの要件により、開発者がリソースのクリーンアップを忘れることがなくなり、マニュアルなサブスクリプション管理におけるミスの共通原因を防ぐことができます。
// キーを使った正しい使い方
DisposableEffect(sensorType) {
val sensor = sensorManager.getDefaultSensor(sensorType)
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
onDispose {
sensorManager.unregisterListener(listener)
}
}
// 一つの DisposableEffect での複数のリソース
DisposableEffect(Unit) {
context.registerReceiver(receiver, intentFilter)
lifecycle.addObserver(observer)
onDispose {
context.unregisterReceiver(receiver)
lifecycle.removeObserver(observer)
}
}
Android アプリケーションでのメモリーリークは、画面が閉じられた後も Activity または Context への参照を保持し続ける、登録されていないリスナーやサブスクリプションが原因となりがちです。DisposableEffect はこの問題をフレームワークレベルで解決します: 開発者がリスナーを登録するために DisposableEffect を使用すると、onDispose はいかなるコンポーネント終了シナリオでもサブスクリプションをキャンセルすることを保証します。
これは特に LazyColumn や LazyGrid において重要で、ユーザーがスクロールすると項目が継続的に作成されては破壊されます。DisposableEffect がない場合、可視領域から消えた各項目はアクティブなサブスクリプションを残します。DisposableEffect では、アンロードされた各項目に対して onDispose が呼び出され、項目が画面から消えた直後にリソースが解放されることが保証されます。
Android Performance Patterns (Google, 2025) によると、すべてのネイティブサブスクリプションに DisposableEffect を使用すると、ライフサイクル callback によるマニュアル管理と比較して、Compose アプリケーションでのメモリーリークの数が 60–70% 減少します。システム自体がコンポーネントがコンポジションを退出する瞬間を追跡し、緊急画面閉鎖の際にも onDispose の実行を保証します。
| リソース | DisposableEffect の動作 | DisposableEffect なし |
|---|---|---|
| BroadcastReceiver | register + onDispose → unregister | レシーバーがアクティブのまま |
| SensorManager | registerListener + onDispose → unregisterListener | センサーがデータを送り続ける |
| Observable (Flow 以外) | subscribe + onDispose → unsubscribe | Callback が参照を保持する |
| TextureView / SurfaceView | setCallback + onDispose → removeCallback | Callback リーク |
| Socket / Channel | open + onDispose → close | 接続が開いたまま |
DisposableEffect の使用例のうち最も具体的な例のひとつは、デバイスセンサー (加速度センサー、ジャイロスコープ、磁気センサー) を扱うことです。センサーは作業終了時に必ず登録解除が必要で、そうしないと画面を閉じた後もバッテリーを消費し続け、データを送り続けます。
実用例: 傾き角度測定アプリケーション。DisposableEffect(Unit) はコンポーネントが現れたときに加速度センサーリスナーを登録し、onDispose で登録を解除します。センサーデータは mutableStateOf を介して状態に渡され、UI を自動的に更新します。画面が LazyColumn でスクロールし、項目が消えると、onDispose が即刻に発火します — センサーはその項目に対してデータを送るのを停止します。
センサーの種類を切り替える際 (例: 加速度センサーからジャイロスコープへ)、sensorType キーが変わり、onDispose が旧サブスクリプションをキャンセルし、新しい DisposableEffect ブロックが新しいセンサーを登録します。キーがない場合、以前に登録されたセンサーを手動で確認し、正しいリスナーで unregisterListener を呼び出す必要があり、エラーを産みやすくなります。
@Composable
fun SensorReadingScreen(sensorType: Int) {
val context = LocalContext.current
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
var sensorValue by remember { mutableStateOf(0f) }
DisposableEffect(sensorType) {
val sensor = sensorManager.getDefaultSensor(sensorType)
val listener = SensorEventListener { event, _ ->
sensorValue = event.values[0]
}
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
onDispose {
sensorManager.unregisterListener(listener)
}
}
Text("値: $sensorValue")
}
BroadcastReceiver は、必須の register / unregister ペアが必要な API の典型的な例です。Compose アプリケーションでは、DisposableEffect は特定の画面のライフタイムにわたってレシーバーを登録するのに理想的です。画面に入ると、必要な IntentFilter を持つ BroadcastReceiver が登録されます; 退出すると、onDispose で自動的にキャンセルされます。
一般的なシナリオは ネットワーク状態監視です。DisposableEffect は ConnectivityManager のレシーバーを登録し、ネットワーク接続の変化を通知します。ステータスが変わると (WiFi / モバイルデータ / ネットワークなし)、composable の状態が更新され、UI に対応する指示器が表示されます。画面が閉じられると、onDispose は登録解除を保証します — アプリがバックグラウンドに行っても同様です。
RECEIVER_EXPORTED / RECEIVER_NOT_EXPORTED フラグ (Android 14+) を使用した ContextCompat.registerReceiver を使用するレシーバーの場合、システムがレシーバーのスコープを明示的に指定する必要があるため、DisposableEffect の使用が必須になります。DisposableEffect はスコープが画面のライフタイムに限定されることを確保し、新しい Android バージョンのセキュリティ要件と合一します。
@Composable
fun NetworkStatusBanner() {
val context = LocalContext.current
var isConnected by remember { mutableStateOf(true) }
DisposableEffect(Unit) {
val receiver = BroadcastReceiver { _, _ ->
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
isConnected = cm.getActiveNetwork() != null
}
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).let { filter ->
context.registerReceiver(receiver, filter)
}
onDispose {
context.unregisterReceiver(receiver)
}
}
if (!isConnected) { ... }
}
1 つ目の重大な趣い込み試鑑は onDispose の呼び出し不足です。DisposableEffect ブロック内のコードは onDispose を呼び出す必要があり、そうしないとコンパイルエラーが発生します。しかし、開発者は時に onDispose を条件内に置いてこれを回避しようとします: if (condition) { onDispose { ... } }。このコードはコンパイルされますが、条件が満たされない場合 onDispose は登録されません — リソースは絶対に解放されません。
2 つ目の趣い込み試鑑は 非同期操作に DisposableEffect を使用することです。DisposableEffect は同期的であるため、その中で delay() や await() を書くことはできません。その後のクリーンアップを伴う非同期初期化が必要な場合は、LaunchedEffect (データ読み込み用) と DisposableEffect (ネイティブリソースの設定/クリーンアップ用) を組み合わせるか、rememberCoroutineScope を使った別のメカニズムを使用します。
3 つ目の趣い込み試鑑は remember なしで DisposableEffect 内に新しいオブジェクトを作成することです。オブジェクト (センサー、listener、receiver) が呼び出しごとにエフェクト内で作成され、キーが頻繁に変更されると、過剰なオブジェクト作成とガベージコレクションを引き起こします。オブジェクト作成は DisposableEffect の外の remember または remember { ... } に移動し、エフェクト内では登録と解除のみを行うべきです。
よくある質問
DisposableEffect は同期的に動作し、リソースの明示的なクリーンアップのために onDispose を提供します。LaunchedEffect はコルーチン内で非同期的に動作し、キーが変わったときやコンポジションを退出するときに自動的にキャンセルされます。リソースがクリーンアップメソッド (close, unregister, dispose) の呼び出しを必要とする場合は DisposableEffect を使用します。操作が suspend 関数の場合は LaunchedEffect を使用します。
はい、onDispose は必須です — Kotlin コンパイラは DisposableEffect ブロック内でそれを呼び出すよう必須です。onDispose を呼び出さない場合、コードはコンパイルされません。これは、開発者が忘れるのを防ぎ、コンポジションを退出するときに開かれたすべてのリソースがクローズされることを確保するために意図的に設計されています。
DisposableEffect ブロック内で try-catch を使用します。リソースの登録が例外をスローする可能性がある場合 (例: センサーが見つからない)、それを try で囲み、別の状態を介して UI でエラーを処理します。onDispose は初期化の成功に関わらず呼び出すべきです — finally ブロックまたは try セクションの最尾に置いてください。
推奨されません。Flow には、collectLatest を使用した LaunchedEffect または Lifecycle.repeatOnLifecycle との .collectAsState() メソッドを使用することを推奨します。DisposableEffect は suspend 関数をサポートしていないため、その内部で Flow にサブスクライブするには CoroutineScope を介して別のコルーチンを起動する必要があり、コードが複雑になり、リークのリスクが高まります。
制限はありませんが、関連するリソースは 1 つの DisposableEffect に複数の操作と 1 つの onDispose でまとめることが推奨されます。リソースが独立している場合 (例: センサーと BroadcastReceiver)、異なるキーを持つ別々の DisposableEffects に分けることが望ましいです — これによりデバッグが簡素化され、あるキーが変わったときにすべてのリソースが再作成されるのを防げます。
統合
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。