Blog Infos
Author
Published
Topics
, , , ,
Published

Unsplash@sam

he Jetpack Compose ecosystem has grown exponentially in recent years, and it is now widely adopted for building production-level UIs in Android applications. We can now say that Jetpack Compose is the future of Android UI development.

One of the biggest advantages of Compose is its declarative approach. It allows developers to describe what the UI should display, while the framework handles how the UI should update when the underlying state changes. This model shifts the focus from imperative UI logic to a more intuitive and reactive way of thinking.

However, while declarative UIs offer many benefits, properly managing side effects becomes essential. Composable functions can be recomposed for various reasons, and if side effects are not handled carefully, apps can behave unpredictably. To address this, Compose provides useful APIs like LaunchedEffect and DisposableEffect to manage effects tied to the composition lifecycle.

But what happens when we need an effect to survive beyond the composition, to live through events like configuration changes or back-stack navigation? It looks like development is still ongoing in AOSPRetainedEffect, has emerged to solve this exact problem. It mirrors the familiar structure of DisposableEffect but ties its lifecycle not to the composition, but to the more durable retention lifecycle.

Following up on our last article, “Previewing retain{} API: A New Way to Persist State in Jetpack Compose”, this piece will explore the RetainedEffect API. We will examine its internal workflow to better understand how Compose manages these long-lived operations and what differentiates this new API from its predecessors.

The Need for a New Kind of Effect

In Jetpack Compose, side effects are changes that occur outside the scope of a composable function. DisposableEffect is excellent for managing resources that need setup and teardown as a composable enters and leaves the composition. A common example is registering and unregistering a listener.

The problem, however, is that “leaving the composition” can be a temporary state. When a user navigates to a new screen, the previous screen’s composables are removed from the tree. If they were managing a resource with DisposableEffect, that resource would be torn down, only to be set up all over again when the user navigates back. This is inefficient.

RetainedEffect is designed for these scenarios. It allows you to execute an effect when an object is first created and retained, and a corresponding cleanup effect only when that object is permanently retired. This gives it a lifecycle similar to a ViewModel, making it perfect for managing resources that should persist across transient UI destructions.

How RetainedEffect Works Internally

To understand its mechanics, we can look directly at its implementation. The public-facing API is simple and familiar, accepting keys just like DisposableEffect.

@Composable
@NonRestartableComposable
public fun RetainedEffect(key1: Any?, effect: RetainedEffectScope.() -> RetainedEffectResult) {
retain(key1) { RetainedEffectImpl(effect) }
}

The core of the implementation is found in this single line: retain(key1) { RetainedEffectImpl(effect) }. This reveals two key components. First, it uses the new retain API to persist an object across UI destructions (surviving across composition destructions). Second, the object it retains is an instance of RetainedEffectImpl. This class is the engine that drives the effect’s lifecycle.

Let’s examine the RetainedEffectImpl class to see how it operates.

private class RetainedEffectImpl(
private val effect: RetainedEffectScope.() -> RetainedEffectResult
) : RetainObserver {
private var onRetire: RetainedEffectResult? = null
override fun onRetained() {
onRetire = InternalRetainedEffectScope.effect()
}
override fun onRetired() {
onRetire?.retire()
onRetire = null
}
override fun onEnteredComposition() {
// Do nothing.
}
override fun onExitedComposition() {
// Do nothing.
}
}

The RetainedEffectImpl class implements RetainObserver, which provides it with callbacks tied to the retention lifecycle. This is the primary difference from DisposableEffect, which uses RememberObserver for the composition lifecycle.

  • onRetained(): This function is called only once, when the RetainedEffectImpl object is first created and “retained” by the system. Inside this method, it executes the effect lambda that you provide. The result of this lambda, which is a RetainedEffectResult containing the cleanup logic, is stored in the onRetire property.
  • onRetired(): This function is called only when the object is permanently discarded by the RetainScope. For example, when a configuration change is complete and the old instance is no longer needed, or when a screen is popped from the back stack for good. It safely calls the retire() function on the stored onRetire object, executing your cleanup logic.
  • onEnteredComposition() and onExitedComposition(): Notice that these callbacks, which are part of the RetainObserver interface, are intentionally left blank. This is by design. RetainedEffect does not care when the composable enters or leaves the screen during its lifetime; its logic is tied only to the beginning and end of the retention period.

The RetainedEffectScope provides the onRetired clause, which mirrors the onDispose from DisposableEffect.

public class RetainedEffectScope {
public inline fun onRetired(crossinline onRetiredEffect: () -> Unit): RetainedEffectResult =
object : RetainedEffectResult {
override fun retire() {
onRetiredEffect()
}
}
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

This pattern creates a clear separation. The setup logic runs once at the start of retention, and the cleanup logic runs once at the very end, ignoring all the recompositions and transient UI changes that happen in between.

The Key Differences from LaunchedEffect and DisposableEffect

Understanding RetainedEffect becomes clearer when we compare it to its predecessors.

LaunchedEffect is designed for launching coroutines that are tied to the composition lifecycle. When its composable enters the composition, it launches a coroutine. When it leaves the composition, that coroutine is immediately canceled. It is ideal for one-off, suspendable actions like making a network request or showing a Snackbar when a certain state appears. It has no concept of retention.

DisposableEffect is for managing resources tied to the composition lifecycle. Its setup block runs when it enters the composition, and its onDispose block runs as soon as it leaves. It is perfect for attaching listeners that should only be active while a composable is on screen. Like LaunchedEffect, it does not survive transient UI destruction.

RetainedEffect is fundamentally different. It is designed for managing resources tied to the retention lifecycle. Its setup block (onRetained) runs only when the associated object is first created. Its cleanup block (onRetired) runs only when that object is permanently discarded. It is built to ignore the comings and goings of the composition, making it the ideal tool for effects that need to persist across configuration changes or navigation events, without the overhead or architectural separation of a ViewModel.

Conclusion

In this article, you explored the new RetainedEffect API, a useful addition to the Jetpack Compose side-effect handling toolkit. Due to the nature of modern Android development, where UI can be frequently created and destroyed, managing effects that need to survive these changes has been a persistent challenge.

By diving into its internal implementation, we see that RetainedEffect provides another approach as a Compose-native solution. It leverages the new retain API and RetainObserver infrastructure to tie an effect’s lifecycle directly to the concept of retention, not just composition. This allows developers to manage long-lived resources and effects with a simple, declarative API that feels right at home in the world of Compose, bridging a gap that previously only a ViewModel could fill.

If you’re looking to stay sharp with the latest skills, news, technical articles, interview questions, and practical code tips, check out Dove Letter. And for a deeper dive into interview prep, don’t miss the ultimate Android interview guide: Manifest Android Interview.

This article was previously published on proandroiddev.com

Menu