Composables 的生命周期

生命周期

组合(Composition)描述了应用的 UI,它是通过运行 composables 生成的。组合是 composables 构成的描述 UI 的树形结构。

当 Jetpack Compose 首次运行你的 composables 时,在初次组合时,它会跟踪你为描述 UI 而调用的 composables。然后,当应用的状态发生变化时,Jetpack Compose 会安排一次重组。重组是指 Jetpack Compose 重新执行那些可能因状态变化而发生改变的 composables ,然后更新组合以反映变化。

组合只能通过初次组合生成,并通过重组进行更新。修改组合的唯一方式是通过重组。

上图展示了组合中 Composable 的生命周期:它进入组合,经历 0 次或多次重组,然后离开组合。

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

如果一个 composable 被多次调用,组合中将放置多个实例。每次调用在组合中都有自己的生命周期。

kotlin 复制代码
@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

上图展示了组合中的 MyComposable。如果一个 composable 被多次调用,组合中会放置多个实例。颜色不同的元素表明它是一个单独的实例。

剖析组合中的 composable

组合中 composable 的实例由其调用点标识,Compose 编译器认为每个调用点都不一样。从多个调用点调用 composables 将在组合中创建该 composable 的多个实例。

如果在重组过程中,某个 composable 调用的 composables 与上一次组合时不同,Compose 会识别出哪些 composables 被调用了、哪些没有被调用,而对于在两次组合中都被调用的 composables ,如果它们的输入没有变化,Compose 不会重组它们。

Compose 会把 Side-effect 和它的宿主组件绑定在一起。只要宿主组件的标识没变,副作用就能一直活着,而不是在每次重组时都重新开始。比如,假设你要下载一张图片(Side-effect),这是一个耗时操作。第一次界面重组,下载任务开始。假如用户输入了文字,输入变了,界面重组,Compose 没认出这是同一个下载任务,以为旧的没了。此时会取消之前的下载任务,启动新的下载任务,重新开始下载。

看如下示例:

kotlin 复制代码
@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 c,并且总会调用 LoginInput 函数。每次调用都有唯一的调用点和源位置,编译器会利用这些来对其进行唯一标识。

上图展示了状态发生变化出现重组时,组合中 LoginScreen 的表现。颜色相同意味着它没有被重组。

即使 LoginInput 从第一次被调用变成第二次被调用,LoginInput 实例将在重组中保留。另外,因为LoginInput 在重组过程中参数没有发生改变,所以 Compose 将跳过对 LoginInput 的调用。

添加额外信息以帮助智能重组

多次调用一个 composable 也会将其多次添加到组合中。当从同一个调用点多次调用 composable 时,Compose 没有任何信息来唯一标识对该 composable 的每次调用,因此除了调用点之外,还会使用执行顺序来区分不同的实例。这种行为有时是需要的,但在某些情况下,它可能会导致不想要的行为。

复制代码
@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 可以重用组合中已有的实例,因为这些实例在列表中的位置没有改变,因此,对于这些实例来说,电影输入是相同的。

上图展示了当一个新元素被添加到列表底部时,MoviesScreen 在组合中的呈现。组合中的 MovieOverview 组件可以被复用。MovieOverview 中相同的颜色意味着 composable 没有被重组。

然而,如果电影列表发生了这些变化:在列表顶部或中间添加、删除或重排 item,都会导致所有在列表中位置发生变化的 MovieOverview 重组。这一点就极为重要,比如,如果 MovieOverview 使用 Side-effect 获取电影图像。如果在 effect 过程中发生重组,该 effect 将会被取消并重新开始。

复制代码
@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)

        /* ... */
    }
}

上图展示了当列表中添加新元素时,MoviesScreen 在组合中的呈现情况。MovieOverview composables 无法被重用,所有 Side-effects 都将重新开始执行。MovieOverview 中不同的颜色意味着该 composable 已重组。

理想情况下,我们希望将 MovieOverview 实例的标识与其所传入 movie 的标识相关联。Compose 提供了一种方法,让你可以告知运行时你希望使用什么值来标识树的特定部分:key composable。

通过调用 key composable 并传入一个或多个值来包裹一段代码块,这些值将用于标识组合中的实例。key 的值不需要全局唯一,只需要在调用点的 composables 中保持唯一即可。因此,在这个示例中,每部 movie 需要有一个在 movies 中唯一的 key;即使它与 App 中其他地方的某个 composable 共享该 key 也是可以的。

kotlin 复制代码
@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}

这样即便列表中的元素发生变化,Compose 也能识别对 MovieOverview 的调用并可以复用它们。

上图展示了向列表中添加新元素时,MoviesScreen 在组合中的呈现。由于 MovieOverview composables 具有唯一键,Compose 能够识别哪些 MovieOverview 实例没有改变,然后可以重用它们;它们的 Side-effect 将继续执行。

一些 composables 内置了对 key composable 的支持。例如,LazyColumn 允许在 items DSL 中指定一个自定义 key。

复制代码
@Composable
fun MoviesScreenLazy(movies: List<Movie>) {
    LazyColumn {
        items(movies, key = { movie -> movie.id }) { movie ->
            MovieOverview(movie)
        }
    }
}

跳过输入未发生变化的

在重组过程中,如果某些符合条件的 composable 函数的输入相比上一次组合没有变化,它们的执行会被完全跳过。在以下条件下, composable 函数不可以被跳过:

  • 该函数具有非 Unit 的返回类型;
  • 该函数被标注了 @NonRestartableComposable 或 @NonSkippableComposable;
  • 某个必需参数的类型是非稳定(non-stable)类型的;

有一种实验性的编译器模式 ------ 强跳过(Strong Skipping),它会放宽最后一项要求。

一种类型要被视为稳定类型,必须遵守以下约定:

  • 对于相同的两个实例,它们的 equals 方法的结果始终保持一致。
  • 如果该类型的某个公共属性发生变化,组合会收到通知。
  • 所有公共属性的类型也都是稳定的。

有一些重要的常见类型符合这一约定,即使它们没有通过 @Stable 注解明确标记为稳定类型,Compose 编译器也会将它们视为稳定类型,这些类型包括:

  • 所有基本值类型:Boolean、Int、Long、Float、Char 等。
  • 字符串(Strings)
  • 所有函数类型(lambda 表达式)

所有这些类型都能遵守稳定类型的约定,因为它们是不可变的。由于不可变类型永远不会发生变化,所以它们永远不需要通知组合,因此更容易遵守这一约定。

一种值得注意的稳定但可变的类型是 Compose 的 MutableState 类型。如果一个值保存在 MutableState 中,那么整个状态对象会被视为稳定的,因为当 State 的 value 属性发生任何变化时,Compose 都会收到通知。

当所有作为参数传递给 composable 的类型都是稳定的时,会基于 composable 在 UI 树中的位置对参数值进行相等性比较。如果自上次调用以来所有值都未发生改变,则会跳过重组。

只有 Compose 能够证实,它才会认为该类型是稳定的。例如,接口通常被视为是不稳定的,那些具有可变公共属性但其实现可能不可变的类型也同样不稳定。

如果 Compose 无法推断出某个类型是稳定的,但你希望强制 Compose 将其视为稳定类型,可以用 @Stable 注解对其进行标记。

kotlin 复制代码
// 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 会将其所有实现视为稳定的。


参考:https://developer.android.com/develop/ui/compose/lifecycle

相关推荐
修炼者10 小时前
【进阶Android】HashMap 的并发“车祸”
android
冬奇Lab12 小时前
Android 15音频子系统(六):音频焦点管理机制深度解析
android·音视频开发·源码阅读
LionelRay14 小时前
Thinking in Compose
android
用户693717500138416 小时前
Google 推 AppFunctions:手机上的 AI 终于能自己干活了
android·前端·人工智能
用户693717500138416 小时前
AI让编码变简单,真正拉开差距的是UI设计和产品思考
android·前端·人工智能
zh_xuan16 小时前
Android Jetpack DataStore存储数据
android·android jetpack·datastore
程序员陆业聪16 小时前
在 Android 上跑大模型,你选错引擎了吗?
android
studyForMokey18 小时前
【Android面试】View绘制流程专题
android·面试·职场和发展
jjinl19 小时前
Android 资源说明
android