深入理解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愉快!🎉👨‍💻

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

保护原创,请勿转载!

相关推荐
福柯柯3 分钟前
Android ContentProvider的使用
android·contenprovider
不想迷路的小男孩3 分钟前
Android Studio 中Palette跟Component Tree面板消失怎么恢复正常
android·ide·android studio
餐桌上的王子5 分钟前
Android 构建可管理生命周期的应用(一)
android
菠萝加点糖9 分钟前
Android Camera2 + OpenGL离屏渲染示例
android·opengl·camera
用户20187928316719 分钟前
🌟 童话:四大Context徽章诞生记
android
yzpyzp27 分钟前
Android studio在点击运行按钮时执行过程中输出的compileDebugKotlin 这个任务是由gradle执行的吗
android·gradle·android studio
aningxiaoxixi37 分钟前
安卓之service
android
TeleostNaCl1 小时前
Android 应用开发 | 一种限制拷贝速率解决因 IO 过高导致系统卡顿的方法
android·经验分享
用户2018792831672 小时前
📜 童话:FileProvider之魔法快递公司的秘密
android
刘龙超2 小时前
如何应对 Android 面试官 -> 玩转 JetPack ViewBinding
android jetpack