浅谈Jetpack Compose LaunchedEffect

在Jetpack Compose 代码中,我们经常看到LaunchedEffect这个常见的。今天就简单了解下这个方法吧。在说LaunchedEffect前,让我们先说一下Side Effect.

什么是Side Effect

在编程语言中,Side-Effect指的是调用函数时,除了返回可能的函数值外,该函数还对函数范围外的变量,参数等进行了修改。
举个🌰,看下面的函数,它有2个参数,返回它们的和,但是没有对任何其他变量有修改,所以它是没有副作用(No Side-Effect)的

fun sum(number1: Int, number2: Int) = number1 + number2

但如果我们将函数修改成下面的实现,每次调用该函数都会对sum重新赋值,而sum又是在函数范围外,所以认为这个函数有一个副作用

var sum = 0
fun sum(number1: Int, number2: Int) {
    sum = number1 + number2
}

在Jetpack Compose 中,什么是LaunchedEffect

根据源码我们看到,LaunchedEffect是一个带有@Composable的函数,注释如下

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 a different key1 or key2. The coroutine will be cancelled when the LaunchedEffect leaves the composition.
This function should not be used to (re-)launch ongoing tasks in response to callback events by way of storing callback data in MutableState passed to key. Instead, see rememberCoroutineScope to obtain a CoroutineScope that may be used to launch ongoing jobs scoped to the composition in response to event callbacks.

简单来说,LaunchedEffect是一个可在当前可组合项的作用域内运行挂起函数(block是suspend的)的可组合函数。
Thinking in Compose中,我们了解到一个可组合项应该是没有副作用的,如果我们想要在可组合项中修改应用状态,需要通过Effect API启动协程操作,所以可以说LaunchedEffect在Jetpack Compose中提供了在可组合项中调用挂起函数的能力。

下面我们通过一个例子简单了解下, 这个例子是一个TodoTask下的一个小功能,根据选择的类别展示当前类别的所有Task。
简单的准备mock tasks,代码如下:

data class TodoTask(
    val id: Long,
    val name: String,
    val description: String
)

// mock tasks respose
val mockTasks = (1..10).map {
    TodoTask(
        id = it.toLong(),
        name = "task name $it",
        description = "task description $it"
    )
}
// mock get tasks from api, delay 2 seconds
private suspend fun getTasks(): List<TodoTask> {
    delay(2000)
    return mockTasks
}

在TodoListOfCategoryScreen主要是一个Lazy Column用来展示任务名称的,其中参数是Category

@Composable
fun TodoListOfCategoryScreen(
    category: String
) {
    println("TASK TAG: out of column scope")
    var tasks by remember {
        mutableStateOf(emptyList<TodoTask>())
    }
    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        println("TASK TAG: TodoListScreen: ${tasks.size}, category: $category")

        LaunchedEffect(key1 = category) {
            tasks = getTasks()
            println("TASK TAG: get tasks from api, category: $category")
        }
        LazyColumn(
            modifier = Modifier.fillMaxSize()
        ) {
            items(tasks, key = {
                it.id
            }) {
                Text(modifier = Modifier.fillMaxWidth(), text = it.name)
            }
        }
    }
}

首先我们的category是固定的,在getTasks之后,修改了tasks导致Column发生重组,所以scope相关的日志打印了2次。而模拟网络请求的LaunchedEffect内只调用了一次,是因为LaunchedEffect的调用时期只有进入重组项或者Key变化重组,如果是普通的启动协程,在重组时,会导致多次调用api请求。
Logcat

PS: 这里为什么out of column scope也会打印2次呢,其实是因为Column是inline的, 它只能共享调用方的重组范围。

总结

  • LaunchedEffect是在Jetpack Compose中提供了在可组合项中调用挂起函数的能力的一种方式。
  • 在进入组合项时,LaunchedEffect会启动一个协程并执行挂起代码块(block),在退出组合项时,协程将取消
  • 使用不同的key(参数key1, key2)重组时,会取消当前协程并启动新的协程执行挂起函数
  • LaunchedEffect函数时,最少要一个key(IDE 会有提示)
  • LaunchedEffect的函数调度器是主线程