ViewCompositionStrategy Demystified
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.
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.
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 beforeonDestroy
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 ComposeView
s 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 theComposeView
shares a 1–1 relationship with a knownLifecycleOwner
.
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 theComposeView
shares a 1–1 relationship with their closestViewTreeLifecycleOwner
, 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!