深入理解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 小时前
Fake Device Test作假屏幕分辨率分析
android·java
zh_xuan3 小时前
github远程library仓库升级
android·github
峥嵘life3 小时前
Android蓝牙停用绝对音量原理
android
小书房3 小时前
Kotlin的内联函数
java·开发语言·kotlin·inline·内联函数
czlczl200209254 小时前
IN和BETWEEN在索引效能的区别
android·adb
Volunteer Technology4 小时前
ES高级搜索功能
android·大数据·elasticsearch
北京自在科技5 小时前
Find Hub App 小更新
android·ios·安卓·findmy·airtag
lbb 小魔仙5 小时前
2026远程办公软件夏季深度横测:ToDesk、向日葵、网易UU远程全面对比,远控白皮书
android·服务器·网络协议·tcp/ip·postgresql
coding_fei5 小时前
AudioServer初始化过程
android
brucelee1865 小时前
Docker 运行 Android 模拟器
android·docker·容器