1.LaunchedEffect
LaunchedEffect 的核心作用是:它解决了"当某个 Composable 出现在屏幕上时,我需要执行一个异步任务(比如加载数据)"这类需求。
2.为什么需要 LaunchedEffect?
在 Compose 的世界里,Composable 函数可能会因为状态变化而频繁地被调用和重组Recomposition。
直接在 Composable 函数体内部调用挂起函数或启动一个不受管理的协程是绝对禁止的,因为:
- 存在副作用 (
Side-Effects): Composable 函数应该只负责描述 UI,保持纯净,不应有副作用。网络请求、数据库读写等都是副作用。 - 会重复执行: 每次重组都会重新调用这个异步任务,造成巨大的资源浪费和不可预测的行为。
- 生命周期问题: 当 Composable 离开屏幕时,你启动的协程可能会继续运行,导致内存泄漏或在不恰当的时候更新 UI。
LaunchedEffect 就是为了在 Compose 的声明式范式中,安全、可控地处理这些副作用而设计的。
3.LaunchedEffect 的核心工作原理与特性
LaunchedEffect方法定义如下:
kotlin
@Composable
fun LaunchedEffect(
key1: Any?,
// key2: Any?, ...
block: suspend CoroutineScope.() -> Unit
)
-
首次启动时机:进入组件树时当 LaunchedEffect 首次进入组件树,即 Composable 第一次被组合显示时,它会启动一个协程,并执行你传入的 block 代码块。
-
取消时机:当包含 LaunchedEffect 的 Composable 从组件树中被移除时,它会自动取消(cancel)之前启动的协程。这确保了不会有协程在 Composable 销毁后仍在后台运行,从而完美地解决了生命周期管理和内存泄漏问题。
-
重启时机:key 参数发生变化时,即key1, key2, ... 这些参数是 LaunchedEffect 的"依赖项"。LaunchedEffect 会记住上一次组合时传入的 key 值,在下一次重组时,如果任何一个 key 的值发生了变化,LaunchedEffect 会执行以下操作:
1.取消正在运行的旧协程。
2.立即启动一个新协程,重新执行 block 代码块。
如果所有 key 的值都没有变化,那么 LaunchedEffect 什么都不会做,已有的协程会继续运行。这个 key 机制是 LaunchedEffect 功能强大的关键。
4.常见使用场景场
场景一:一次性的初始化任务
如果你希望某个任务只在 Composable 第一次显示时执行一次,并且之后不再重复执行,可以使用一个不会改变的常量作为 key,最常见的就是 Unit或 true。
示例:进入屏幕时加载数据
kotlin
@Composable
fun UserProfileScreen(userId: String, viewModel: UserProfileViewModel) {
// 当 UserProfileScreen 首次出现时,执行加载
LaunchedEffect(Unit) { // key 为 Unit,永不改变
viewModel.loadUserData(userId)
}
// ... 显示用户信息的 UI ...
}
场景二:根据状态变化执行任务
当你希望在某个特定的 state发生变化时执行一个异步操作,就把那个 state作为 key。
示例:一个竖向的轮播控件,每2s会触发state的更新,然后让VerticalPager滚动翻页,这是一个挂起函数。
kotlin
@Composable
fun <T> LoopVerticalPager(
data : List<T>?,
content: @Composable (page: Int, item : T) -> Unit
) {
if (data.isNullOrEmpty()) {
return
}
val size = data.size
val totalSize = data.size * 1000
val pagerState = rememberPagerState(0) { totalSize }
val tickerFlow = remember {
flow {
var count = 0
while (true) {
emit(count)
count++
delay(2000L)
}
}
}
// 这里由于flow没有变,返回的State是同一个,并且内部的协程副作用由于key(Flow本身)没有变化,也没有重启
val ticker = tickerFlow.collectAsStateWithLifecycle(0)
Log.d("", "ticker:${ticker.value}")
// 这里会观察State数值的变化,如果有变化,就会触发重组
LaunchedEffect(ticker.value) {
pagerState.animateScrollToPage(ticker.value % totalSize)
}
Box(contentAlignment = Alignment.BottomCenter) {
VerticalPager(state = pagerState) { page->
content(page, data[page % size])
}
}
}
5.LaunchedEffect总结
LaunchedEffect 是 Jetpack Compose 中用于处理副作用的"瑞士军刀"。它通过与 Composable 的生命周期和 key 变化绑定,提供了一种声明式、安全且可控的方式来运行异步代码。
当你需要根据组件的出现、销毁或状态变化来触发一个一次性或可重启的后台任务时,LaunchedEffect 就是你的首选工具。
6.rememberCoroutineScope
rememberCoroutineScope 它的核心作用是:在 Composable 函数内部获取一个与当前组合生命周期绑定的 CoroutineScope,以便在非挂起环境(如点击回调、普通逻辑块)中启动协程。
如果你需要在 onClick 这种非挂起回调里启动协程(比如点击按钮后发网络请求、滑动列表、显示 Snackbar),你就必须使用 rememberCoroutineScope。
7.rememberCoroutineScope 核心特性
7.1.生命周期绑定:
•rememberCoroutineScope 返回的 scope 会自动绑定到调用它的 Composable 函数的生命周期。
•当这个 Composable 离开屏幕即从组合树中移除时,该 scope 启动的所有未完成的协程都会被自动取消。这完美地避免了内存泄漏。
7.2.线程调度:
•默认是在 主线程 Main Thread启动协程(Dispatchers.Main)。
•如果做耗时操作(如读写文件、网络请求),记得在 launch 内部切换到Dispatchers.IO。
8.常见使用场景
最典型的就是控制LazyList的滚动,或者 Drawer、BottomSheet 的开关。这些 API 都是挂起函数。
kotlin
@Composable
fun ScrollToTopList() {
val listState = rememberLazyListState()
val scope = rememberCoroutineScope() // 获取 scope
Column {
Button(onClick = {
scope.launch {
// animateScrollToItem 是挂起函数,必须在协程里调
listState.animateScrollToItem(0)
}
}) {
Text("回到顶部")
}
LazyColumn(state = listState) { /* ... */ }
}
}
9.禁忌(不要这样做)
kotlin
@Composable
fun BadExample() {
val scope = rememberCoroutineScope()
// 错误!
// 这会导致每次重组(Recomposition)都启动一个新的协程!
// 瞬间启动成百上千个协程,导致 App 崩溃或卡死。
scope.launch {
doSomething()
}
Button(onClick = {}) { Text("Bad") }
}
10.rememberCoroutineScope总结
rememberCoroutineScope 是连接 Compose 事件回调 与 协程挂起函数 的桥梁。
- 在 Composable 中定义:val scope = rememberCoroutineScope()
- 在 onClick 等回调中使用:scope.launch { ... }
- 它会自动处理生命周期取消,安全且方便。
11. rememberCoroutineScope vs LaunchedEffect
这是初学者最容易混淆的一点。
- 触发时机: rememberCoroutineScope,用户手动触发(如 onClick、onTouch); LaunchedEffect,组合(Composition)进入时或key 变化时自动触发 。
- 使用的目的: rememberCoroutineScope,用来响应事件,如点击按钮去发请求、滚动列表等; LaunchedEffect,用来初始化数据、订阅流、倒计时、根据状态变化做副作用。
- 调用的位置: rememberCoroutineScope,在点击回调 的Lambda 内部调用 scope.launch ; LaunchedEffect,直接写在 Composable 函数体中。