ViewCompositionStrategy Demystified

Chris Arriola
Android Developers
Published in
6 min readMay 16, 2023

--

In Jetpack Compose, a Composition is a tree-like structure describing the UI of your app and is produced by running composables. When the Composition is no longer needed, state will no longer be tracked by Jetpack Compose, and the Composition gets disposed so that resources can be released.

ViewCompositionStrategy defines when the Composition should be disposed. The default, ViewCompositionStrategy.Default, disposes the Composition when the underlying ComposeView detaches from the window, unless it is part of a pooling container such as a RecyclerView. However, if you are incrementally adding Compose in your codebase, this behavior may cause state loss in some scenarios. For example, if you are seeing weird glitches like scroll positions getting reset in your Fragment-based Compose app, perhaps you are using the wrong ViewCompositionStrategy (you should be using one of the Lifecycle-based strategies instead).

In this blog post, I’ll cover what ViewCompositionStrategy is, why it’s needed, and how you can pick the right strategy for your use case to avoid state loss.

TL;DR:

* Note: ComposeView is mentioned for simplicity’s sake, though the same behaviors apply for different forms of AbstractComposeView.

For a more in-depth understanding, keep reading!

Disposing the Composition with ViewCompositionStrategy

ViewCompositionStrategy affects the disposal phase of the Composition by automatically disposing the Composition when certain conditions are met. Once the Composition is disposed, resources are cleaned up and state will no longer be tracked by Compose.

The specific strategy applied will determine when the Composition should be disposed automatically. Without a strategy, you would have to explicitly call disposeComposition on the ComposeView to dispose the underlying Composition.

Thankfully, a default strategy as defined by ViewCompositionStrategy.Default, which is currently set to DisposeOnDetachedFromWindowOrReleasedFromPool, is already applied when you create a ComposeView (or call setContent from a ComponentActivity) so in a vast majority of cases, you won’t have to set it explicitly. However, you can change the default to a different strategy by providing it via setViewCompositionStrategy.

Compose-only vs mixed View/Compose apps

In a single-Activity Compose-only app, only one Composition is typically active. I mention typically because there are some exceptions to this — like subcomposition — but that’s out of scope for this blog post. Initial Composition occurs when the Activity is created. It runs the composables provided within setContent, and the Composition remains active until the Compose content is detached from the window — this detachment happens when the Activity is being destroyed. This is the default ViewCompositionStrategy of a ComposeView (more on this below), and in a Compose-only app, this behavior is what you want.

Compose-only app with only 1 Composition

Each instance of a ComposeView maintains its own separate Composition. So, if you are incrementally migrating your View-based app to Compose, you may have multiple Compositions. For example, if you have a ViewPager2 paging through Fragments and each Fragment’s content is in Compose, each ComposeView would be a separate Composition.

Mixed View/Compose app wherein each ComposeView in the ViewPager2 maintains a separate Composition

The interaction between each Composition, and components with a Lifecycle such as an Activity or Fragment, is the reason why you may have to change the default ViewCompositionStrategy so that you are disposing at the right time.

Different ViewCompositionStrategy types

DisposeOnDetachedFromWindow

When the strategy is set to DisposeOnDetachedFromWindow, the underlying Composition will be disposed when:

the ComposeView detaches from the window

So when does View detachment occur?

Generally, this happens when the View is going off screen and is no longer visible to the user. Some instances include:

  • When the View is removed from the View hierarchy via ViewGroup.removeView* APIs
  • When the View is part of a transition
  • When the containing Activity is being destroyed — after onStop, but before onDestroy

Note that you can listen to window attach/detach events by setting a View.OnAttachStateChangeListener via addOnAttachStateChangeListener.

Before Compose UI version 1.2.0-beta02, this strategy was the default strategy as it is the preferred strategy for a majority of use cases. However, since version 1.2.0-beta02, this default has been replaced by DisposeOnDetachedFromWindowOrReleasedFromPool.

DisposeOnDetachedFromWindowOrReleasedFromPool (Default)

When a ComposeView is used within a pooling container, such as a RecyclerView, View elements are constantly being attached and reattached to the window as elements are recycled as the UI scrolls. This means if you use DisposeOnDetachedFromWindow, the underlying Composition of ComposeViews would also constantly undergo initial Composition and disposals. Frequent disposing and recreating Compositions can hurt scrolling performance, especially when quickly flinging through the list.

To improve upon this, DisposeOnDetachedFromWindowOrReleasedFromPool disposes the Composition when:

the ComposeView detaches from the window, unless it is part of a pooling container such as a RecyclerView. When the Composition is within a pooling container, it will dispose when either the underlying pooling container itself detaches from the window, or when the item is being discarded (i.e. when the pool is full).

In other words, DisposeOnDetachedFromWindowOrReleasedFromPool is like DisposeOnDetachedFromWindow but with added functionality.

If you are curious about how this works and why it was introduced, check out Jetpack Compose Interop: Using Compose in a RecyclerView.

DisposeOnLifecycleDestroyed

When the strategy is set to DisposeOnLifecycleDestroyed, a Lifecycle or LifecycleOwner must be provided and the underlying Composition will dispose when:

the provided Lifecycle is destroyed. This strategy is appropriate when the ComposeView shares a 1–1 relationship with a known LifecycleOwner.

For instance, the snippet below disposes the Composition when a Fragment’s lifecycle is destroyed:

This strategy is beneficial in circumstances where you want to tie the Composition’s lifecycle to a known Lifecycle. The canonical example of this is a Fragment View wherein the View can be detached from the window (that is, the Fragment is no longer visible in the screen), and the Fragment might not be destroyed yet (onDestroy not yet called). This can happen on a ViewPager2’s Fragment Views as you page through content. If you were to use either of the previous strategies, the Composition would be disposed prematurely, resulting in potential state loss (for example, scroll state in a LazyColumn would not be remembered).

A question you might ask yourself is this: “What if I have a ComposeView as an item in a RecyclerView that is within a Fragment? Which strategy should I use?” The immediate ancestor will dictate which strategy to apply — so since the ComposeView is an item in a RecyclerView, you would use DisposeOnDetachedFromWindowOrReleasedFromPool, otherwise, use DisposeOnLifecycleDestroyed.

DisposeOnViewTreeLifecycleDestroyed

A related but alternative to the previous strategy is DisposeOnViewTreeLifecycleDestroyed. This strategy can be used if it is desired to tie the Composition lifecycle with a Lifecycle object, but the Lifecycle is not known yet. The underlying Composition will dispose when:

the ViewTreeLifecyleOwner of the next window the View is attached to is destroyed. This strategy is appropriate when the ComposeView shares a 1–1 relationship with their closest ViewTreeLifecycleOwner, such as a Fragment View.

For instance, the snippet below shows a custom View that inherits from AbstractComposeView. The Composition will be disposed when the closest ViewTreeLifecycleOwner is destroyed as the ViewCompositionStrategy is modified to DisposeOnViewTreeLifecycleDestroyed:

Essentially, this works by finding the associated LifecycleOwner responsible for managing the ComposeView by using the ViewTreeLifecycleOwner.get API.

A question you might have is, when should I use DisposeOnLifecycleDestroyed vs. DisposeOnViewTreeLifecycleDestroyed? If the Lifecycle object is already known, then use DisposeOnLifecycleDestroyed; otherwise, use DisposeOnViewTreeLifecycleDestroyed.

Summary

We covered all the different types of ViewCompositionStrategy options to use and how selecting the right one in an interop scenario is important to properly dispose of the Composition. See the table in the introduction of the post as a reference for when you should use what.

Have any questions? Leave a comment below!

--

--