一文彻底吃透 Compose 中的副作用(附带效应)

Compose 官方:副作用(附带效应)是指发生在可组合函数作用域之外的应用状态的变化。


为什么不能有副作用?

首先需要明白 Compose 中关于重组(Recompose)的一个关键特点:可组合函数可以按任何顺序执行

这是一个官方的示例代码,用于在标签页布局中绘制三个页面:

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}

按照传统的应用开发思维,这种代码结构意味着三个页面的绘制是按其出现的顺序依次运行的。但其实不是,如果某个可组合函数包含对其他可组合函数的调用,这些函数可以按任何顺序运行。Compose 可以选择识别出某些界面元素的优先级高于其他界面元素,因而首先绘制这些元素。

这就意味着对 StartScreenMiddleScreenEndScreen 的调用可以按任何顺序进行的。

那么看下面的操作场景:

@Composable
fun ButtonRow() {
    MyFancyNavigation {
    	// 1. 这里定义了一个全局变量
        StartScreen()  // 3. 内部修改了这个全局变量
        MiddleScreen() // 2. 内部要使用这个变量
        EndScreen()
    }
}

前面我们说了,三个可组合函数的调用顺序是不定的,如果按照 1 -> 3 -> 2 的顺序调用,那么功能就会出错,这种行为就是所谓的:发生在可组合函数作用域之外的应用状态的变化,也就是:副作用(附带效应)

所以 Compose 官方建议:可组合项在理想情况下应该是无副作用的!

不过你也注意到了:是理想情况下不应该有副作用,但有时副作用又是必要的,它很有用!



为什么要使用副作用?

Compose 中副作用的目的:允许执行与 UI 无关的操作,这些操作以可控且可预测的方式更改可组合函数之外的应用状态。

副作用(如更新数据库或进行网络调用)应与 UI 呈现逻辑分开,以提高代码的性能和可维护性。

Compose 提供了多个可组合函数,例如 SideEffectLaunchedEffectDisposableEffect,这些函数使开发人员能够有效地管理副作用,方法是将它们与界面渲染逻辑分离并在单独的协程范围内执行它们。

在 Compose 中使用副作用的主要好处是:

  1. 改进的性能: 通过在可组合函数之外执行与 UI 无关的操作,UI 呈现逻辑可以保持响应和性能。
  2. 更好的代码组织: 通过将非 UI 相关操作与 UI 呈现逻辑分离,代码库变得更易于理解和维护。
  3. 更好的调试: 副作用可用于日志记录和分析操作(比如埋点),这可以帮助我们更好地了解应用的行为并识别问题。

总之,Compose 中副作用的目的是通过将非 UI 相关操作与 UI 渲染逻辑分离来提高代码库的性能、可维护性和调试。



副作用

📓 SideEffect

SideEffect 主要用于在不影响 UI 性能的情况下执行副作用。

SideEffect 应该算是最简单的副作用函数了。我们需要在 Composable 函数中调用它,并传入一个包含我们想要执行的副作用的 lambda。

比如看下面这个代码:

@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }  // 定义一个用于计数的状态变量

    SideEffect {                                // 使用 SideEffect 记录 count 的当前值
        println("Count is ${count.value}")      // 每次重组时会调用
    }

    Column {
        Button(onClick = { count.value++ }) {
            Text("Increase Count")
        }
        Text("Counter ${count.value}")          // 每次状态更新时,文本都会更改并触发重组
    }
}

在此示例中,每当重构 Counter 函数时,SideEffect 函数都会记录 count 状态变量的当前值。

在这里插入图片描述

但有一点需要注意,仅当当前可组合函数被重构时,才会触发副作用,而对于任何嵌套的可组合函数,则不会触发。这意味着,如果有一个 Composable 函数调用另一个 Composable 函数,则在重构内部 Composable 函数时,不会触发外部 Composable 函数中的 SideEffect。

什么意思?比如,我把代码改成这样:

@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }  // 定义一个用于计数的状态变量

    SideEffect {                                // 使用 SideEffect 记录 count 的当前值
        println("Count is ${count.value}")      // 每次重组时会调用
    }

    Column {
        Button(onClick = { count.value++ }) {
            Text("Increase Count ${count.value}")  // 每次点击按钮时,这种重组不会触发外部副作用
        }
    }
}

在上面的代码中,单击 Button 时,Text 可组合项将使用新值 count 重新组合,但这不会再次触发 SideEffect。

在这里插入图片描述

现在,让我们添加内部副作用,看看它是如何工作的:

@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }  // 定义一个用于计数的状态变量

    SideEffect {                                // 使用 SideEffect 记录 count 的当前值
        println("Count is ${count.value}")      // 每次重组时会调用
    }

    Column {
        Button(onClick = { count.value++ }) {
            SideEffect {
                println("@@@ Count is ${count.value}")  // 每次重组时会调用
            }
            Text("Increase Count ${count.value}")       // 每次点击按钮时,这种重组不会触发外部副作用
        }
    }
}

再看下运行效果:

在这里插入图片描述

这个时候 count 变化就会引发重组,从而 SideEffect 会被调用。一般埋点的需求场景,就很适合用这种方式,可以统计页面或者组件的曝光。


📓 DisposableEffect

DisposableEffect 可以理解为 SideEffect 的升级款,它不仅可以实现 SideEffect 的效果,而且内部还提供了一个 onDispose() 方法,用于当某可组合项从 UI 页面消失时做一些释放资源的操作。

比如:DisposableEffect 函数可用于管理不再使用可组合项时需要清理的资源,例如事件侦听器或动画。

下面是如何使用 DisposableEffect 的示例:

@Composable
fun TimerScreen() {
    val elapsedTime = remember { mutableStateOf(0) }

    DisposableEffect(Unit) {
        val scope = CoroutineScope(Dispatchers.Default)
        val job = scope.launch {
            while (true) {
                delay(1000)
                elapsedTime.value += 1
                println("@@@ Timer is still working ${elapsedTime.value}")
            }
        }

        onDispose {
            job.cancel()
        }
    }

    Text(
        text = "Elapsed Time: ${elapsedTime.value}",
        modifier = Modifier.padding(16.dp),
        fontSize = 24.sp
    )
}

在此代码中,我们使用 DisposableEffect 启动一个协程,该协程每秒递增 elapsedTime 状态值。我们还使用 DisposableEffect 来确保在不再使用可组合项时取消协程,并清理协程使用的资源。

在 DisposableEffect 的 onDispose 函数中,我们使用存储在 job 中的 Job 实例的 cancel() 方法取消协程。

当 Composable 从 UI 层次结构中删除时,将调用 onDispose 函数,它提供了一种清理 Composable 使用的任何资源的方法。在这种情况下,我们使用 onDispose 来取消协程,并确保清理协程使用的任何资源。

现在重新修改代码,添加 Text() 组件显示与否的逻辑,让我们运行以下代码来查看结果:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            RunTimerScreen()
        }
    }
}

@Composable
fun RunTimerScreen() {
    val isVisible = remember { mutableStateOf(true) }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Bottom
    ) {
        Spacer(modifier = Modifier.height(10.dp))

        if (isVisible.value)
            TimerScreen()

        Button(onClick = { isVisible.value = !isVisible.value }) {
            Text("Hide the timer")
        }
    }
}

上面代码中,添加了一个新的 RunTimerScreen 可组合项,允许用户切换 TimerScreen 的可见性。当用户单击“Hide the timer”按钮时,TimerScreen 可组合项将从 UI 层次结构中删除,协程将被取消并清理。

在这里插入图片描述

但是要注意: 如果你在 onDispose 函数中没有添加 job.cancel(),即使 TimerScreen 可组合项消失,协程也会继续运行,这就可能会导致泄漏和其他性能问题。


📓 LaunchedEffect

LaunchedEffect 是一个 Composable 函数,用于在单独的协程作用域中执行副作用。此函数可用于执行可能需要很长时间的操作(例如网络调用或动画),而不会阻塞 UI 线程。

它需要两个参数 keycoroutineScope 块

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            LaunchedEffect(key1 = , block = )
        }
    }
}
fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
)
  1. 在 key 参数中,你可以传递任何状态,因为它是 Any 类型。
  2. 在 coroutineScope 块中,您可以传递任何挂起或非挂起的函数。

LaunchEffect 将始终在可组合函数中只运行一次。如果要再次运行 LaunchEffect,则必须在 key 参数中传递随时间变化的任何状态(mutableStateOf ,StateFlow)。

下面是如何使用 LaunchedEffect 的示例:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            LaunchedEffectComposable()
        }
    }
}

@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])
                }
            }
        }
    }
}

// 通过暂停协程 3 秒来模拟网络调用
private suspend fun fetchData(): List<String> {
    // Simulate a network delay
    delay(3000)
    return listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5",)
}

在此示例中,当 isLoading 状态变量设置为 true 时,LaunchedEffect 函数执行网络调用以从 API 获取数据。该函数在单独的协程作用域中执行,允许 UI 在执行操作时保持响应。

LaunchedEffect 函数采用两个参数:key(设置为 isLoading.value)和 block(定义要执行的副作用的 lambda)。在本例中,block lambda 调用 fetchData() 函数,该函数通过暂停协程 3 秒钟来模拟网络调用。获取数据后,它会更新 data 状态变量并将 isLoading 设置为 false,从而隐藏加载指示符并显示获取的数据。

在这里插入图片描述

LaunchedEffect 参数背后的逻辑:

LaunchedEffect 中的 key 参数用于标识 LaunchedEffect 实例,并防止其被不必要地重构。

重构可组合项时,Jetpack Compose 会确定是否需要重绘该项。如果可组合项的状态或属性已更改,或者可组合项调用invalidate,则 Jetpack Compose 将重新绘制可组合项。重绘可组合项可能是一项成本高昂的操作,特别是如果可组合项包含长时间运行的操作或不需要在每次重构可组合项时重新执行的副作用。

通过向 LaunchedEffect 提供 key 参数,我们可以指定一个唯一标识 LaunchedEffect 实例的值。如果 key 参数的值发生变化,Jetpack Compose 会将 LaunchedEffect 实例视为新实例,并再次执行副作用。如果 key 参数的值保持不变,Jetpack Compose 将跳过副作用的执行,并重复使用之前的结果,从而防止不必要的重组。


📓 rememberCoroutineScope

rememberCoroutineScope 是 Compose 中的一个可组合函数,它将创建一个与当前组合关联的协程范围,我们可以在其中调用任何挂起函数。

  1. 此协程作用域可用于启动新的协程,当组合(可组合函数)不再处于活动状态时,这些协程会自动取消。
  2. rememberCoroutineScope() 创建的 CoroutineScope 对象是每个组合的单例。这意味着,如果在同一组合中多次调用该函数,它将返回相同的协程作用域对象。

看如下代码示例:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MyComponent()
        }
    }
}
@Composable
fun MyComponent() {
    val coroutineScope = rememberCoroutineScope()
    val data = remember { mutableStateOf("") }

    Column {
        Button(onClick = {
            coroutineScope.launch {
                // Simulate network call
                delay(2000)
                data.value = "Data loaded"
            }
        }) {
            Text("Load data")
        }

        Text(text = data.value)
    }
}

此处,rememberCoroutineScope 用于创建与 Composable 函数的生命周期绑定的协程范围。这样一来,你就可以高效、安全地管理协程,确保可组合函数消失时取消协程。您可以在范围内使用 launch功能,轻松安全地管理异步操作。

在这里插入图片描述



副作用状态

📓 rememberUpdateState

如果要引用一个值,如果该值发生更改,则不应重新启动,请使用 rememberUpdatedState。当关键参数的值之一更新时,LaunchedEffect 会重新启动,但有时我们希望在不重新启动的情况下捕获效果中更改的值。如果我们有长时间运行的选项,重新启动成本很高,则此过程会很有帮助。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            var dynamicData by remember { mutableStateOf("") }
            LaunchedEffect(Unit) {
                delay(3000L)
                dynamicData = "New Compose Text"
            }
            MyComponent(title = dynamicData)
        }
    }
}

@Composable
fun MyComponent(title: String) {
    var data by remember { mutableStateOf("Hi, Compose") }

    val updatedData by rememberUpdatedState(title)

    LaunchedEffect(Unit) {
        delay(5000L)
        data = updatedData
    }

    Text(text = data)
}

最初,title 是一个 “Hi, Compose”。3 秒后,title 变为“New Compose Text”。5 秒后,data也会变为“New Compose Text”,从而触发 UI 的重构。这将更新 Text 可组合项。因此,总延迟为 5 秒,如果我们没有使用 rememberUpdatedState,那么我们必须重新启动第二个 LaunchedEffect,这将需要 8 秒。

📓 produceState

produceState 会启动一个协程,该协程将作用域限定为可将值推送到返回的 State 的组合。使用此协程将非 Compose 状态转换为 Compose 状态,例如将外部订阅驱动的状态(如 Flow、LiveData 或 RxJava)引入组合。

以下是一个官方示例:展示了如何使用 produceState 从网络加载图像。

@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository
): State<Result<Image>> {

    // Creates a State<T> with Result.Loading as initial value
    // If either `url` or `imageRepository` changes, the running producer
    // will cancel and will be re-launched with the new inputs.
    return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {

        // In a coroutine, can make suspend calls
        val image = imageRepository.load(url)

        // Update State with either an Error or Success result.
        // This will trigger a recomposition where this State is read
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}

loadNetworkImage 可组合函数会返回可以在其他可组合项中使用的 State。

📓 snapshotFlow

snapshotFlow 主要用于装 Compose 的 State 转换成协程 Flow。

使用 snapshotFlow 可以将 Compose 的 State 对象转换为冷 Flow(冷流)。snapshotFlow 会在收集到块时运行该块,并发出从块中读取的 State 对象的结果。当在 snapshotFlow 块中读取的 State 对象之一发生变化时,如果新值与之前发出的值不相等,Flow 会向其收集器发出新值。

这段解释来自官网,如果比较难以理解,我们拆分下核心思想:

  1. snapshotFlow 可以把 Compose 的 State 状态对象转成协程的 Flow
  2. Flow 可以同步 State 状态,获取到最新的值
  3. 当 State 变化,Flow 也会向收集器发送新值
  4. snapshotFlow 不仅仅只能读取一个 Compose State 对象,可以是多个,只要一个变化即可。

如果还是很难懂,看完下面的代码,你就会明白。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ComposeBlogTheme {
                var name by remember { mutableStateOf("Hi, Compose") }
                val flow = snapshotFlow { name }
                LaunchedEffect(Unit) {
                    flow.collect {
                        println("@@@: $it")
                    }
                }
            }
        }
    }
}

让我来解释一下这段代码:

  1. 首先创建了一个 Compose 的状态对象:name
  2. 用 snapshotFlow 函数把 name 包起来,此时就会得到一个 Flow 对象。(满足核心思想 1)
  3. 在协程里面你就可以通过 flow.collect 获取到 “Hi, Compose”。(满足核心思想 2)

在这里插入图片描述

现在我来改下这段代码:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ComposeBlogTheme {
                var name by remember { mutableStateOf("Hi, Compose") }
                Button(onClick = {
                    name = "Hi, Kotlin"
                }) {
                    Text("修改 name,测试流值是否变化")
                }
                val flow = snapshotFlow { name }
                LaunchedEffect(Unit) {
                    flow.collect {
                        println("@@@: $it")
                    }
                }
            }
        }
    }
}

代码中仅仅就添加一个 Button,修改 Compose 的 State 状态值,从 “Hi, Compose” 改成 “Hi, Kotlin”。

在这里插入图片描述

随着 Compose 的 State 对象 name 的值改变,Flow 也会同步收到最新的值。(符合核心思想 3)

现在,我们再改下代码:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ComposeBlogTheme {
                var name by remember { mutableStateOf("Hi, Compose") }
                var age by remember { mutableStateOf(20) }
                Button(onClick = {
                    age = 26
                }) {
                    Text("修改 name,测试流值是否变化")
                }
                val flow = snapshotFlow { "$name, $age" }
                LaunchedEffect(Unit) {
                    flow.collect {
                        println("@@@: $it")
                    }
                }
            }
        }
    }
}

我新增了一个 Compose 的 State 对象:age,这个时候,snapshotFlow 就包了两个 State 对象,再测试一下效果:

在这里插入图片描述

点击 Button 后修改了 age 状态的值,Flow 也会收到新值,触发 Log 输出。(符合核心思想 4)

另外,这里的代码示例太简单了,在实际开发中,如果你的需求是需要在协程中获取到 Compose 的状态,然后针对这些最新的状态值做一些后续操作,那么就可以考虑 snapshotFlow。

  • 9
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Compose副作用是指在Composable的生命周期执行的操作,例如内存缓存、数据库操作、网络请求、文件读取、日志处理、页面跳转等。Compose提供了一些API来处理这些副作用,以确保它们在Composable的特定阶段被执行,从而保证行为的可预期性。 在Compose副作用可以通过以下方式实现: 1. 使用LaunchedEffect函数:这个函数可以在Composable的生命周期启动一个协程,并在协程完成后自动取消。这样可以执行一些异步操作,例如网络请求或数据库查询。以下是一个使用LaunchedEffect函数的示例: ```kotlin @Composable fun MyComposable() { LaunchedEffect(Unit) { // 执行异步操作,例如网络请求或数据库查询 // ... } } ``` 2. 使用DisposableEffect函数:这个函数可以在Composable的生命周期创建和清理资源。当Composable第一次创建时,会执行创建资源的代码块;当Composable被移除时,会执行清理资源的代码块。以下是一个使用DisposableEffect函数的示例: ```kotlin @Composable fun MyComposable() { DisposableEffect(Unit) { // 创建资源,例如打开文件或建立数据库连接 // ... onDispose { // 清理资源,例如关闭文件或断开数据库连接 // ... } } } ``` 3. 使用SideEffect函数:这个函数可以在Composable的生命周期执行一些副作用操作,例如弹出Toast提醒或记录日志。以下是一个使用SideEffect函数的示例: ```kotlin @Composable fun MyComposable() { SideEffect { // 执行副作用操作,例如弹出Toast提醒或记录日志 // ... } } ``` 这些是Compose处理副作用的常用方法。通过使用这些API,可以确保副作用Composable的生命周期被正确执行,从而保证行为的可预期性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值