Composable 的生命周期

在本页面中,您将了解可组合项的生命周期以及 Compose 如何确定可组合项是否需要重新组合。

生命周期概述

管理状态文档中所述,组合描述了应用的 UI,并且是通过运行可组合项生成的。组合是描述 UI 的可组合项的树状结构。

当 Jetpack Compose 首次运行您的可组合项时(在初始组合期间),它会跟踪您为描述 UI 而调用的可组合项,并将它们保存在一个组合中。然后,当应用的状态发生变化时,Jetpack Compose 会安排重新组合。重新组合是指 Jetpack Compose 重新执行可能已因状态更改而发生更改的可组合项,然后更新组合以反映任何更改。

组合只能由初始组合生成,并通过重新组合更新。修改组合的唯一方法是通过重新组合。

Diagram showing the lifecycle of a composable

图 1. 可组合项在组合中的生命周期。它进入组合、重新组合 0 次或多次以及离开组合。

重新组合通常由对State<T>对象的变化触发。Compose 会跟踪这些变化,并运行组合中读取该特定State<T>的所有可组合项,以及它们调用的任何无法跳过的可组合项。

如果多次调用可组合项,则会在组合中放置多个实例。每次调用在组合中都有自己的生命周期。

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

Diagram showing the hierarchical arrangement of the elements in the previous code snippet

图 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可组合项。每次调用都有唯一的调用站点和源位置,编译器将使用这些位置来唯一标识它。

Diagram showing how the preceding code is recomposed if the showError flag is changed to true. The LoginError composable is added, but the other composables are not recomposed.

图 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输入是相同的。

Diagram showing how the preceding code is recomposed if a new element is added to the bottom of the list. The other items in the list have not changed position, and are not recomposed.

图 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)

        /* ... */
    }
}

Diagram showing how the preceding code is recomposed if a new element is added to the top of the list. Every other item in the list changes position and has to be recomposed.

图 5.当将新元素添加到列表时,MoviesScreen在组合中的表示。MovieOverview可组合项无法重用,所有副作用都将重新启动。MovieOverview中不同的颜色表示可组合项已被重新组合。

理想情况下,我们希望将MovieOverview实例的标识与传递给它的movie的标识相关联。如果我们重新排序电影列表,理想情况下,我们将类似地重新排序组合树中的实例,而不是使用不同的电影实例重新组合每个MovieOverview可组合项。Compose 提供了一种方法,可以让您告诉运行时要使用哪些值来标识树的特定部分:key可组合项。

通过使用一个或多个传递的值调用key可组合项来包装代码块,这些值将被组合起来用于标识组合中的该实例。key的值不需要全局唯一,它只需要在调用站点上的可组合项调用之间唯一即可。因此,在本例中,每个movie都需要一个在movies中唯一的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的各个调用并可以重用它们。

Diagram showing how the preceding code is recomposed if a new element is added to the top of the list. Because the list items are identified by keys, Compose knows not to recompose them, even though their positions have changed.

图 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注解明确标记为稳定。

  • 所有原始值类型:BooleanIntLongFloatChar等。
  • 字符串
  • 所有函数类型(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 将将其所有实现视为稳定的。