深入理解Jetpack Compose中的函数的执行顺序

本文译自「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副作用函数之间,谁又将先执行呢?

让我们来仔细看看。

副作用函数的执行顺序(组合完成后):

  1. DisposableEffect → 首先运行
  2. LaunchedEffect → 随后运行

为啥子呢?

此顺序由Compose运行时定义的:

  • DisposableEffect 是同步的,用于在组合后立即处理设置/清理。
  • LaunchedEffect 会启动一个协程,而协程的启动是异步的,计划在其他同步效果(例如 DisposableEffect)之后运行。

内部机制:Jetpack Compose 维护了明确定义的效果应用顺序。

  1. DisposableEffect、SideEffect、SnapshotFlow 等副作用会在组合后立即触发(同步)。
  2. 然后,基于协程的效果(例如 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中围绕可组合项的行为遵循以下逻辑:

  1. 首先进行新目的地(此处为 ScreenA)的组合。
    • 导航切换时,Compose会立即为新屏幕创建UI。
    • 新页面(此处为 ScreenA)的 DisposableEffect 和 LaunchedEffect 会在新页面组合期间或之后立即执行。
  2. 在新目的地成功组合并提交到 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 才会触发处理上一个屏幕的操作。

在调用 popBackStack() 或 navigate() 时,Compose 的 NavHost 内部的工作方式如下:

  • 新的路由组合开始(可组合项创建)。
  • 成功组合并提交帧后,不再位于 NavHost 后栈中的旧可组合项节点将被标记为待处理。
  • 然后,Compose 会在下一帧中运行这些已移除可组合项的处置逻辑 (onDispose)。

因此,即使你在视觉上立即导航回原点,销毁式的操作(如onDispose)也会略微延迟执行,以保证界面的整体稳定性。

如果你有任何疑问,请留言,我会尽快回复你。💬✨ 我们很快会深入探讨Jetpack Compose,敬请期待!🚀 在此之前,祝你coding愉快!🎉👨‍💻

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
teacher伟大光荣且正确2 小时前
Qt Creator 配置 Android 编译环境
android·开发语言·qt
飞猿_SIR5 小时前
Android Exoplayer 实现多个音视频文件混合播放以及音轨切换
android·音视频
HumoChen995 小时前
GZip+Base64压缩字符串在ios上解压报错问题解决(安卓、PC模拟器正常)
android·小程序·uniapp·base64·gzip
沙振宇9 小时前
【HarmonyOS】ArkTS开发应用的横竖屏切换
android·华为·harmonyos
橙子1991101610 小时前
Kotlin 中的 Unit 类型的作用以及 Java 中 Void 的区别
java·开发语言·kotlin
橙子1991101611 小时前
Kotlin 中的作用域函数
android·开发语言·kotlin
zimoyin11 小时前
Kotlin 懒初始化值
android·开发语言·kotlin
枣伊吕波12 小时前
第六节第二部分:抽象类的应用-模板方法设计模式
android·java·设计模式
萧然CS12 小时前
使用ADB命令操作Android的apk/aab包
android·adb
_extraordinary_16 小时前
MySQL 事务(二)
android·数据库·mysql