在本页中,您将了解可组合项的生命周期以及 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 除了使用调用点之外,还使用执行顺序来区分组合中的实例。如果列表底部新增了一个 movie
,Compose 可以重复使用组合中既有的实例,因为这些实例在列表中的位置没有发生变化,因此这些实例的 movie
输入是相同的。
图 4. 列表底部新增元素时,组合中 MoviesScreen
的表示。可以重复使用组合中的 MovieOverview
可组合项。MovieOverview
中的相同颜色表示该可组合项尚未重组。
但是,如果因在列表顶部或中间新增内容,移除项目或对项目进行重新排序而导致 movies
列表发生改变,将导致输入参数在列表中的位置已更改的所有 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
的值不必是全局唯一的,只需要在调用点处调用可组合项的作用域内确保其唯一性即可。在此示例中,每个 movie
都需要一个在 movies
的作用域内具有唯一性的 key
;movie 也可以与应用中其他位置的其他可组合项共享该 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
- 必需参数属于非稳定类型
有一个实验性编译器模式强跳过 这会放宽最后一个要求
为了让某个类型被视为稳定类型,它必须符合以下要求 合同:
- 对于相同的两个实例,其
equals
的结果将始终相同。 - 如果类型的某个公共属性发生变化,组合将收到通知。
- 所有公共属性类型也都是稳定。
此协定包含一些重要的常见类型
Compose 编译器会将其视为稳定版本,尽管它们并未明确显示
使用 @Stable
注解标记为稳定版:
- 所有基元值类型:
Boolean
、Int
、Long
、Float
、Char
等。 - 字符串
- 所有函数类型 (lambda)
所有这些类型都可以遵循稳定协定,因为它们是不可变的。由于不可变类型绝不会发生变化,它们就永远不必通知组合更改方面的信息,因此遵循该协定就容易得多。
Compose 的 MutableState
类型是一种众所周知稳定但可变的类型。如果 MutableState
中存储了值,状态对象整体会被视为稳定对象,因为 State
的 .value
属性如有任何更改,Compose 就会收到通知。
当作为参数传递到可组合项的所有类型都很稳定时,系统会根据可组合项在界面树中的位置来比较参数值,以确保相等性。如果所有值自上次调用后未发生变化,则会跳过重组。
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 中保存界面状态