1.前言
学习Compose,Side-effect是其中必然要了解的重点和难点。正常情况下,composable函数不应该包含Side-effect(副作用),但是当composable函数需要修改app的状态时,这个composable函数需要从一个受控制的环境中被调用,此环境需要感知到本composable函数的生命周期,此时就需要使用Side-effect。谷歌推荐使用其提供的Effect APIs,这样这些Side-effect能以可预料的方式执行。本篇文章将简单介绍Compose中的几个常用Side-effect,并举例说明其使用场景。
2.LaunchedEffect
有时候composable函数中需要使用耗时函数,此时需要将其放入coroutine中,而coroutine需要在CoroutineScope中创建,因此谷歌特意提供了LaunchedEffect用于创建coroutine。LaunchedEffect有以下特点:
-
当LaunchedEffect进入Composition时,会启动一个coroutine,并将LaunchedEffect后括号中的代码放入该coroutine中执行;假如LaunchedEffect离开Composition时该coroutine还未被执行完毕,该coroutine会被取消。
-
如果LaunchedEffect在recompose时其key不变,那LaunchedEffect不会被重新启动;如果其key发生了变化,则LaunchedEffect会被重新启动。
以下是使用LaunchedEffect的一个示例,该LaunchedEffect用于启动一个Snackbar并进行等待,然后在点击Snackbar上的按钮时对传入的文案进行处理并显示。代码如下:
@Composable
fun LaunchedEffectWrapper1(state: SnackbarHostState, text: String, onResult: (String) -> Unit) {
val mostRecentText by rememberUpdatedState(text)
LaunchedEffect(Unit) {
val snackbarResult = state.showSnackbar(
message = "Are you happy with your input?",
actionLabel = "Yes",
duration = SnackbarDuration.Indefinite,
)
// Do something with text parameter
when (snackbarResult) {
SnackbarResult.ActionPerformed -> {
onResult("final text:$mostRecentText")
}
else -> {}
}
}
}
LaunchedEffect常用于将一些只需要在Composition中执行一次的操作抽离出来,防止其在recompose时被反复执行,例如注册事件的监听等。
3.rememberCoroutineScope
由于LaunchedEffect是composable函数,它只能在其他composable函数中被调用。因此想从非composable函数中创建coroutine时需要另寻他法。谷歌提供了rememberCoroutineScope用于在非composable函数中创建coroutine。rememberCoroutineScope特点如下:
-
rememberCoroutineScope可以返回一个coroutineScope,便于开发者手动控制该coroutine的生命周期,例如有用户点击事件时启动该coroutine。
-
rememberCoroutineScope返回的coroutineScope会和其调用点的生命周期保持一致,当调用点所在的Composition退出时,该coroutineScope会被取消。
以下是使用rememberCoroutineScope的一个示例,当用户点击按钮时,程序会显示Snackbar。
@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {
// Creates a CoroutineScope bound to the MoviesScreen's lifecycle
val scope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
Column {
/* ... */
Button(
onClick = {
// Create a new coroutine in the event handler
// to show a snackbar
scope.launch {
scaffoldState.snackbarHostState
.showSnackbar("Something happened!")
}
}
) {
Text("Press me")
}
}
}
}
rememberCoroutineScope常用于开发者需要在回调事件中需要控制coroutine的场景。
4.rememberUpdatedState
上文提到过,如果key值没有更新,那LaunchedEffect在recompose时不会被重新启动。但是有时候,你需要在LaunchedEffect中使用最新的参数值,但是又不想重新启动LaunchedEffect,因为LaunchedEffect中包含了重量级的操作,重新启动会浪费资源,此时就需要用到rememberUpdatedState。rememberUpdatedState的作用是给某个参数创建一个引用,并保证其值被使用时是最新值。
我们来看以下示例,示例中有一个文字输入框以及一个Snackbar,当点击Snackbar中的yes按钮时,需要对文字输入框中的内容进行处理,然后显示在文字输入框下方。其界面如下:
对应的程序如下:
@Composable
fun SnackbarSample() {
val snackbarHostState = remember { SnackbarHostState() }
var text by remember { mutableStateOf("") }
var descrption by remember { mutableStateOf("") }
LaunchedEffectWrapper1 (snackbarHostState, text) { descrption = it }
// .. code for handling the change of the text state
Scaffold(
// attach snackbar host state to the scaffold
scaffoldState = rememberScaffoldState(snackbarHostState = snackbarHostState),
content = {
Column(
content = {
OutlinedTextField(
value = text,
onValueChange = {
text = it
},
label = { Text("Input") }
)
Text(descrption)
}
)
}
)
}
@Composable
fun LaunchedEffectWrapper1(state: SnackbarHostState, text: String, onResult: (String) -> Unit) {
// val mostRecentText by rememberUpdatedState(text)
LaunchedEffect(Unit) {
val snackbarResult = state.showSnackbar(
message = "Are you happy with your input?",
actionLabel = "Yes",
duration = SnackbarDuration.Indefinite,
)
// Do something with text parameter
when (snackbarResult) {
SnackbarResult.ActionPerformed -> {
onResult("final text:$text")
}
else -> {}
}
}
}
此时我们发现,无论输入框中输入什么内容,输入框下面显示的文案都是“final text:”。原因是LaunchedEffectWrapper在recompose时不会重启其中的LaunchedEffect,读取的text参数的值是第一次传入的值,即空字符串,因此最终结果为“final text:”,这显然不是我们预期的结果。
怎样获取最新的text的值呢?一种方式是将text作为LaunchedEffect的key,这样每次text有变动LaunchedEffect都会重启,自然会获取text的最新值。但是LaunchedEffect频繁重启违背了LaunchedEffect在非必要时不重启的原则,造成了资源的浪费,同时每次重启都会显示Snackbar,会导致Snackbar不停的闪烁。
另一种方式是使用rememberUpdatedState存储参数text的值。LaunchedEffectWrapper函数可改为如下形式。
@Composable
fun LaunchedEffectWrapper1(state: SnackbarHostState, text: String, onResult: (String) -> Unit) {
val mostRecentText by rememberUpdatedState(text)
LaunchedEffect(Unit) {
val snackbarResult = state.showSnackbar(
message = "Are you happy with your input?",
actionLabel = "Yes",
duration = SnackbarDuration.Indefinite,
)
// Do something with text parameter
when (snackbarResult) {
SnackbarResult.ActionPerformed -> {
onResult("final text:$mostRecentText")
}
else -> {}
}
}
}
此时在OutlinedTextField输入“123”,其下的描述文案则变为“final text:123”。原因是rememberUpdatedState可以使该参数的引用被读取时一直使用最新的值。
5.小结
本文主要介绍了LauchedEffect,rememberCoroutineScope,rememberUpdatedState三种Side-effect的作用和使用场景。其重点如下:
-
LauchedEffect用于在composable函数中创建coroutine,执行耗时函数。
-
rememberCoroutineScope用于在非composable函数中启动coroutine,便于开发者自己控制coroutine的生命周期。
-
rememberUpdatedState用于保持参数的值在使用时是最新值。
6.参考文档
Side-effects in Compose
Tricky refactoring of Jetpack Compose code — be careful with side effects