rememberCoroutineScope()
rememberCoroutineScope 是除了 LaunchedEffect 在 Compose 使用协程的另一种方式。
为什么不能直接用 lifecycleScope.launch?
那为什么不能使用常用的方式来写协程呢?
比如我直接这样写:
lifecycleScope.launch {
// ..
}
这样写,会有一个警告:Calls to launch should happen inside a LaunchedEffect and not composition
那为什么不能这样写呢?
我们启动协程所使用的 launch 函数其实是 CoroutineScope 接口的扩展函数,所以必须要提供一个 CoroutineScope 的上下文,才能调用它。比如 lifecycleScope 就提供了 LifecycleCoroutineScope 上下文。(LifecycleCoroutineScope 是 CoroutineScope 接口的实现类)
提供 CoroutineScope 上下文的目的:
- 让这个 Scope 被取消时,可以自动取消其启动的所有协程,而 Scope 会在 Activity/Fragment 被销毁时被取消,这使得协程的生命周期与 Activity/Fragment 的生命周期相绑定,确保了协程不会发生泄漏。
- 提供协程启动需要的上下文信息,并对这些协程进行管理。
关键在于,Composable 函数的生命周期和 Activity/Fragment 的生命周期不同,Composable 的生命周期是:因首次调用或重组进入组合 (Composition) 作为其开始,因不再是 UI 树的一部分而离开组合 (Leaving Composition) 作为其结束。
因此,我们在 Composable 函数中启动的协程,也应该在该 Composable 离开组合时自动被取消掉,以避免潜在的内存泄漏和不必要的后台工作。
补充:不能使用 viewModelScope 来启动协程,也是同样的道理, viewModelScope 的生命周期与 ViewModel 的生命周期作用相绑定。
rememberCoroutineScope()
Compose 给我们提供了 rememberCoroutineScope() 函数,它能创建一个 CoroutineScope 实例。这个 Scope 的生命周期与调用它的 Composable 的生命周期同步。
也就是说,当该 Composable 进入组合时,Scope 被创建;当它离开组合时,Scope 被取消,从而自动取消所有由它启动的协程。
但是你直接这样写的话,依然会有警告:Calls to launch should happen inside a LaunchedEffect and not composition
val coroutineScope = rememberCoroutineScope()
coroutineScope.launch {
// ..
}
这是因为在 Composable 函数直接启动协程,那么每当 Composable 函数发生重组时,导致协程会被重复启动,造成资源浪费甚至逻辑错误等问题。
所以我们可以使用 remember 函数包住协程,像这样:
val coroutineScope = rememberCoroutineScope()
val coroutine = remember {
coroutineScope.launch {
Log.d("Snow","获取远程数据")
}
}
并且我们去看 LaunchedEffect 的源码,它也是类似的做法:
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block) }
}
rememberCoroutineScope 的使用场景:用户交互触发的协程
这么看来,rememberCoroutineScope 和 LaunchedEffect 都能启动一个协程, LaunchedEffect 反而更简单,我就无脑使用 LaunchedEffect?
这显然是不对的。
首先 LaunchedEffect 会在 Composable 首次进入组合或者 key 参数改变时,自动被执行,它主要用于处理那些需要与 Composable 生命周期或其依赖数据变化同步的副作用,例如进入界面时加载初始数据。
而 rememberCoroutineScope() 提供一个 CoroutineScope 实例,允许我们手动调用 launch 来启动协程,与 Composable 的组合/重组流程无关。
因此,rememberCoroutineScope 专用于需要在用户交互(如按钮点击、滑动操作)中启动协程的场景。
比如:
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun DataFetcherButton() {
val coroutineScope = rememberCoroutineScope()
var dataText by remember { mutableStateOf("No data loaded") }
var isLoading by remember { mutableStateOf(false) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(
onClick = {
if (!isLoading) { // 防止重复点击
isLoading = true
dataText = "Loading..."
coroutineScope.launch {
try {
val result = fetchDataFromServer() // 模拟网络请求
dataText = result
} catch (e: Exception) {
dataText = "Error: ${e.message}"
} finally {
isLoading = false
}
}
}
},
enabled = !isLoading // 加载时禁用按钮
) {
Text(if (isLoading) "Loading..." else "Fetch Data")
}
Text(text = dataText, modifier = Modifier.padding(top = 8.dp))
}
}
// 模拟的挂起函数
@RequiresApi(Build.VERSION_CODES.O)
suspend fun fetchDataFromServer(): String {
delay(2000) // 模拟耗时操作
val currentTime = LocalDateTime.now()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val formattedTime = currentTime.format(formatter)
return "Data fetched successfully at $formattedTime"
}
在这个例子中,点击按钮后,我们在 coroutineScope 中启动一个协程来模拟获取数据,并根据加载状态和结果更新UI。
运行结果:
协程的生命周期
最后声明一点,通过 rememberCoroutineScope() 函数获取的 CoroutineScope 实例启动的协程,其生命周期是与调用 rememberCoroutineScope() 的那个 Composable 函数的生命周期绑定的,本质上是与CoroutineScope 实例的生命周期相绑定。
就比如上面的 DataFetcherButton 示例中,如果 DataFetcherButton 因为某些条件离开了组合(离开了UI树,不再显示在界面),那么其内部创建的 CoroutineScope 实例就会被取消,从而所有由这个 CoroutineScope 实例启动的协程,也会被取消,即使仍然在运行。