DisposableEffect — resource cleanup in Jetpack Compose

Author: IT Sectr Published: 2026-06-30 Reading time: 9 min

DisposableEffect is a composable function in Jetpack Compose designed for operations that require explicit initialization and subsequent resource cleanup. Unlike other side-effect APIs, DisposableEffect provides an onDispose block that is guaranteed to execute when the component leaves composition or when the key changes. This makes it indispensable for working with native subscriptions, sensor listeners, and hardware resources. According to Android Developers Documentation (2025), DisposableEffect is recommended for all scenarios requiring a setup/teardown pair, analogous to onStart/onStop in Activity lifecycle.

Key Takeaways

  • DisposableEffect — side-effect API for setup and guaranteed resource cleanup.
  • onDispose — mandatory block that executes when leaving composition or changing the key.
  • Synchronous — unlike LaunchedEffect, DisposableEffect runs synchronously without coroutines.
  • Cleanup — typical scenarios: unsubscribing from LiveData, closing sockets, unregistering BroadcastReceiver.
  • Keys — when the key changes, onDispose executes for the old value and re-initialization occurs with the new one.

What is DisposableEffect in Jetpack Compose

DisposableEffect is a key tool for resource management in Jetpack Compose. Its main feature is the guaranteed invocation of the onDispose block when the composable component's lifecycle ends. This behavior is critical for Android development, where unclosed subscriptions to system services can lead to memory leaks and application crashes.

Unlike LaunchedEffect, which runs in an asynchronous coroutine context, DisposableEffect executes synchronously. This means you cannot call suspend functions inside it. Synchronous execution ensures predictability: you can be sure that initialization code runs before the first render, and cleanup code runs before the component is removed from memory.

According to Jetpack Compose Documentation (2025), DisposableEffect should be used in four main scenarios: (1) subscribing to system services (sensors, LocationManager), (2) registering BroadcastReceiver, (3) working with callback-based libraries that do not support coroutines, (4) binding Compose components to legacy View systems via AndroidView.

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("Sensor: $value")
}

How DisposableEffect works with onDispose

The internal mechanics of DisposableEffect are based on the phases of the composition lifecycle. When a composable component enters composition, DisposableEffect executes the passed code block. This block returns a DisposableEffectResult object containing the onDispose lambda. Composition saves this result and calls onDispose when the component leaves composition — regardless of the reason (navigation, parent state change, removal from LazyColumn).

The key mechanism in DisposableEffect works similarly to LaunchedEffect: when any key changes, onDispose is first executed for the old state, then the initialization block runs again with the new keys. This allows reconfiguring a resource when its parameters change. For example, if the key is a socket URL, when it changes the old socket is closed and a new one is opened.

Important: the onDispose block is a mandatory element of DisposableEffect. If you do not call onDispose inside the block, the code will not compile. This compiler requirement ensures that the developer does not forget to provide resource cleanup, which is a common cause of errors in manual subscription management.

kotlin
// Correct usage with a key
DisposableEffect(sensorType) {
    val sensor = sensorManager.getDefaultSensor(sensorType)
    sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
    
    onDispose {
        sensorManager.unregisterListener(listener)
    }
}

// Multiple resources in one DisposableEffect
DisposableEffect(Unit) {
    context.registerReceiver(receiver, intentFilter)
    lifecycle.addObserver(observer)
    
    onDispose {
        context.unregisterReceiver(receiver)
        lifecycle.removeObserver(observer)
    }
}

DisposableEffect vs memory leaks

Memory leaks in Android applications often occur due to unregistered listeners and subscriptions that continue to hold a reference to an Activity or Context after the screen has been closed. DisposableEffect solves this problem at the framework level: if a developer uses DisposableEffect to register a listener, onDispose will guarantee to cancel the subscription in any component termination scenario.

This is especially critical for LazyColumn and LazyGrid, where items are constantly created and destroyed as the user scrolls. Without DisposableEffect, each item that disappears from the visible area would leave an active subscription. With DisposableEffect, onDispose is called for each unloaded item, ensuring that resources are freed immediately after the item leaves the screen.

According to Android Performance Patterns (Google, 2025), using DisposableEffect for all native subscriptions reduces the number of memory leaks in Compose applications by 60–70% compared to manual management through lifecycle callbacks. The system itself tracks when a component leaves composition and guarantees the execution of onDispose even during emergency screen closure.

ResourceWhat DisposableEffect doesWithout DisposableEffect
BroadcastReceiverregister + onDispose → unregisterReceiver remains active
SensorManagerregisterListener + onDispose → unregisterListenerSensor continues sending data
Observable (not Flow)subscribe + onDispose → unsubscribeCallback holds a reference
TextureView / SurfaceViewsetCallback + onDispose → removeCallbackCallback leak
Socket / Channelopen + onDispose → closeConnection remains open

Subscribing to sensors via DisposableEffect

One of the most illustrative examples of using DisposableEffect is working with device sensors (accelerometer, gyroscope, magnetometer). Sensors require mandatory unregistration when work is complete, otherwise they continue consuming battery power and sending data even after the screen is closed.

Practical example: an angle measurement application. DisposableEffect(Unit) registers an accelerometer listener when the component appears and unregisters it in onDispose. Sensor data is passed to the state via mutableStateOf, which automatically updates the UI. If the screen scrolls in LazyColumn and the item disappears, onDispose fires immediately — the sensor stops sending data for this item.

When changing the sensor type (e.g., from accelerometer to gyroscope), the sensorType key changes, onDispose cancels the old subscription, and the new DisposableEffect block registers the new sensor. Without keys, you would have to manually check which sensor was previously registered and call unregisterListener with the correct listener — which is error-prone.

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("Value: $sensorValue")
}

Registering BroadcastReceiver via DisposableEffect

BroadcastReceiver is a classic example of an API requiring a mandatory register / unregister pair. In a Compose application, DisposableEffect is ideal for registering a receiver for the lifetime of a specific screen. When entering the screen, a BroadcastReceiver with the required IntentFilter is registered; when leaving, it is automatically canceled in onDispose.

A typical scenario is network state monitoring. DisposableEffect registers a receiver for ConnectivityManager that notifies about network connectivity changes. When the status changes (WiFi / mobile data / no network), the composable state updates and the UI displays the corresponding indicator. When the screen closes, onDispose guarantees to cancel the registration — even if the app goes to background.

For receivers using ContextCompat.registerReceiver with the RECEIVER_EXPORTED / RECEIVER_NOT_EXPORTED flag (Android 14+), using DisposableEffect becomes mandatory because the system requires explicit specification of the receiver's scope. DisposableEffect ensures that the scope is limited to the screen's lifetime, which aligns with the security requirements of newer Android versions.

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

Common mistakes with DisposableEffect

The first critical mistake is missing the onDispose call. The code inside the DisposableEffect block must call onDispose, otherwise a compilation error occurs. However, developers sometimes try to bypass this by placing onDispose in a condition: if (condition) { onDispose { ... } }. Such code will compile, but onDispose will not be registered if the condition is not met — the resource will never be released.

The second mistake is using DisposableEffect for asynchronous operations. Since DisposableEffect is synchronous, you cannot write delay() or await() calls inside it. If you need asynchronous initialization with subsequent cleanup, use a combination of LaunchedEffect (for data loading) and DisposableEffect (for setting up/cleaning up native resources), or use a separate mechanism with rememberCoroutineScope.

The third mistake is creating new objects inside DisposableEffect without remember. If objects (sensor, listener, receiver) are created inside the effect on every call and keys change frequently, this leads to excessive object creation and garbage collection. It is better to move object creation to remember or remember { ... } outside DisposableEffect, and only register and unregister them inside the effect.

Frequently Asked Questions

What is the difference between DisposableEffect and LaunchedEffect?

DisposableEffect runs synchronously and provides onDispose for explicit resource cleanup. LaunchedEffect runs asynchronously in a coroutine and automatically cancels it when the key changes or when leaving composition. If a resource requires calling a cleanup method (close, unregister, dispose) — use DisposableEffect. If the operation is a suspend function — use LaunchedEffect.

Is the onDispose block mandatory in DisposableEffect?

Yes, onDispose is mandatory — the Kotlin compiler requires it to be called inside the DisposableEffect block. If you do not call onDispose, the code will not compile. This is intentional to prevent developers from forgetting and to guarantee that every opened resource will be properly closed when leaving composition.

How to handle errors inside DisposableEffect?

Use try-catch inside the DisposableEffect block. If resource registration can throw an exception (e.g., sensor not found), wrap it in try and handle the error in the UI through a separate state. onDispose should be called regardless of initialization success — place it in a finally block or at the end of the try section.

Can I use DisposableEffect to subscribe to Flow?

Not recommended. For Flow, it is better to use LaunchedEffect with collectLatest or the .collectAsState() method with Lifecycle.repeatOnLifecycle. DisposableEffect does not support suspend functions, so subscribing to Flow inside it would require launching a separate coroutine via CoroutineScope, which complicates the code and increases the risk of leaks.

How many DisposableEffects can be in one composable?

There are no limits, but it is recommended to group related resources into one DisposableEffect with multiple operations inside and one onDispose. If resources are independent (e.g., sensor and BroadcastReceiver), it is better to split them into separate DisposableEffects with different keys — this simplifies debugging and prevents unwanted recreation of all resources when one key changes.

Summary

  • DisposableEffect — Jetpack Compose side-effect API for synchronous initialization with guaranteed cleanup via onDispose.
  • onDispose — mandatory block that executes when leaving composition or changing the key, preventing memory leaks.
  • Keys — when a key changes, onDispose is first executed for the old value, then re-initialization occurs with the new one.
  • Synchronous — DisposableEffect runs synchronously; suspend functions are not available inside it.
  • Typical scenarios — BroadcastReceiver, sensors, native listeners, callback-based libraries, AndroidView integration.
  • Leaks — DisposableEffect reduces the number of leaks by 60–70% compared to manual lifecycle callback management.
  • Mistakes — main risks: conditional onDispose call, using for async operations, creating objects without remember inside the effect.

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also