更多翻译好文:
文章作者:Chris Arriola
原文链接:ViewCompositionStrategy Demystified | by Chris Arriola)
©️一切版权归作者所有,本译文仅用于技术交流请勿用于商业用途,未经允许禁止转载,违者后果自负]
在 Jetpack Compose 中,组合(Composition)是一种树状结构,通过可运行的可组合项生成,用于描述app 的UI。当不再需要组合时,Jetpack Compose 将不再跟踪其状态,并且组合会被释放以便释放资源。
View重组策略(下面称ViewCompositionStrategy)定义应如何释放组合,默认情况下,ViewCompositionStrategy.Default 会在底层 ComposeView 从窗口分离时释放组合,除非它是池容器(例如RecyclerView)的一部分。然而,如果你在逐步在项目中使用Compose的时候,这种默认策略可能会导致某些场景下的状态丢失。例如,如果你在基于Fragment的Compose程序中看到了诸如滚动位置重置之类的奇怪故障,则可能是你使用了错误的重组策略(你应该改用一种基于生命周期的策略)。
本文中,我将介绍什么ViewCompositionStrategy、为什么需要它、以及如何为你的用例选择正确的策略以避免状态丢失。
简介
1.DisposeOnDetachedFromWindow
当组合依赖的ComposeView从窗口分离时,组合将被释放。这种策略目前已经被DisposeOnDetachedFromWindowOrReleasedFromPool所取代了。
使用场景:
- ComposeView是View层次结构中的唯一元素,或者是View/Compose的混合结构中的一员(不在Fragment中)。
2.DisposeOnDetachedFromWindowOrReleasedFromPool (默认)
当组合不在池容器中(例如RecyclerView)时,它类似于DisposeOnDetachedFromWindow,如果组合在池容器中时,它将在池容器本身与窗口分离式或者Item被丢弃时(即池已满)分离。(译者注:两种情况,第一种是RecyclerView从窗口分离时,第二种是由于池满了,RecyclerView将要抛弃一些Item对应的View时)。
使用场景:
- ComposeView是View层次结构中的唯一元素,或者是View/Compose的混合结构中的一员(不在Fragment中)。
ComposeView是池容器里面的一个item,例如RecyclerView。
3.DisposeOnLifecycleDestroyed
当ComposeView对应的Lifecycle被销毁时,组合将被处理。
使用场景:
- 作为Fragment中的View的ComposeView。
4.DisposeOnViewTreeLifecycleDestroyed
当ViewTreeLifecycleOwner提供的Lifecycle被销毁时,组合将被释放(译者注:viewTree提供的Lifecycle和组件提供的Lifecycle生命周期不一致,例如Fragment#mView和Fragment提供的Lifecycle不一样,这里不展开,请读者自行查阅)。
使用场景:
- 作为
Fragment中的View的ComposeView。 - 生命周期未知的
View中的ComposeView。
使用 ViewCompositionStrategy释放组合
当组合进入释放阶段,当满足特定条件时,ViewCompositionStrategy会自动释放组合。一旦组合被释放后,资源将被清理,也不会继续追踪状态。
应用的具体策略将决定何时应自动释放组合。如果没有策略,你将不得不显式调用 ComposeView 的 disposeComposition 来释放它内部的组合。
幸好,当你创建 ComposeView(或从 ComponentActivity 调用 setContent)时,已经应用了 ViewCompositionStrategy.Default 定义的默认策略,目前的默认策略是DisposeOnDetachedFromWindowOrReleasedFromPool,因此在绝大多数情况下,你不必显式去声明它。但是,你可以通过 setViewCompositionStrategy来更改为不同的策略。
val composeView = ComposeView(context = context)
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
纯Compose应用程序 vs View/Compose混合应用程序
在单Activity纯Compose的App中,通常只有一个组合处于活跃状态,我之所以说通常是因为有一些例外——例如subcomposition(译者注:译者也没搞懂这个具体指的是什么)——但这超出了这篇文章的范围。第一次的重组发生于Activity被创建时(is created)。Activity通过调用setContent来执行可组合项,并且组合保持活跃直到组合的内容与窗口分离——这种分离发生于Activity被销毁时。这是ComposeView默认的ViewCompositionStrategy,如果你正在写一个纯Compose的App,这个策略就是你应该使用的。
只有一个组合的纯Compose应用
ComposeView的每个实例都维护自己独立的组合。因此,如果你正在逐步将基于View的App迁移到 Compose,你可能有多个组合。例如,如果你有一个通过Fragment进行分页的 ViewPager2,并且每个片段的内容都在 Compose中,则每个 ComposeView 都将是一个单独的组合。
混合View/Compose APP,其中 `ViewPager2` 中的每个 `ComposeView` 都维护一个单独的组合
每个组合与具有生命周期的组件(例如 Activity 或 Fragment)之间的交互是你可能必须更改默认 ViewCompositionStrategy 以便在正确的时间释放的原因。
不同的 ViewCompositionStrategy 类型
DisposeOnDetachedFromWindow
当策略设置为DisposeOnDetachedFromWindow时,ComposeView内部的组合将被释放在如下情况:
ComposeView从窗口分离。
那么View的分离发生在什么时候呢?
一般来说,当View离开屏幕而且用户不可再见时,就会发生这种情况。下面是一些例子:
- 通过
ViewGroup#removeView,将View从层次结构中移除。 View是transition的一部分时。- 当
Activity被销毁时——onStop之后,但onDestroy之前。
请注意,你可以通过 addOnAttachStateChangeListener 设置 View.OnAttachStateChangeListener 来监听窗口附加/分离事件。
在 Compose UI 版本 1.2.0-beta02 之前,此策略是默认策略,因为它是大多数用例的首选策略。但是,从 1.2.0-beta02 版本开始,此默认值已被 DisposeOnDetachedFromWindowOrReleasedFromPool 取代。
DisposeOnDetachedFromWindowOrReleasedFromPool (默认)
当 ComposeView 在池化容器(例如 RecyclerView)中使用时,View 元素会不断地附加和重新附加到窗口,因为元素会随着 UI 滚动而被回收。这意味着如果您使用 DisposeOnDetachedFromWindow,ComposeViews 的底层组合也会不断地进行初始化和释放。频繁释放和重新创建组合会损害滚动性能,尤其是在快速浏览列表时。
为了改进这一点,DisposeOnDetachedFromWindowOrReleasedFromPool 在以下情况下处理组合:
ComposeView与窗口分离,除非它是池容器(如RecyclerView)的一部分。当组合在池容器中时,它将在底层池容器本身与窗口分离时或项目被丢弃时(即池已满时)释放。
换句话说,DisposeOnDetachedFromWindowOrReleasedFromPool 类似于 DisposeOnDetachedFromWindow 但有额外的功能。
如果您对它的工作原理和引入原因感到好奇,请查看Jetpack Compose Interop: Using Compose in a RecyclerView。
DisposeOnLifecycleDestroyed
当策略设置为DisposeOnLifecycleDestroyed时,必须提供Lifecycle或者LifecycleOwner,同时组合将在如下情况被释放:
Lifecycler被销毁。当ComposeView与已知的LifecycleOwner是一对一的关系时(例如一个Fragment View),此策略是合适的。
例如,下面的代码片段在 Fragment 的Lifecycle被销毁时释放组合:
class MyFragment : Fragment() {
// …
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = ComposeView(requireContext()).apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnLifecycleDestroyed(
lifecycle = this@MyFragment.lifecycle
)
)
// …
}
}
在你希望将组合的生命周期与已知Lifecycle相关联的情况下,此策略很有用。一个典型的例子就是一个Fragment的View,当该View从窗口中分离(也就是说,Fragment在屏幕上不再可见),并且Fragment可能还没有被销毁(onDestory未被调用)。这种情况会发生在你滑动ViewPager2的Fragment时。如果你使用上述的任何一种策略,组合将被过早的释放,从而导致潜在的状态丢失(例如,丢失LazyColumn中的滚动状态)。
你可能会问自己的一个问题是:“如果我有一个 ComposeView 作为 RecyclerView 中的一个item,同时RecyclerView位于 Fragment 中应该怎么办?”。
应该用ComposeView的直接祖先来决定应用哪种策略——因此,由于ComposeView是RecyclerView中的一个item,你将使用DisposeOnDetachedFromWindowOrReleasedFromPool,否则使用DisposeOnLifecycleDestroyed。
DisposeOnViewTreeLifecycleDestroyed
DisposeOnViewTreeLifecycleDestroyed和前一个策略很像,但是它是前一个策略的替代方法。如果希望组合的生命周期和Lifecycle联系起来,但Lifecycle尚不清楚,可以使用此策略。组合将会在如下情况释放:
View添加到的ViewTreeLifecyleOwner被销毁时。当ComposeView与已知的ViewTreeLifecycleOwner是一对一的关系时(例如一个Fragment View),此策略是合适的。
例如,下面的代码片段显示了一个继承自 AbstractComposeView 的自定义View。 组合将在最近的 ViewTreeLifecycleOwner 被销毁时被释放,因为 ViewCompositionStrategy 被修改为 DisposeOnViewTreeLifecycleDestroyed:
class MyCustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {
init {
// Related lifecycle may not be known at this point yet
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
}
@Composable
override fun Content() {
// Compose
}
}
本质上,这是通过使用 ViewTreeLifecycleOwner.get API 查找负责管理 ComposeView 的关联 LifecycleOwner 来实现的。 你可能有一个问题,我应该什么时候使用 DisposeOnLifecycleDestroyed 与 DisposeOnViewTreeLifecycleDestroyed?如果 Lifecycle 对象已知,则使用 DisposeOnLifecycleDestroyed;否则,使用 DisposeOnViewTreeLifecycleDestroyed。
总结
我们介绍了要使用的所有不同类型的 ViewCompositionStrategy 选项,以及在互操作场景中选择正确的选项对于正确处理组合的重要性。