Deep-dive into DisposableEffect side-effect API and its similarities/differences with LaunchedEffect and remember(key){} APIs.

The story will explore in detail the side-effect API DisposableEffect and will take a use-case to show application of DisposableEffect, where we will see how to log analytics events while observing lifecycle events.
What is side-effect?
Side-effect is anything happening out of the scope of a composable which is eventually effecting composable. Jetpack Compose provides different side-effect APIs to deal with such side-effects in a controlled and predictable manner.
I wrote a detailed story about LaunchedEffect and rememberCoroutineScope side-effect APIs, if interested you can read from the link below.
LaunchedEffect vs rememberCoroutineScope in Jetpack Compose
Let’s see the overview of the page content.
Page Content
DisposableEffectAPIDisposableEffectAPI UnderTheHoodDisposableEffectcode exampleDisposableEffectvsremember(key)DisposableEffectvsLaunchedEffectAPIDisposableEffectApplications (usecase: Logging analytics when screen becomes visible to the user)- Github Project
DisposableEffect API
DisposableEffect is a composable function that means it can only be used inside another composable function. DisposableEffect takes a key and block of code to execute when that key changes and when it enters the Composition phase. DisposableEffect provides onDispose block of code which is used to remove any observer if we were listening to those callbacks in the effect block of DisposableEffect . It ensures that onDispose block is provided otherwise it gives an error.
DisposableEffect API UnderTheHood
Let’s see one of the function declarations of DisposableEffect .
| @Composable | |
| @NonRestartableComposable | |
| fun DisposableEffect( | |
| key1: Any?, | |
| effect: DisposableEffectScope.() -> DisposableEffectResult | |
| ) { | |
| remember(key1) { DisposableEffectImpl(effect) } | |
| } |
| class DisposableEffectScope { | |
| /** | |
| * Provide [onDisposeEffect] to the [DisposableEffect] to run when it leaves the composition | |
| * or its key changes. | |
| */ | |
| inline fun onDispose( | |
| crossinline onDisposeEffect: () -> Unit | |
| ): DisposableEffectResult = object : DisposableEffectResult { | |
| override fun dispose() { | |
| onDisposeEffect() | |
| } | |
| } | |
| } |
| private class DisposableEffectImpl( | |
| private val effect: DisposableEffectScope.() -> DisposableEffectResult | |
| ) : RememberObserver { | |
| private var onDispose: DisposableEffectResult? = null | |
| override fun onRemembered() { | |
| onDispose = InternalDisposableEffectScope.effect() | |
| } | |
| override fun onForgotten() { | |
| onDispose?.dispose() | |
| onDispose = null | |
| } | |
| override fun onAbandoned() { | |
| // Nothing to do as [onRemembered] was not called. | |
| } | |
| } |
Looking at the code above confirms the following points.
DisposableEffectis a composable function.effectblock is not asuspendfunction so if we have to execute asuspendfunction thenDisposableEffectis not the choice.DisposableEffectis expecting at least one key, If we don’t want to pass any key we can passnullorUnit, I would recommend passingUnitfor better code readability or you can useDisposableEffect(true).DisposableEffectis usingrememberKey(key)API internally to execute a block of code when key changes. We will see differences btw both below.DisposableEffectis usingDisposableEffectScopefor theeffectblock.DisposableEffectScopemakes sure thatonDisposeblock is provided and it ensures the block provided inonDisposeis executed when key changes or whenDisposableEffectleaves the composition, in order to cleanup resources.
DisposableEffect API Code Example
Let’s see a code example to verify characteristics of DisposableEffect .
| @Composable | |
| fun DisposableEffectTestScreen( | |
| viewModel: DisposableEffectTestViewModel | |
| ) { | |
| val count = viewModel.count.collectAsState() | |
| DisposableEffect(count.value) { | |
| println("DisposableEffect block called for count = ${count.value}") | |
| onDispose { | |
| println("onDispose Called") | |
| } | |
| } | |
| // compose content | |
| } |
In the code about DisposableEffect is used with the key count.value . We print logs when the effect block executes and when onDispose block executes. In ViewModel its starting with count 1 and incrementing the count value every second till max 3.
After running the code the logs look like this.
| I/System.out: DisposableEffect block called for count = 1 | |
| I/System.out: onDispose Called | |
| I/System.out: DisposableEffect block called for count = 2 | |
| I/System.out: onDispose Called | |
| I/System.out: DisposableEffect block called for count = 3 |
Job Offers
Logs show that the effect block executes on every key change and onDispose block executes before executing effect block with the new key change. onDispose for count 3 has not yet called because the key is not changed further.
At this point if we rotate the device then onDispose block for count 3 will be called first before executing the effect block execution from count 1 onward.
Configuration changes starts Composition phase again
So the logs after rotation of the device will look further like below.
| I/System.out: onDispose Called | |
| I/System.out: DisposableEffect block called for count = 1 | |
| I/System.out: onDispose Called | |
| I/System.out: DisposableEffect block called for count = 2 | |
| I/System.out: onDispose Called | |
| I/System.out: DisposableEffect block called for count = 3 |
DisposableEffect vs remember(key)
remember has its own applications of calculating and holding State information, but if we use remember with key and block of code {} like remember(key) {} ,then it works same as DisposableEffect except that DisposableEffect provides onDispose block for cleanup. So if we don’t need to clean-up resources or we do not observe any changes that need to be cleaned-up then remember(key) {} can be used for simple cases.
To summarise the points.
remember(key){}executes the block of code first time during the Composition phase and also when the passed key torememberchanges.remember(key) {}works similar toDisposableEffect(key) {}except thatDisposableEffectprovidesonDisposeto cleanup.remember(key) {}can be used interchangeably withDisposableEffect(key) {}if we don’t have to cleanup anything on key change or when composable leaves composition.DisposableEffectusesremember(key) {}under the hood. ( you can see in the UnderTheHood section above)
DisposableEffect vs LaunchedEffect
Sometimes it’s good to see an API in comparison with others and it helps to understand and decide about when/where we should use which API. So let’s see similarities and differences between DisposableEffect and LaunchedEffect.
Similarities
LaunchedEffectandDisposableEffectare composable functions and can only be used inside another composable function.LaunchedEffectandDisposableEffectboth takekey/keysto execute blocks of code when passedkey/keyschanges and during composition phase.LaunchedEffectcanceled with previous running coroutine before starting a new one whereasDisposableEffectcallsonDisposefor previous running block of code to cleanup resources before executing a new block of code.
Differences
LaunchedEffectexecutessuspendfunctions whereasDisposableEffectis used for non suspending functions.DisposableEffectprovidesonDisposeblock where we can cleanup resources whereasLaunchedEffectdoes not provide any such thing but it runs coroutine within the scope of the composable so its lifecycle is managed automatically.LaunchedEffectshould only be used to perform UI related tasks whereasDisposableEffectis mostly used to perform other tasks which are not UI specific e.g logging analytics events
DisposableEffect Application ( use-case: logging analytics events when screen become visible)
Let’s look at the real use-case where DisposableEffect is an ideal choice.
Usecase
We want to log analytics events as soon as the user views a screen.
To achieve that we want to observe lifecycle events, using that we will detect when the screen was open checking life-cycle state and log analytics events where suitable.
We will observe life-cycle changes via adding an observer to those changes as it enters the Composition phase so we then also remove that observer as soon as composable leaves Composition.
DisposableEffect will help to achieve this as it provides an effect block of code to execute on Composition and onDispose which is called when composable leaves Composition.
Let’s see the overall code below
| @Composable | |
| fun DisposableEffectScreen( | |
| lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, | |
| viewModel: DisposableEffectViewModel | |
| ) { | |
| val logOnResume by rememberUpdatedState(newValue = { viewModel.logViewEvent() }) | |
| // If `lifecycleOwner` changes, dispose and reset the effect | |
| DisposableEffect(lifecycleOwner) { | |
| // Create an observer that triggers our remembered callbacks | |
| // for sending analytics events | |
| val observer = LifecycleEventObserver { _, event -> | |
| if (event == Lifecycle.Event.ON_RESUME) { | |
| logOnResume() | |
| } | |
| } | |
| // Add the observer to the lifecycle | |
| lifecycleOwner.lifecycle.addObserver(observer) | |
| // When the effect leaves the Composition, remove the observer | |
| onDispose { | |
| lifecycleOwner.lifecycle.removeObserver(observer) | |
| } | |
| } | |
| // Content of page | |
| } |
In DisposableEffect we are observing to life-cycle events changes during Composition. When it’s in Resume state it calls logOnResume method of viewModel which eventually will log event. (we will not go into viewModel function details as its not required for this story )
There are many other applications of DisposableEffect where it can be useful, below are a few lists.
- Observing keyboard opening and closing events
- Observing life-cycle events to perform reload/refresh/cleanup tasks
- Stop/Start observing data stream based on lifecycle-event
Sources
- DisposableEffect API Official Documentation
- Lifecycle Events
- Lifecycle Observer
- remember(key)
- LaunchedEffect
Github Project
That’s it for now! Hope it was helpful… Looking forward to any questions/suggestions in the comments.
Remember to follow and 👏 if you liked it 🙂
— — — — — — — — —
This article was previously published on proandroiddev.com



