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
DisposableEffect
APIDisposableEffect
API UnderTheHoodDisposableEffect
code exampleDisposableEffect
vsremember(key)
DisposableEffect
vsLaunchedEffectAPI
DisposableEffect
Applications (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.
DisposableEffect
is a composable function.effect
block is not asuspend
function so if we have to execute asuspend
function thenDisposableEffect
is not the choice.DisposableEffect
is expecting at least one key, If we don’t want to pass any key we can passnull
orUnit
, I would recommend passingUnit
for better code readability or you can useDisposableEffect(true).
DisposableEffect
is usingrememberKey(key)
API internally to execute a block of code when key changes. We will see differences btw both below.DisposableEffect
is usingDisposableEffectScope
for theeffect
block.DisposableEffectScope
makes sure thatonDispose
block is provided and it ensures the block provided inonDispose
is executed when key changes or whenDisposableEffect
leaves 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 toremember
changes.remember(key) {}
works similar toDisposableEffect(key) {}
except thatDisposableEffect
providesonDispose
to 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.DisposableEffect
usesremember(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
LaunchedEffect
andDisposableEffect
are composable functions and can only be used inside another composable function.LaunchedEffect
andDisposableEffect
both takekey/keys
to execute blocks of code when passedkey/keys
changes and during composition phase.LaunchedEffect
canceled with previous running coroutine before starting a new one whereasDisposableEffect
callsonDispose
for previous running block of code to cleanup resources before executing a new block of code.
Differences
LaunchedEffect
executessuspend
functions whereasDisposableEffect
is used for non suspending functions.DisposableEffect
providesonDispose
block where we can cleanup resources whereasLaunchedEffect
does not provide any such thing but it runs coroutine within the scope of the composable so its lifecycle is managed automatically.LaunchedEffect
should only be used to perform UI related tasks whereasDisposableEffect
is 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