本文译自「Understanding Execution Order in Jetpack Compose: DisposableEffect, LaunchedEffect, and Composables」,原文链接proandroiddev.com/understandi...,由Sahil Thakar发布于2025年4月13日。

大家好,今天我们又来聊聊Jetpack-Compose的小话题。无论对于新手还是经验丰富的开发者来说,这都是一个小话题,但却是很关键的。我们将讨论一下Jetpack Compose中副作用和可组合项(composables)的执行顺序,特别是 DisposableEffect、LaunchedEffect 和可组合函数的执行顺序以及其生命周期交互过程。
我们将仔细探究 DisposableEffect 和 LaunchedEffect 在可组合项之间导航切换时是如何执行的,特别关注它们在返回之前访问过的页面时的行为。(许多经验丰富的开发者会告诉我我知道这一点,但我敢打赌,你们中的很多人并不知道)。
那么,让我们开始吧。
Kotlin
@Composable
fun MyComposable(cartId: String) {
val lifecycleOwner = LocalLifecycleOwner.current
// DisposableEffect observes the lifecycleOwner
DisposableEffect(lifecycleOwner) {
Log.e("Init", "DisposableEffect")
onDispose {
Log.e("Init", "DisposableEffect - onDispose")
}
}
// LaunchedEffect triggers when cartId changes
LaunchedEffect(key1 = cartId) {
Log.e("Init", "LaunchedEffect")
}
// Scaffold is the UI container
Column {
Log.e("Init", "Column")
// You can add your screen content here
}
}
Output:-
E/Init: Column
E/Init: DisposableEffect
E/Init: LaunchedEffect
执行顺序迷题:为什么Column先执行?
答案在于这些副作用 API(DisposableEffect、LaunchedEffect)相对于组合(Composition)的实际执行时间。
1. 组合阶段优先
- Jetpack Compose 首先在组合期间构建UI树。
- 此时,Column 是一个可组合函数。它会在组合阶段立即执行,以构建 UI。
- 因此:Column() 首先运行 → 打出日志 "Column"。
2. 副作用在组合期间注册,但在组合完成后执行
- DisposableEffect 和 LaunchedEffect 在组合期间注册其工作, 但它们的实际执行发生在组合完成后。
- Compose 使用内部调度程序(通过 Recomposer)在提交帧后运行副作用。
因此,实际时间线如下所示:
Bash
组合(Composition) 开始
→ Column() 运行 → 打印日志 "Column"
→ 注册 DisposableEffect 代码块
→ 注册 LaunchedEffect 代码块
组合 结束
→ 副作用函数开始执行
→ DisposableEffect 执行 → 打印日志 "DisposableEffect"
→ LaunchedEffect 启动协程 → 打印日志 "LaunchedEffect"
这里我们讨论了composables和副作用之间的执行顺序。 那么在 LaunchedEffect和 DisposableEffect副作用函数之间,谁又将先执行呢?
让我们来仔细看看。
副作用函数的执行顺序(组合完成后):
- DisposableEffect → 首先运行
- LaunchedEffect → 随后运行
为啥子呢?
此顺序由Compose运行时定义的:
- DisposableEffect 是同步的,用于在组合后立即处理设置/清理。
- LaunchedEffect 会启动一个协程,而协程的启动是异步的,计划在其他同步效果(例如 DisposableEffect)之后运行。
内部机制:Jetpack Compose 维护了明确定义的效果应用顺序。
- DisposableEffect、SideEffect、SnapshotFlow 等副作用会在组合后立即触发(同步)。
- 然后,基于协程的效果(例如 LaunchedEffect)会被调度到下一个运行(异步,通过 Recomposer)。
现在,让我们看看在可组合项之间导航切换时 DisposableEffect 和 LaunchedEffect 是如何执行的,尤其关注它们在返回之前访问过的屏幕时的行为。
输出结果会让你大吃一惊。
Kotlin
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "screenA") {
composable("screenA") {
ScreenA(
cartId = "123",
onNavigateToB = { navController.navigate("screenB") }
)
}
composable("screenB") {
ScreenB(
cartId = "456",
onNavigateBack = { navController.popBackStack() }
)
}
}
}
@Composable
fun ScreenA(cartId: String, onNavigateToB: () -> Unit) {
DisposableEffect(Unit) {
println("😇 ScreenA -> DisposableEffect")
onDispose {
println("😇 ScreenA -> DisposableEffect - onDispose")
}
}
LaunchedEffect(cartId) {
println("😇ScreenA -> LaunchedEffect")
}
Column(modifier = Modifier.padding(top = 100.dp)) {
Button(onClick = onNavigateToB) {
Text(text = "Navigate To ScreenB")
}
}
}
@Composable
fun ScreenB(cartId: String, onNavigateBack: () -> Unit) {
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
println("😇 ScreenB -> DisposableEffect")
onDispose {
println("😇 ScreenB -> DisposableEffect - onDispose")
}
}
LaunchedEffect(cartId) {
println("😇 ScreenB -> LaunchedEffect")
}
Column{
Column(modifier = Modifier) {
Button(onClick = onNavigateBack) {
Text("Back to Screen A")
}
}
}
}
Output:-
when ScreenA init
😇 ScreenA -> DisposableEffect
😇 ScreenA -> LaunchedEffect
Navigate To ScreenA -> ScreenB
😇 ScreenB -> DisposableEffect
😇 ScreenB -> LaunchedEffect
😇 ScreenA -> DisposableEffect - onDispose
Navigate back to ScreenB -> ScreenA
😇 ScreenA -> DisposableEffect
😇 ScreenA -> LaunchedEffect
😇 ScreenB -> DisposableEffect - onDispose
它(Jetpack Compose导航)内部实际发生了什么?
Compose Navigation在 NavHost中围绕可组合项的行为遵循以下逻辑:
- 首先进行新目的地(此处为 ScreenA)的组合。
- 导航切换时,Compose会立即为新屏幕创建UI。
- 新页面(此处为 ScreenA)的 DisposableEffect 和 LaunchedEffect 会在新页面组合期间或之后立即执行。
- 在新目的地成功组合并提交到 UI 层次结构后,会处理上一个页面的可组合项(此处为 ScreenB)。
- Compose 会保持上一个可组合项(此处为 ScreenB)短暂处于活动状态,直到新可组合项(此处为 ScreenA)稳定,以确保导航顺畅。
- 只有在新的可组合项(此处为 ScreenA)完全组合后,Compose 才会清理并移除(dispose)上一个可组合项(此处为 ScreenB)。
因此,导航期间的实际生命周期流程是酱婶儿的:
Bash
导航返回 (ScreenB → ScreenA)
│
├── 1️⃣ Compose 立即创建 ScreenA
│ ├─ ScreenA DisposableEffect executes instantly.
│ └─ ScreenA LaunchedEffect coroutine launched.
│
└── 2️⃣ 在ScreenA成功运行之后:
└─ ScreenB DisposableEffect onDispose runs.
为啥Compose要酱紫 搞?
Compose Navigation 会谨慎处理页面的组合,以确保丝滑(seamless)的用户体验和稳定性:
- 它不会在确保目标页面 (ScreenA) 已组合并准备就绪之前过早地处理上一个可组合项 (ScreenB)。
- 这可以避免在导航切换过程中出现视觉故障或空白屏幕。
- 只有在确保新页面安全到位后,Compose 才会触发处理上一个屏幕的操作。
Jetpack Compose NavHost内部机制(简化版本):
在调用 popBackStack() 或 navigate() 时,Compose 的 NavHost 内部的工作方式如下:
- 新的路由组合开始(可组合项创建)。
- 成功组合并提交帧后,不再位于 NavHost 后栈中的旧可组合项节点将被标记为待处理。
- 然后,Compose 会在下一帧中运行这些已移除可组合项的处置逻辑 (onDispose)。
因此,即使你在视觉上立即导航回原点,销毁式的操作(如onDispose)也会略微延迟执行,以保证界面的整体稳定性。
如果你有任何疑问,请留言,我会尽快回复你。💬✨ 我们很快会深入探讨Jetpack Compose,敬请期待!🚀 在此之前,祝你coding愉快!🎉👨💻
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!