Jetpack Compose系列:LaunchedEffect与rememberCoroutineScope的使用

Jetpack Compose是Google基于Kotlin语言编写的一套UI框架,协程(Coroutines)的使用贯穿了整个Jetpack Compose,如动画的启停、数据的存取(数据库SQLite+Room、SharedPreferences的替代者DataStore)、网络请求等,都需要在协程中进行。可以说,熟练掌握协程是入门Jetpack Compose的基本要求。

一、协程作用域

”众所周知“,协程需要在作用域中启动。

什么是作用域

协程作用域(Coroutine Scope)是协程运行的作用范围。launch、async都是CoroutineScope的扩展函数,CoroutineScope定义了新启动的协程作用范围,同时会继承了他的coroutineContext自动传播其所有的 elements和取消操作。换句话说,如果这个作用域销毁了,那么里面的协程也随之失效,就好比变量的作用域。

协程的作用域大致分为三种:

GlobalScope:即全局协程作用域,在这个范围内启动的协程可以一直运行直到应用停止运行。GlobalScope 本身不会阻塞当前线程,且启动的协程相当于守护线程,不会阻止 JVM 结束运行。

runBlocking:一个顶层函数,和 GlobalScope 不一样,它会阻塞当前线程直到其内部所有相同作用域的协程执行结束。

自定义 CoroutineScope:可用于实现主动控制协程的生命周期范围,对于 Android 开发来说最大意义之一就是可以在 Activity、Fragment、ViewModel 等具有生命周期的对象中按需取消所有协程任务,从而确保生命周期安全,避免内存泄露。

在Jetpack Compose中使用最多的协程作用域就是LaunchedEffect和rememberCoroutineScope,严格来说LaunchedEffect中已经包含了launch函数,直接传入要在协程中执行的代码即可,而通过rememberCoroutineScope获取的作用域还要自行调用launch、async函数去启动一个协程。

LaunchedEffect(key1 = Unit) {
	//需要在协程中执行的代码
}

rememberCoroutineScope().launch {
	//需要在协程中执行的代码
}

这两个都是Composable函数,需要在Composable函数中调用。换个说法就是,需要在ComponentActivity的setContent函数中才能调用。

override fun onCreate(savedInstanceState: Bundle?) {
	super.onCreate(savedInstanceState)
	setContent {
		LaunchedEffect(key1 = Unit) {
			//需要在协程中执行的代码
		}
	
		rememberCoroutineScope().launch {
			//需要在协程中执行的代码
		}
	}
}

但是当你直接在Composable函数中调用rememberCoroutineScope().launch{},你会发现Android Studio给出了警告信息:

Calls to launch should happen inside a LaunchedEffect and not composition

应该通过LaunchedEffect去调用launch函数

又”众所周知“,Jetpack Compose中有”重组“这一概念,当Composable函数(组件)所依赖的mutableStateOf数据发生变化时,Composable函数就会发生重组,重组就会重新调用Composable函数。

如果在Composable函数中直接使用rememberCoroutineScope().launch{},那么当Composable函数发生重组时,就会重复创建协程并执行代码,会造成重复请求接口、重复执行某些操作导致结果异常等。而且协程的大量创建对于系统资源的消耗也是一种浪费。

二、LaunchedEffect与rememberCoroutineScope的正确使用方式

因此官方推荐的正确做法是,在Composable函数通过LaunchedEffect函数去创建使用协程。来看下LaunchedEffect函数的定义:

//重载形式1
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1, key2, key3) { LaunchedEffectImpl(applyContext, block) }
}

//重载形式2
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
    key1: Any?,
    key2: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1, key2, key3) { LaunchedEffectImpl(applyContext, block) }
}

//重载形式3
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
    key1: Any?,
    key2: Any?,
    key3: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1, key2, key3) { LaunchedEffectImpl(applyContext, block) }
}

//重载形式4
@Composable
@NonRestartableComposable
@Suppress("ArrayReturn")
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
    vararg keys: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(*keys) { LaunchedEffectImpl(applyContext, block) }
}

LaunchedEffect函数有多种重载形式,但仔细一看无非就是key的数量不一样。

When LaunchedEffect enters the composition it will launch block into the composition's CoroutineContext. The coroutine will be cancelled and re-launched when LaunchedEffect is recomposed with any different keys. The coroutine will be cancelled when the LaunchedEffect leaves the composition.

简要概括一下就是当key值发生变化时,协程会被取消并重新执行

举个例子,当列表的页码发生变化时,你需要请求接口去获取该页的数据,那么通过LaunchedEffect就可以这么写:

LaunchedEffect(key1 = viewModel.page) {
	requestData(viewModel.page, viewModel.size)
}

那么rememberCoroutineScope().launch{}到底是什么时候用呢?那当然就是在非Composable函数中使用了。比如说,ModalBottomSheetState的hide函数、show函数,这两个函数都是suspend挂起函数,必须在协程中调用。

假设有一个按钮,点击后隐藏底部弹窗ModalBottomSheet,那么代码就可以这么写:

val modalBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)

Modifier.clickable(
    onClick = {
        coroutineScope.launch {
            modalBottomSheetState.hide()
        }
    }
)

注意onClick函数不是Composable函数,是没法使用LaunchedEffect函数的。

三、总结

Composable函数中通过LaunchedEffect函数创建协程,并尽量传入key值以防重复创建协程,非Composable函数通过rememberCoroutineScope().launch{}创建协程。

 

参考:

编辑于 2024-12-05 19:35
目录