Blog Infos
Author
Published
Topics
Published

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.

Let’s see the overview of the page content.

Page Content
  • DisposableEffect API
  • DisposableEffect API UnderTheHood
  • DisposableEffect code example
  • DisposableEffect vs remember(key)
  • DisposableEffect vs LaunchedEffectAPI
  • 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 a suspend function so if we have to execute a suspend function then DisposableEffect is not the choice.
  • DisposableEffect is expecting at least one key, If we don’t want to pass any key we can pass null or Unit , I would recommend passingUnit for better code readability or you can use DisposableEffect(true).
  • DisposableEffect is using rememberKey(key) API internally to execute a block of code when key changes. We will see differences btw both below.
  • DisposableEffect is using DisposableEffectScope for the effect block. DisposableEffectScope makes sure that onDispose block is provided and it ensures the block provided in onDispose is executed when key changes or when DisposableEffect 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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Build adaptive apps with Jetpack Compose

In this workshop you will learn how to build adaptive apps for phones, tablets, and foldables, and how they enhance reachability with Jetpack Compose.
Watch Video

Build adaptive apps with Jetpack Compose

Miguel Montemayor
Developer Relations Engineer
Google

Build adaptive apps with Jetpack Compose

Miguel Montemayor
Developer Relations ...
Google

Build adaptive apps with Jetpack Compose

Miguel Montemayo ...
Developer Relations Engin ...
Google

Jobs

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 to remember changes.
  • remember(key) {} works similar to DisposableEffect(key) {} except that DisposableEffect provides onDispose to cleanup.
  • remember(key) {} can be used interchangeably with DisposableEffect(key) {} if we don’t have to cleanup anything on key change or when composable leaves composition.
  • DisposableEffect uses remember(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 and DisposableEffect are composable functions and can only be used inside another composable function.
  • LaunchedEffect and DisposableEffect both take key/keys to execute blocks of code when passed key/keys changes and during composition phase.
  • LaunchedEffect canceled with previous running coroutine before starting a new one whereas DisposableEffect calls onDispose for previous running block of code to cleanup resources before executing a new block of code.

Differences

  • LaunchedEffect executes suspend functions whereas DisposableEffect is used for non suspending functions.
  • DisposableEffect provides onDispose block where we can cleanup resources whereas LaunchedEffect 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 whereas DisposableEffect 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
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 🙂

— — — — — — — — —

GitHub | LinkedIn | Twitter

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu