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 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.
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")
}
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.
// 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)
}
}
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.
| Resource | What DisposableEffect does | Without DisposableEffect |
|---|---|---|
| BroadcastReceiver | register + onDispose → unregister | Receiver remains active |
| SensorManager | registerListener + onDispose → unregisterListener | Sensor continues sending data |
| Observable (not Flow) | subscribe + onDispose → unsubscribe | Callback holds a reference |
| TextureView / SurfaceView | setCallback + onDispose → removeCallback | Callback leak |
| Socket / Channel | open + onDispose → close | Connection remains open |
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.
@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")
}
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.
@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) { ... }
}
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
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.
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.
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.
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.
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
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.
Read also