Disposable Effect API in Jetpack Compose: LifecycleEventEffects

Oğuzhan Doğdu
6 min readFeb 9, 2024

Hello everyone 👋🏻, in this article we will focus on Disposable Effect, one of the side effects that comes with Jetpack Compose. This focus will actually shift to the newly released LifecycleEventEffects after making a basic approach to Disposable Effect. It will be a not too long and enjoyable article. Then let’s get started.🏃🏻

What is the Side-Effect? ☢️

The term “side effect” refers to situations where a function not only returns a result, but also changes the state of the application or interacts with the outside world. In simple terms, the “side effect” of a function is the unexpected or extra effects that occur as a result of the function’s execution. This can include operations such as writing a file, writing to a database, or changing the value of a global variable. Such effects occur outside the main function of the function and usually affect the behaviour and results of the function. Therefore, side effects are often a situation that needs to be managed carefully.🚧

How can we handle side effects? 👨🏻‍🔧

Basically, we can handle Side Effects with 4 different methods. Two of them can be suspended and the other two cannot be suspended.

a-) Suspended Effect Handlers

  • LaunchedEffect
  • rememberCoroutineScope

b-) Non-suspended Effect Handlers

  • DisposableEffect
  • SideEffect

As I mentioned in the introduction, in this article we will discuss the Disposable Effect.

What is the DisposableEffect?

DisposableEffect is basically a composable function. It must be used for any operation that depends on the Lifecycle on the screen where it is used. The most important feature is that when the Lifecycle is Destroy after the operation, we can perform the process of not leaving the operation behind, that is, deleting it from the memory.

How Does DisposableEffect Work? ⚙️

Let’s examine step by step how Disposable Effect is implemented in Android source code.

@Composable
@NonRestartableComposable
fun DisposableEffect(
key1: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1) { DisposableEffectImpl(effect) }
}

We have 2 parameters: key and effect lambda. If we examine key, it is a parameter of type Any and the status of this key is saved with the remember API. This means that during composition, the remember block will run once and save this key and then the block will not run again until the value of the key changes.

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()
}
}
}

interface DisposableEffectResult {
fun dispose()
}

There is a basic onDispose function in the DisposableEffectScope Class created in the effect parameter of DisposableEffect. onDispose function works when the value given to the key parameter changes or when the DisposableEffect leaves the composable function in which it is used. What it does is to clean the resources used for the related jobs. So why is onDispose an inline function? The reason for this is that we can use more than one DisposableEffect and we can use more than one onDispose block within these blocks. In this scenario, if we constantly create a new object, we may encounter performance problems by increasing the workload on the garbage collector due to high memory usage.

private val InternalDisposableEffectScope = DisposableEffectScope()

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

Finally, if we examine the DisposableEffectImpl class; RememberObserver interfaces have been implemented and 3 functions have been processed accordingly.

onRemembered() describes a method that is invoked when an object is successfully remembered by a composition. The phrase “apply thread” indicates that this method is called on a thread where composition apply operations are performed. In other words, this method operates in a context where the composition updates its state, ensuring that the object is successfully remembered.

onForgotten() specifies a method that is triggered when an object is no longer held by a composition, implying that it is forgotten. The term “apply thread” indicates that this method is executed in the thread responsible for applying composition operations. In other words, this method is called in the context of composition updates when the object is removed from the composition or forgotten.

onAbondened() refers to a method that is called when an object is returned by a callback to be remembered but cannot be successfully retained by a composition. In other words, this method is called when the object is attempted to be remembered but cannot be successfully integrated into the composition.

Now that we have learnt the working structure of DisposableEffect, we can take a look at the use case with a few scenarios.

Let’s say you need to handle the status of the video by Lifecycle using ExoPlayer for a screen.

val exoPlayer = remember(context) { ExoPlayer.Builder(context).build() }

DisposableEffect(exoPlayer) {
val lifecycleObserver = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> exoPlayer.play()
Lifecycle.Event.ON_STOP -> exoPlayer.pause()
}
}

lifecycleOwner.lifecycle.addObserver(lifecycleObserver)

onDispose {
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
exoPlayer.release()
}
}

If you want to handle ExoPlayer states with LazyColumn, Denis Rudenko has a great article: https://medium.com/proandroiddev/video-playback-in-lazycolumn-in-jetpack-compose-df355097f26e

As you can see how much our code has grown just to handle ExoPlayer. If we need to check a few more cases like this, we will end up with a longer code block. For example, when using Broadcast Receiver, registering when the Lifecycle starts and unregistering when the Lifecycle ends, or caching an image list and clearing this cache when the Lifecycle ends.

At this point, with the 2.7.0 version of the Android Lifecycle API, LifecycleEventEffects entered our lives.

What is the LifecycleEventEffects?

In short, LifecycleEventEffects is an API created for a specific Lifecycle.Event.

a-) Dependency

val lifecycle_version = "2.7.0"

// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
// ViewModel utilities for Compose
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version")
// Lifecycles only (without ViewModel or LiveData)
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version")
// Lifecycle utilities for Compose
implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version")
}

b-) Event Effect Types

  • LifecycleEventEffect
  • LifecycleStartEffect
  • LifecycleResumeEffect

LifecycleEventEffects Usage

Let’s reconsider the ExoPlayer case I gave as an example above here.

   val lifecycleOwner = LocalLifecycleOwner.current
LifecycleStartEffect(key1 = exoPlayer,lifecycleOwner = lifecycleOwner) {
exoPlayer.play()
onStopOrDispose {
exoPlayer.pause()
}
}

The code we write is now shorter and more readable. When a Lifecycle.Event.ON_STOP event occurs or the effect exits the composition, it triggers an onStopOrDispose block. This provides an opportunity to perform cleanup for any tasks initiated in the starting block. But if you need to use a library that needs to be handled in more detail, using this LifecycleEventEffect API’s may not solve your needs.

We can show a few more simple examples with LifecycleResumeEffect and LifecycleEventEffect and now we can come to the end of our topic.

val lifecycleOwner = LocalLifecycleOwner.current    
LifecycleResumeEffect(lifecycleOwner = lifecycleOwner) {
viewModel.fetchAllData()
onPauseOrDispose {
viewModel.emptyAllData()
}
}

A similar situation applies here, while the Lifecycle is in the Resume state, data will start coming from the ViewModel. In Pause state, the data will become empty.

LifecycleEventEffect(event = Lifecycle.Event.ON_CREATE) {
viewModel.fetchAllData()
}
LifecycleEventEffect(event = Lifecycle.Event.ON_STOP) {
viewModel.emptyAllData()
}

That’s it! 🥳 In this topic, we tried to understand the working structure of Disposable Effect and then we examined the LifecycleEventEffects that work using Disposable Effect. As Compose continues to evolve, it’s great that different solutions are brought to developers problems. As I mentioned in the topic, it is necessary to use this API carefully as it may cause any crash or stability problems.🚧

Thank you for reading this far.🙏🏻 If you have any questions or suggestions, you can contact me from my github account below. You can also access my repo where I started using EventEffects from the link below.

Sources 📚

1-)https://developer.android.com/topic/libraries/architecture/compose#collect-lifecycle

2-)https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LifecycleEffect.kt;l=55?q=lifecycleeventeffe&sq=

3-)https://proandroiddev.com/disposableeffect-side-effect-api-in-jetpack-compose-56cb2d3f7888

4-)https://developermemos.com/posts/disposableeffect-jetpack-compose

--

--