【Compose】附带作用

【Compose】附带作用

本文介绍了Jetpack Compose里附带作用的使用及特性

Side Effect, 官方有一段时间翻译为副作用,后来终于改为了更为精准的附带作用。

设计目的

官方定义:

附带效应是指发生在可组合函数作用域之外的应用状态的变化。由于可组合项的生命周期和属性(例如不可预测的重组、以不同顺序执行可组合项的重组或可以舍弃的重组),可组合项在理想情况下应该是无附带效应的。不过,有时附带效应是必要的,例如,触发一次性事件(例如显示信息提示控件),或在满足特定状态条件时进入另一个屏幕。这些操作应从能感知可组合项生命周期的受控环境中调用。在本页中,您将了解 Jetpack Compose 提供的不同附带效应 API。

简单点说,就是Composable函数设计之初就已经规定好了,每一次Composable的函数的重组会尽量以最小重组范围来进行。而且,顺序写在一起的Composable函数,他们之间的调用顺序不是确定的。理想情况不需要跳脱到这个规则之外,否则可能会产生未知的数据问题。

但是很多情况,我们用这个设计理念来写代码,会产生很多不优雅的情况,比如超长的变量传递链,不合理不直观的架构设计。

所以就设计了一些附带作用的API,在某些情况下代码块里面的跳脱到重组的范围之外,比如只有初次进入时调用一次,后面都不参与重组;还比如每次退出Composable函数时调用一次。

LauncheEffect

如需在可组合项的 生命周期内执行工作并能够调用挂起函数 ,请使用 LaunchedEffect 可组合项。

当它的内部逻辑要触发时,它会启动一个协程,并将代码块作为参数传递。

如果 LaunchedEffect 退出组合,协程将取消。

源码:

@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1) { LaunchedEffectImpl(applyContext, block) }
}
  • key参数是其监听变化状态的值
  • block就是丢给协程执行的代码块。

当这个外部的key的值发生变化时,协程会被取消,重新启动。并且,当LaunchedEfftect的所属可组合项退出时,协程会被直接取消。

最常用用法是用来触发网络数据获取,在挂起函数执行完毕数据返回来之后,更新界面的状态。

如下面例子所示:

@Composable
fun LaunchedEffectComposable() {
    val isLoading = remember { mutableStateOf(false) }
    val data = remember { mutableStateOf(listOf<String>()) }

    // 定义一个 LaunchedEffect 来异步执行长时间运行的操作,
    // 如果 isLoading.value 发生变化,LaunchedEffect 将取消并重新启动
    LaunchedEffect(isLoading.value) {
        if (isLoading.value) {
            val newData = fetchData()  // 执行长时间运行的操作,例如从网络获取数据
            data.value = newData       // 使用新数据更新状态
            isLoading.value = false
        }
    }

    Column {
        Button(onClick = { isLoading.value = true }) {
            Text("Fetch Data")
        }
        if (isLoading.value) {
            CircularProgressIndicator()  // 显示加载指示器
        } else {
            LazyColumn {
                items(data.value.size) { index ->
                    Text(text = data.value[index])
                }
            }
        }
    }
}

remeberCoroutineScope

由于 LaunchedEffect 是可组合函数,因此只能在其他可组合函数中使用。

remeberCoroutineScope 可以在可组合项外启动协程,但存在作用域限制,以便协程在退出组合后自动取消,请使用 rememberCoroutineScope。 此外,如果您需要手动控制一个或多个协程的生命周期,请使用 rememberCoroutineScope,例如在用户事件发生时取消动画。

rememberCoroutineScope 是一个可组合函数,会返回一个 CoroutineScope,该 CoroutineScope 绑定到调用它的组合点。调用退出组合后,作用域将取消。

@Composable
fun SideEffectPage() {

    val isShowButtton = remember {
        mutableStateOf(true)
    }
    LaunchedEffect(Unit) {
        delay(3000)
        isShowButtton.value = false
    }
    
    if (isShowButtton.value) {
        val scope = rememberCoroutineScope()
        Button(onClick = {

            scope.launch {
                delay(2000)
                Log.i("rememberCoroutineScope", "fdgbfv")
            }
        }) {
            Text(text = "SidsfgvdcsPage ")
        }
    }
}

在isShowButtton作用域代码块里面使用这个附加作用获取一个协程对象,在点击按钮时,启动一个协程,在协程里面执行一些耗时操作。

同时外部的 LaunchedEffect 作用下,3s后,这个协程所属的可组合项会退出生命周期,这个协程里的所有任务也会被取消。

SideEffect

想象这样一个需求,要求用户进入某个界面之后,记录他们的某个操作的埋点数据。

如果直接在Composable方法中执行这个记录工作,那么即使重组失败,数据仍然会更新。需要确保Composable重组成功的情况下,才会记录埋点数据。

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        FirebaseAnalytics()
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

DisposableEffect

DisposableEffect 可组合项会在每次重新组合时调用其效果,并且在每次重新组合时,都会在退出组合时调用其 onDispose 回调。可以在 onDispose 回调中执行清理工作。回收该Composable函数中使用的所有资源。

看这样一个场景,在isShowButton.value为true时,在作用域内启动一个协程任务,在3s后更新Text的数据。

注意直接在Composable函数中启动协程任务,会导致协程任务的生命周期和Composable函数的生命周期不一致,导致协程任务无法被正确的回收。这里仅作演示。

@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun SideEffectPage() {
    val isShowButton = remember { mutableStateOf(true) }
    
    LaunchedEffect (Unit){
        delay(1000)
        isShowButton.value = false
    }

    if(isShowButton.value) {
        val text = remember { mutableStateOf("Hello") }
        val scope = CoroutineScope(Dispatchers.Main)
        val job = scope.launch {
            delay(3000)
            Log.d("SideEffectPage", "SideEffectPage: ")
            text.value = "Hello World"
        }
        Text(text = text.value)
    }
}

1s后尽管已经隐藏了Button,退出了可组合项的生命周期,但是协程任务依然在运行,导致内存泄漏。

这时候我们可以使用DisposableEffect,在onDispose回调中,取消协程任务。

更改后如下:

@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun SideEffectPage() {
    val isShowButton = remember { mutableStateOf(true) }
//
    LaunchedEffect (Unit){
        delay(1000)
        isShowButton.value = false
    }

    if(isShowButton.value) {
        val text = remember { mutableStateOf("Hello") }
        DisposableEffect(Unit) {
            val scope = CoroutineScope(Dispatchers.Main)
            val job = scope.launch {
                delay(3000)
                Log.d("SideEffectPage", "SideEffectPage: ")
                text.value = "Hello World"
            }
            onDispose {
                job.cancel()
            }
        }

        Text(text = text.value)
    }
}

rememberUpdateState

如果我们使用LaunchedEffect时要引用一个值,但是如果该值发生更改,则不应重新启动,请使用 rememberUpdatedState。

当关键参数的值之一更新时, LaunchedEffect 会重新启动,但有时我们希望在不重新启动的情况下捕获效果中更改的值。

如果我们有长时间运行的选项,重新启动成本很高,则此过程会很有帮助。

@Composable
fun SideEffectPage() {

    val netText = remember { mutableStateOf("") }

    LaunchedEffect(Unit) {
        delay(3000L)
        netText.value = "got a new String from network"
    }

    UpdateText(netText.value)
}

@Composable
fun UpdateText(test: String) {
    val uiText = remember { mutableStateOf("initial local text") }

    val updateText = rememberUpdatedState(test)

    LaunchedEffect(Unit) {
        delay(5000L)
        uiText.value = updateText.value
    }
    
    Box(
        modifier = Modifier.fillMaxSize(1f),
        contentAlignment = Alignment.Center
    ) {
        Text(text = uiText.value)
    }
}

例如在5s后更新一次Text的数据,但是在3s后,网络请求已经返回了新的数据,这个时候我们就可以使用 rememberUpdateState 先将这个更新的数据存储起来。5s后就可以使用更新的正确的数据。

另外还有几个附带作用的函数,我日常使用的时候几乎没有使用过,暂时先不做记录。