DisposableEffect — Jetpack Compose でのリソース解放

著者: IT Sectr 公開日: 2026-06-30 読了時間: 9 分

DisposableEffect は、明示的な初期化とその後のリソースクリーニングが必要な操作のために設計された、Jetpack Compose の composable 関数です。他の side-effect API とは異なり、DisposableEffect は onDispose ブロックを提供し、コンポーネントがコンポジションを退出する際やキーが変わった際に実行されることが保証されます。これにより、ネイティブサブスクリプション、センサーリスナー、ハードウェアリソースの作業に不可欠です。Android Developers Documentation (2025) によると、DisposableEffect は Activity ライフサイクルの onStart/onStop に似た setup/teardown ペアが必要なあらゆるシナリオで使用することが推奨されています。

まとめ

  • DisposableEffect — 設定と保証されたリソースクリーンアップのための side-effect API。
  • onDispose — コンポジション退出時やキー変更時に実行される必須ブロック。
  • 同期 — LaunchedEffect と異なり、DisposableEffect はコルーチンなしで同期的に動作する。
  • クリーンアップ — 一般的なシナリオ: LiveData のサブスクリプション解除、ソケットのクローズ、BroadcastReceiver の登録解除。
  • キー — キー変更時、旧値に対して onDispose が実行され、新値で再初期化が行われる。

Jetpack Compose での DisposableEffect とは

DisposableEffect は Jetpack Compose におけるリソース管理のための重要なツールです。その主な特徴は、composable コンポーネントのライフサイクルが終了したときに onDispose ブロックが保証されて呼び出されることです。この行動は Android 開発において重要で、システムサービスへの閉じられないサブスクリプションはメモリーリークやアプリケーションのクラッシュの原因となりうるためです。

非同期コルーチンコンテキストで動作する LaunchedEffect とは異なり、DisposableEffect は同期的に実行されます。これは、その中で suspend 関数を呼び出せないことを意味します。同期的な実行は予測可能性を保証します: 初期化コードが最初のレンダリングの前に実行され、クリーンアップコードがコンポーネントがメモリから削除される前に実行されることを確信できます。

Jetpack Compose ドキュメンテーション (2025) によると、DisposableEffect は 4 つの主なシナリオで使用すべきです: (1) システムサービスへのサブスクライブ (センサー、LocationManager)、(2) BroadcastReceiver の登録、(3) コルーチンをサポートしない callback ベースのライブラリへの対応、(4) AndroidView を介して Compose コンポーネントをレガシー View システムに結合すること。

kotlin
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 と onDispose の仕組み

DisposableEffect の内部メカニズムは、コンポジションライフサイクルのフェーズに基づいています。composable コンポーネントがコンポジションに入ると、DisposableEffect は渡されたコードブロックを実行します。このブロックは onDispose ラムダを含む DisposableEffectResult オブジェクトを返します。コンポジションはこの結果を保存し、コンポーネントがコンポジションを退出する際に onDispose を呼び出します — 理由に関わらず (ナビゲーション、親状態の変更、LazyColumn からの削除)。

DisposableEffect における キーの仕組みは LaunchedEffect と同様に動作します: いずれかのキーが変わると、まず onDispose が旧状態に対して実行され、その後に初期化ブロックが新しいキーで再実行されます。これにより、パラメータが変わった際にリソースを再構成できます。例えば、キーがソケット URL の場合、それが変わると旧ソケットが閉じられ、新しいソケットが開かれます。

重要: onDispose ブロックは DisposableEffect の必須要素です。ブロック内で onDispose を呼び出さないと、コードはコンパイルされません。このコンパイラの要件により、開発者がリソースのクリーンアップを忘れることがなくなり、マニュアルなサブスクリプション管理におけるミスの共通原因を防ぐことができます。

kotlin
// キーを使った正しい使い方
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)
    }
}

DisposableEffect とメモリーリークの比較

Android アプリケーションでのメモリーリークは、画面が閉じられた後も Activity または Context への参照を保持し続ける、登録されていないリスナーやサブスクリプションが原因となりがちです。DisposableEffect はこの問題をフレームワークレベルで解決します: 開発者がリスナーを登録するために DisposableEffect を使用すると、onDispose はいかなるコンポーネント終了シナリオでもサブスクリプションをキャンセルすることを保証します。

これは特に LazyColumnLazyGrid において重要で、ユーザーがスクロールすると項目が継続的に作成されては破壊されます。DisposableEffect がない場合、可視領域から消えた各項目はアクティブなサブスクリプションを残します。DisposableEffect では、アンロードされた各項目に対して onDispose が呼び出され、項目が画面から消えた直後にリソースが解放されることが保証されます。

Android Performance Patterns (Google, 2025) によると、すべてのネイティブサブスクリプションに DisposableEffect を使用すると、ライフサイクル callback によるマニュアル管理と比較して、Compose アプリケーションでのメモリーリークの数が 60–70% 減少します。システム自体がコンポーネントがコンポジションを退出する瞬間を追跡し、緊急画面閉鎖の際にも onDispose の実行を保証します。

リソースDisposableEffect の動作DisposableEffect なし
BroadcastReceiverregister + onDispose → unregisterレシーバーがアクティブのまま
SensorManagerregisterListener + onDispose → unregisterListenerセンサーがデータを送り続ける
Observable (Flow 以外)subscribe + onDispose → unsubscribeCallback が参照を保持する
TextureView / SurfaceViewsetCallback + onDispose → removeCallbackCallback リーク
Socket / Channelopen + onDispose → close接続が開いたまま

DisposableEffect によるセンサーへのサブスクライブ

DisposableEffect の使用例のうち最も具体的な例のひとつは、デバイスセンサー (加速度センサー、ジャイロスコープ、磁気センサー) を扱うことです。センサーは作業終了時に必ず登録解除が必要で、そうしないと画面を閉じた後もバッテリーを消費し続け、データを送り続けます。

実用例: 傾き角度測定アプリケーション。DisposableEffect(Unit) はコンポーネントが現れたときに加速度センサーリスナーを登録し、onDispose で登録を解除します。センサーデータは mutableStateOf を介して状態に渡され、UI を自動的に更新します。画面が LazyColumn でスクロールし、項目が消えると、onDispose が即刻に発火します — センサーはその項目に対してデータを送るのを停止します。

センサーの種類を切り替える際 (例: 加速度センサーからジャイロスコープへ)、sensorType キーが変わり、onDispose が旧サブスクリプションをキャンセルし、新しい DisposableEffect ブロックが新しいセンサーを登録します。キーがない場合、以前に登録されたセンサーを手動で確認し、正しいリスナーで unregisterListener を呼び出す必要があり、エラーを産みやすくなります。

kotlin
@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")
}

DisposableEffect による BroadcastReceiver の登録

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 バージョンのセキュリティ要件と合一します。

kotlin
@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) { ... }
}

DisposableEffect の一般的な趣い込み試鑑

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 と LaunchedEffect の違いは何ですか?

DisposableEffect は同期的に動作し、リソースの明示的なクリーンアップのために onDispose を提供します。LaunchedEffect はコルーチン内で非同期的に動作し、キーが変わったときやコンポジションを退出するときに自動的にキャンセルされます。リソースがクリーンアップメソッド (close, unregister, dispose) の呼び出しを必要とする場合は DisposableEffect を使用します。操作が suspend 関数の場合は LaunchedEffect を使用します。

DisposableEffect で onDispose ブロックは必須ですか?

はい、onDispose は必須です — Kotlin コンパイラは DisposableEffect ブロック内でそれを呼び出すよう必須です。onDispose を呼び出さない場合、コードはコンパイルされません。これは、開発者が忘れるのを防ぎ、コンポジションを退出するときに開かれたすべてのリソースがクローズされることを確保するために意図的に設計されています。

DisposableEffect 内のエラーをどう扱えばよいですか?

DisposableEffect ブロック内で try-catch を使用します。リソースの登録が例外をスローする可能性がある場合 (例: センサーが見つからない)、それを try で囲み、別の状態を介して UI でエラーを処理します。onDispose は初期化の成功に関わらず呼び出すべきです — finally ブロックまたは try セクションの最尾に置いてください。

Flow へのサブスクライブに DisposableEffect を使用できますか?

推奨されません。Flow には、collectLatest を使用した LaunchedEffect または Lifecycle.repeatOnLifecycle との .collectAsState() メソッドを使用することを推奨します。DisposableEffect は suspend 関数をサポートしていないため、その内部で Flow にサブスクライブするには CoroutineScope を介して別のコルーチンを起動する必要があり、コードが複雑になり、リークのリスクが高まります。

1 つの composable に DisposableEffect はいくつまでコードできますか?

制限はありませんが、関連するリソースは 1 つの DisposableEffect に複数の操作と 1 つの onDispose でまとめることが推奨されます。リソースが独立している場合 (例: センサーと BroadcastReceiver)、異なるキーを持つ別々の DisposableEffects に分けることが望ましいです — これによりデバッグが簡素化され、あるキーが変わったときにすべてのリソースが再作成されるのを防げます。

統合

  • DisposableEffect — onDispose による保証されたクリーンアップ付き同期初期化のための Jetpack Compose side-effect API。
  • onDispose — コンポジションからの退出時やキー変更時に実行される必須ブロック、メモリーリークを防ぐ。
  • キー — キー変更時、まず onDispose が旧値に対して実行され、その後に新値で再初期化が行われる。
  • 同期的 — DisposableEffect は同期的に実行される; 内部で suspend 関数は使用不可。
  • 一般的なシナリオ — BroadcastReceiver、センサー、ネイティブリスナー、callback ベースのライブラリ、AndroidView 統合。
  • リーク — DisposableEffect はマニュアルなライフサイクル callback 管理と比較してリーク数を 60–70% 減少させる。
  • 趣い込み試鑑 — 主なリスク: 条件付き onDispose 呼び出し、非同期操作への使用、エフェクト内での remember なしのオブジェクト作成。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください