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

2023-12-06 14:20

本文主要是介绍一文彻底吃透 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 是 Compose 中的一个函数,用于在不影响 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}")       // 每次点击按钮时,这种重组不会触发外部副作用}}
}

再看下运行效果:

在这里插入图片描述


📓 DisposableEffect

DisposableEffect 函数在首次渲染组合项时执行副作用,并在从 UI 层次结构中删除可组合项时释放该效果。此函数可用于管理不再使用可组合项时需要清理的资源,例如事件侦听器或动画。

下面是如何使用 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 += 1println("@@@ 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 delaydelay(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 calldelay(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 秒。

这篇关于一文彻底吃透 Compose 中的副作用(附带效应)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/462148

相关文章

一文详解JavaScript中的fetch方法

《一文详解JavaScript中的fetch方法》fetch函数是一个用于在JavaScript中执行HTTP请求的现代API,它提供了一种更简洁、更强大的方式来处理网络请求,:本文主要介绍Jav... 目录前言什么是 fetch 方法基本语法简单的 GET 请求示例代码解释发送 POST 请求示例代码解释

一文详解SpringBoot响应压缩功能的配置与优化

《一文详解SpringBoot响应压缩功能的配置与优化》SpringBoot的响应压缩功能基于智能协商机制,需同时满足很多条件,本文主要为大家详细介绍了SpringBoot响应压缩功能的配置与优化,需... 目录一、核心工作机制1.1 自动协商触发条件1.2 压缩处理流程二、配置方案详解2.1 基础YAML

一文详解如何从零构建Spring Boot Starter并实现整合

《一文详解如何从零构建SpringBootStarter并实现整合》SpringBoot是一个开源的Java基础框架,用于创建独立、生产级的基于Spring框架的应用程序,:本文主要介绍如何从... 目录一、Spring Boot Starter的核心价值二、Starter项目创建全流程2.1 项目初始化(

通过Docker Compose部署MySQL的详细教程

《通过DockerCompose部署MySQL的详细教程》DockerCompose作为Docker官方的容器编排工具,为MySQL数据库部署带来了显著优势,下面小编就来为大家详细介绍一... 目录一、docker Compose 部署 mysql 的优势二、环境准备与基础配置2.1 项目目录结构2.2 基

一文带你了解SpringBoot中启动参数的各种用法

《一文带你了解SpringBoot中启动参数的各种用法》在使用SpringBoot开发应用时,我们通常需要根据不同的环境或特定需求调整启动参数,那么,SpringBoot提供了哪些方式来配置这些启动参... 目录一、启动参数的常见传递方式二、通过命令行参数传递启动参数三、使用 application.pro

一文带你深入了解Python中的GeneratorExit异常处理

《一文带你深入了解Python中的GeneratorExit异常处理》GeneratorExit是Python内置的异常,当生成器或协程被强制关闭时,Python解释器会向其发送这个异常,下面我们来看... 目录GeneratorExit:协程世界的死亡通知书什么是GeneratorExit实际中的问题案例

一文详解SQL Server如何跟踪自动统计信息更新

《一文详解SQLServer如何跟踪自动统计信息更新》SQLServer数据库中,我们都清楚统计信息对于优化器来说非常重要,所以本文就来和大家简单聊一聊SQLServer如何跟踪自动统计信息更新吧... SQL Server数据库中,我们都清楚统计信息对于优化器来说非常重要。一般情况下,我们会开启"自动更新

一文详解kafka开启kerberos认证的完整步骤

《一文详解kafka开启kerberos认证的完整步骤》这篇文章主要为大家详细介绍了kafka开启kerberos认证的完整步骤,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、kerberos安装部署二、准备机器三、Kerberos Server 安装1、配置krb5.con

一文详解Nginx的强缓存和协商缓存

《一文详解Nginx的强缓存和协商缓存》这篇文章主要为大家详细介绍了Nginx中强缓存和协商缓存的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、强缓存(Strong Cache)1. 定义2. 响应头3. Nginx 配置示例4. 行为5. 适用场景二、协商缓存(协

一文详解如何在Python中使用Requests库

《一文详解如何在Python中使用Requests库》:本文主要介绍如何在Python中使用Requests库的相关资料,Requests库是Python中常用的第三方库,用于简化HTTP请求的发... 目录前言1. 安装Requests库2. 发起GET请求3. 发送带有查询参数的GET请求4. 发起PO