Compose基础-Side-effect(一)

3,590 阅读3分钟

1.前言

学习ComposeSide-effect是其中必然要了解的重点和难点。正常情况下,composable函数不应该包含Side-effect(副作用),但是当composable函数需要修改app的状态时,这个composable函数需要从一个受控制的环境中被调用,此环境需要感知到本composable函数的生命周期,此时就需要使用Side-effect。谷歌推荐使用其提供的Effect APIs,这样这些Side-effect能以可预料的方式执行。本篇文章将简单介绍Compose中的几个常用Side-effect,并举例说明其使用场景。

2.LaunchedEffect

有时候composable函数中需要使用耗时函数,此时需要将其放入coroutine中,而coroutine需要在CoroutineScope中创建,因此谷歌特意提供了LaunchedEffect用于创建coroutineLaunchedEffect有以下特点:

  • LaunchedEffect进入Composition时,会启动一个coroutine,并将LaunchedEffect后括号中的代码放入该coroutine中执行;假如LaunchedEffect离开Composition时该coroutine还未被执行完毕,该coroutine会被取消。

  • 如果LaunchedEffectrecompose时其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

由于LaunchedEffectcomposable函数,它只能在其他composable函数中被调用。因此想从非composable函数中创建coroutine时需要另寻他法。谷歌提供了rememberCoroutineScope用于在非composable函数中创建coroutinerememberCoroutineScope特点如下:

  • 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值没有更新,那LaunchedEffectrecompose时不会被重新启动。但是有时候,你需要在LaunchedEffect中使用最新的参数值,但是又不想重新启动LaunchedEffect,因为LaunchedEffect中包含了重量级的操作,重新启动会浪费资源,此时就需要用到rememberUpdatedStaterememberUpdatedState的作用是给某个参数创建一个引用,并保证其值被使用时是最新值。

我们来看以下示例,示例中有一个文字输入框以及一个Snackbar,当点击Snackbar中的yes按钮时,需要对文字输入框中的内容进行处理,然后显示在文字输入框下方。其界面如下:

image.png

对应的程序如下:

@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:”。原因是LaunchedEffectWrapperrecompose时不会重启其中的LaunchedEffect,读取的text参数的值是第一次传入的值,即空字符串,因此最终结果为“final text:”,这显然不是我们预期的结果。

怎样获取最新的text的值呢?一种方式是将text作为LaunchedEffectkey,这样每次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.小结

本文主要介绍了LauchedEffectrememberCoroutineScoperememberUpdatedState三种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