小白参考 Android Compose 官方文档的一些学习笔记,如有错误恳请指正。
官方文档链接:Compose 官方文档
可组合项的生命周期
一、可组合项是什么
可组合项 Compose 中用于描述界面元素的基础单元,本质是被 @Composable 注解标记的函数。核心作用是 “描述界面应该是什么样子”,而非像 xml 中使用 findViewById 操作 UI 组件。
二、可组合项的生命周期
可组合项的生命周期只有三个关键阶段:
- 进入组合(Enter the Composition):当 Compose 首次执行这个可组合项时,它会把这个组件加入到 “组合树” 里,相当于组件 “出生”,开始在界面树中占据位置。
- 执行 0 次或多次重组(Recompose):当应用的状态(比如按钮点击、数据更新)发生变化时,Compose 会判断这个可组合项是否需要重新执行来更新界面,这就是 “重组”。如果组件不依赖变化的状态,可能一次重组都不会执行;如果依赖,就会多次更新,相当于组件在 “工作中调整自己”。
- 退出组合(Leave the Composition):当组件不再需要(比如页面关闭、条件判断不显示该组件),就会从组合树中移除,相当于组件 “消失”,生命周期结束。

所以,在 Compose 中如果一个组件一旦定义,想要修改只能通过重组更新,没有其它方式可以修改。而重组通常是由 State<T> 类型的状态变化触发的,比如 mutableStateOf 包裹的值变了,Compose 就会触发所有依赖这个状态的可组合项的重组。
@Composable
fun MyComposable() {
Column {
Text("Hello")
Text("World")
}
}
另外,如果一个可组合项被多次调用,那么每个调用都会在组合树里生成一个独立的实例,每个实例都有自己的生命周期,互不干扰。比如在 Column 里放了两个 Text,它们就是两个独立的可组合项实例,一个的状态变化不会影响另一个的生命周期。

三、组合树中的身份标识:调用点与实例
在上面例子中,一个可组合项被多次调用,Compose 是怎么区分不同可组合项实例的?如果某一个可组合项实例发生更改的话,是如何精准定位到该发生更改的实例使其重组?这里的核心就是:调用点 (调用可组合项的源代码位置)。
@Composable
fun LoginScreen(showError: Boolean) {
if (showError) {
LoginError()
}
LoginInput()
}
@Composable
fun LoginInput() { /* ... */ }
@Composable
fun LoginError() { /* ... */ }
例如,在 LoginScreen 里有个条件判断:当 showError 为 true 时显示 LoginError,而 LoginInput 始终显示。当 showError 从 false 变成 true 时,LoginError 会 “进入组合”,但 LoginInput 的调用点没变,所以它的实例会被保留不会重新创建,如果 LoginInput 的任何参数都没有变化,Compose 将避免重组这些可组合项,即跳过 LoginInput 的重组。

如果从同一个调用点多次调用某个可组合项,Compose 就无法唯一标识对该可组合项的每次调用,因此除了调用点之外,还会使用执行顺序来区分实例,即:“调用点 + 执行顺序” 来区分实例。
@Composable
fun MoviesScreen(movies: List<Movie>) {
Column {
for (movie in movies) {
MovieOverview(movie)
}
}
}
同一个调用点多次调用某个可组合项会出现一个问题:
执行顺序的局限性
如果在列表顶部或中间新增内容,移除项目或对项目进行重新排序而导致 movies 列表发生改变,将导致输入参数在列表中的位置已更改的所有 MovieOverview 调用发生重组。比如在列表顶部新增一个电影导致 movies 列表发生改变,原来第 1 位的 MovieOverview 会变成第 2 位,执行顺序变了,Compose 会认为这是一个 “新实例”,原来的实例会被销毁再重建。如果 MovieOverview 里有耗时的操作,例如加载图片等,就会导致加载被取消、重新开始。

但是在列表底部新增了一个 movie 就不会出现这样的问题,Compose 可以重复使用组合中既有的实例,因为这些实例在列表中的位置没有发生变化,因此这些实例的 movie 输入是相同的。

为了使 movies 列表即使发生了变化顺序被改变,Compose 也可以复用组合中已有的实例,不要全部销毁再重建的解决办法:用 key 给实例 “唯一身份证”
@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
Column {
for (movie in movies) {
// 用电影的唯一 id 作为 key
// key 的值不必是全局唯一,只需要在调用点处调用可组合项的作用域内确保其唯一性即可。
key(movie.id) {
MovieOverview(movie)
}
}
}
}
// LazyColumn 这样的常用可组合项已经内置了 key 支持:
@Composable
fun MoviesScreenLazy(movies: List<Movie>) {
LazyColumn {
items(movies, key = { movie -> movie.id }) { movie ->
MovieOverview(movie)
}
}
}
这样给每个实例一个唯一标识 movie.id 不管列表怎么排序、增删,只要 key 不变,Compose 就认为是同一个实例,不会重新创建。

四、性能优化 - 跳过重组
Compose 有个很智能的优化逻辑:如果可组合项的输入参数没变化,就会跳过它的重组,避免不必要的计算,提升性能。但不是所有可组合项都能被跳过,需要满足一定条件,也和输入参数的 “稳定性” 密切相关。
1. 可组合项能被跳过的条件
只有满足以下所有条件,Compose 才会在输入不变时跳过重组:
- 可组合项的返回值类型是
Unit(大多数 UI 可组合项都是如此,比如Text、Column); - 没有被
@NonRestartableComposable或@NonSkippableComposable注解标记; - 所有必需参数的类型都是 “稳定类型” (val)。
2. 什么是 “稳定类型”?
稳定类型要符合三个协定,简单说就是 “可预测、能通知、全稳定”:
- 相同的两个实例,调用
equals()方法的结果永远一致(比如Int类型,1 和 1 永远相等); - 如果类型的公共属性变了,Compose 能收到通知(比如
MutableState,它的value变了会触发重组); - 该类型所有的公共属性类型也都是稳定类型。
Compose 会默认把以下这些类型当作稳定类型,不用我们额外处理:
- 所有基本数据类型:
Boolean、Int、Long、Float、Char等; - 字符串(
String),因为它是不可变的; - 所有函数类型(lambda),同样是不可变的;
MutableState类型,虽然它是可变的,但value变化时能通知 Compose,所以被视为稳定。
而像接口(比如自定义的 UiState 接口)、有可变公共属性的类(比如一个 User 类有 var name: String),Compose 默认会认为是 “不稳定类型”,即使输入没变化,也可能触发重组。
3. @Stable 注解
如果我们自定义的类型明明稳定协定,但 Compose 无法自动推断(比如接口),或者可以保证输入的数据是不变的,就可以用 @Stable 注解手动标记,告诉 Compose“这个类型是稳定的,放心用它来判断是否跳过重组”。比如下面的 UiState 接口,虽然是接口,但它的属性满足稳定条件,加了 @Stable 后,Compose 会把它当作稳定类型处理:
@Stable
interface UiState<T : Result<T>> {
val value: T?
val exception: Throwable?
val hasError: Boolean
get() = exception != null
}
注意:加 @Stable 注解是 “承诺” 该类型符合稳定协定,所以一定要确保代码逻辑满足条件,否则可能导致界面更新异常。
五、可组合项生命周期总结
1. 生命周期核心
- 可组合项生命周期:进入组合 → 0 次 / 多次重组 → 退出组合;
- 重组触发源:
State<T>类型状态变化; - 多实例规则:同一可组合项可多次调用生成多个独立实例,各有自己的生命周期。
2. 组合树实例识别
- 基础标识:调用点(源代码位置);
- 重复调用问题:同一调用点多次调用时,默认用 “调用点 + 执行顺序” 标识,列表变化易导致实例重建;
- 解决方案:用
key可组合项(或LazyColumn内置key),通过唯一标识(如movie.id)稳定实例身份,避免不必要的重组和效应重启。
3. 重组优化(输入不变则跳过)
- 可跳过条件:返回值
Unit、无特殊注解、参数类型稳定; - 稳定类型:默认包含基本类型、
String、lambda、MutableState;自定义类型符合协定可加@Stable手动标记; - 优化逻辑:参数为稳定类型且值未变时,Compose 跳过重组,提升性能。
3467

被折叠的 条评论
为什么被折叠?



