在本页面中,您将了解可组合项的生命周期以及 Compose 如何决定可组合项是否需要重新组合。
生命周期概览
正如状态管理文档中所述,组合描述了您应用的界面,并通过运行可组合项生成。组合是描述您界面的可组合项的树状结构。
当 Jetpack Compose 首次运行您的可组合项(在初始组合期间)时,它会跟踪您在组合中调用以描述界面的可组合项。然后,当您的应用状态发生变化时,Jetpack Compose 会安排一次重新组合。重新组合是指 Jetpack Compose 重新执行可能因状态变化而改变的可组合项,然后更新组合以反映任何变化。
组合只能通过初始组合生成,并通过重新组合更新。修改组合的唯一方法是通过重新组合。
图 1. 可组合项在组合中的生命周期。它进入组合,重新组合 0 次或多次,然后离开组合。
重新组合通常由 State<T>
对象的更改触发。Compose 会跟踪这些变化,并运行组合中读取特定 State<T>
的所有可组合项,以及它们调用的无法跳过的任何可组合项。
如果一个可组合项被多次调用,那么组合中也会放置多个实例。每次调用在组合中都有其自己的生命周期。
@Composable fun MyComposable() { Column { Text("Hello") Text("World") } }
图 2. MyComposable
在组合中的表示。如果一个可组合项被多次调用,那么组合中也会放置多个实例。元素颜色不同表示它是单独的实例。
可组合项在组合中的剖析
组合中可组合项的实例由其调用点标识。Compose 编译器将每个调用点视为不同的。从多个调用点调用可组合项将在组合中创建该可组合项的多个实例。
如果在重新组合期间,可组合项调用的可组合项与上次组合期间不同,Compose 将识别哪些可组合项被调用或未被调用,并且对于在两次组合中都被调用的可组合项,如果它们的输入没有改变,Compose 将避免重新组合它们。
保留身份对于将副作用与其可组合项关联起来至关重要,这样它们才能成功完成,而不是在每次重新组合时重新开始。
考虑以下示例
@Composable fun LoginScreen(showError: Boolean) { if (showError) { LoginError() } LoginInput() // This call site affects where LoginInput is placed in Composition } @Composable fun LoginInput() { /* ... */ } @Composable fun LoginError() { /* ... */ }
在上面的代码片段中,LoginScreen
将有条件地调用 LoginError
可组合项,并始终调用 LoginInput
可组合项。每次调用都有唯一的调用点和源位置,编译器将使用它们来唯一标识它。
图 3. 状态改变并发生重新组合时 LoginScreen
在组合中的表示。相同的颜色表示它尚未重新组合。
尽管 LoginInput
从第一次被调用变为第二次被调用,但 LoginInput
实例将在重新组合期间保留下来。此外,由于 LoginInput
没有在重新组合期间发生变化的任何参数,因此 Compose 将跳过对 LoginInput
的调用。
添加额外信息以帮助智能重新组合
多次调用一个可组合项也会将其多次添加到组合中。当从同一个调用点多次调用一个可组合项时,Compose 没有唯一标识对该可组合项的每次调用的信息,因此除了调用点之外,还使用执行顺序来保持实例的区分。这种行为有时是所需的,但在某些情况下可能会导致意外行为。
@Composable fun MoviesScreen(movies: List<Movie>) { Column { for (movie in movies) { // MovieOverview composables are placed in Composition given its // index position in the for loop MovieOverview(movie) } } }
在上面的示例中,Compose 除了调用点之外,还使用执行顺序来保持实例在组合中的区分。如果将新电影
添加到列表的底部,Compose 可以重用组合中已有的实例,因为它们在列表中的位置没有改变,因此这些实例的电影
输入是相同的。
图 4. 新元素添加到列表底部时 MoviesScreen
在组合中的表示。组合中的 MovieOverview
可组合项可以被重用。MovieOverview
中相同的颜色表示该可组合项尚未重新组合。
但是,如果电影
列表因添加到列表的顶部或中间、移除或重新排序项而发生更改,则会导致所有输入参数在列表中位置发生改变的 MovieOverview
调用进行重新组合。这一点非常重要,例如,如果 MovieOverview
使用副作用获取电影图片。如果在效果进行期间发生重新组合,它将被取消并重新开始。
@Composable fun MovieOverview(movie: Movie) { Column { // Side effect explained later in the docs. If MovieOverview // recomposes, while fetching the image is in progress, // it is cancelled and restarted. val image = loadNetworkImage(movie.url) MovieHeader(image) /* ... */ } }
图 5. 新元素添加到列表时 MoviesScreen
在组合中的表示。MovieOverview
可组合项无法重用,所有副作用都将重新开始。MovieOverview
中不同的颜色表示该可组合项已重新组合。
理想情况下,我们希望将 MovieOverview
实例的身份与传递给它的 movie
的身份关联起来。如果我们重新排序电影列表,理想情况下我们也会相应地重新排序组合树中的实例,而不是用不同的电影实例重新组合每个 MovieOverview
可组合项。Compose 提供了一种方法,让您可以告诉运行时您希望使用哪些值来标识树的给定部分:即 key
可组合项。
通过将代码块与对 key 可组合项的调用包装起来,并传入一个或多个值,这些值将组合起来用于在组合中标识该实例。key
的值不需要全局唯一,它只需要在调用点上可组合项的调用中是唯一的。因此,在此示例中,每部电影
都需要有一个在所有电影
中唯一的 key
;如果它与应用中其他地方的某个可组合项共享该 key
,那也无妨。
@Composable fun MoviesScreenWithKey(movies: List<Movie>) { Column { for (movie in movies) { key(movie.id) { // Unique ID for this movie MovieOverview(movie) } } } }
有了上述功能,即使列表上的元素发生变化,Compose 也能识别对 MovieOverview
的独立调用并可以重用它们。
图 6. 新元素添加到列表时 MoviesScreen
在组合中的表示。由于 MovieOverview
可组合项具有唯一的键,Compose 会识别哪些 MovieOverview
实例没有改变,并可以重用它们;它们的副作用将继续执行。
一些可组合项对 key
可组合项有内置支持。例如,LazyColumn
接受在 items
DSL 中指定自定义 key
。
@Composable fun MoviesScreenLazy(movies: List<Movie>) { LazyColumn { items(movies, key = { movie -> movie.id }) { movie -> MovieOverview(movie) } } }
如果输入未更改则跳过
在重新组合期间,如果某些符合条件的可组合函数的输入自上次组合以来没有改变,则可以完全跳过它们的执行。
可组合函数符合跳过条件,除非
- 该函数具有非
Unit
返回类型 - 该函数已使用
@NonRestartableComposable
或@NonSkippableComposable
进行注解 - 必需参数的类型不稳定
有一个实验性编译器模式,强跳过 (Strong Skipping),它放宽了最后一个要求。
为了使类型被视为稳定,它必须符合以下约定
- 两个实例的
equals
结果对于相同的两个实例将永远保持不变。 - 如果类型的公共属性发生变化,组合将收到通知。
- 所有公共属性类型也都是稳定的。
有一些重要的常见类型符合此约定,Compose 编译器会将其视为稳定类型,即使它们没有通过使用 @Stable
注解显式标记为稳定。
- 所有原始值类型:
Boolean
、Int
、Long
、Float
、Char
等。 - 字符串
- 所有函数类型(lambda 表达式)
所有这些类型都能够遵循稳定契约,因为它们是不可变的。由于不可变类型永不改变,因此它们无需通知组合更改,因此遵循此契约要容易得多。
一个值得注意的稳定但可变的类型是 Compose 的 MutableState
类型。如果一个值保存在 MutableState
中,则整个状态对象被认为是稳定的,因为 Compose 会收到对 State
的 .value
属性的任何更改通知。
当作为参数传递给可组合项的所有类型都是稳定类型时,将根据可组合项在 UI 树中的位置比较参数值的相等性。如果所有值自上次调用以来未发生变化,则跳过重新组合。
Compose 只有在能够证明一个类型是稳定时才会将其视为稳定。例如,接口通常被视为不稳定,而具有可变公共属性但其实现可能是不可变的类型也不是稳定的。
如果 Compose 无法推断某个类型是否稳定,但您希望强制 Compose 将其视为稳定,请使用 @Stable
注解标记它。
// Marking the type as stable to favor skipping and smart recompositions. @Stable interface UiState<T : Result<T>> { val value: T? val exception: Throwable? val hasError: Boolean get() = exception != null }
在上面的代码片段中,由于 UiState
是一个接口,Compose 通常会认为此类型不稳定。通过添加 @Stable
注解,您告诉 Compose 此类型是稳定的,从而使 Compose 倾向于智能重新组合。这也意味着如果接口用作参数类型,Compose 会将其所有实现视为稳定。
为您推荐
- 注意:链接文本在 JavaScript 关闭时显示
- 状态和 Jetpack Compose
- Compose 中的副作用
- 在 Compose 中保存界面状态