一文彻底吃透 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

相关文章

一文详解SpringBoot中控制器的动态注册与卸载

《一文详解SpringBoot中控制器的动态注册与卸载》在项目开发中,通过动态注册和卸载控制器功能,可以根据业务场景和项目需要实现功能的动态增加、删除,提高系统的灵活性和可扩展性,下面我们就来看看Sp... 目录项目结构1. 创建 Spring Boot 启动类2. 创建一个测试控制器3. 创建动态控制器注

一文详解Git中分支本地和远程删除的方法

《一文详解Git中分支本地和远程删除的方法》在使用Git进行版本控制的过程中,我们会创建多个分支来进行不同功能的开发,这就容易涉及到如何正确地删除本地分支和远程分支,下面我们就来看看相关的实现方法吧... 目录技术背景实现步骤删除本地分支删除远程www.chinasem.cn分支同步删除信息到其他机器示例步骤

一文详解Java Stream的sorted自定义排序

《一文详解JavaStream的sorted自定义排序》Javastream中的sorted方法是用于对流中的元素进行排序的方法,它可以接受一个comparator参数,用于指定排序规则,sorte... 目录一、sorted 操作的基础原理二、自定义排序的实现方式1. Comparator 接口的 Lam

如何在Mac上彻底删除Edge账户? 手动卸载Edge浏览器并清理残留文件技巧

《如何在Mac上彻底删除Edge账户?手动卸载Edge浏览器并清理残留文件技巧》Mac上的Edge账户里存了不少网站密码和个人信息,结果同事一不小心打开了,简直尴尬到爆炸,想要卸载edge浏览器并清... 如果你遇到 Microsoft Edge 浏览器运行迟缓、频繁崩溃或网页加载异常等问题,可以尝试多种方

一文深入详解Python的secrets模块

《一文深入详解Python的secrets模块》在构建涉及用户身份认证、权限管理、加密通信等系统时,开发者最不能忽视的一个问题就是“安全性”,Python在3.6版本中引入了专门面向安全用途的secr... 目录引言一、背景与动机:为什么需要 secrets 模块?二、secrets 模块的核心功能1. 基

一文详解MySQL如何设置自动备份任务

《一文详解MySQL如何设置自动备份任务》设置自动备份任务可以确保你的数据库定期备份,防止数据丢失,下面我们就来详细介绍一下如何使用Bash脚本和Cron任务在Linux系统上设置MySQL数据库的自... 目录1. 编写备份脚本1.1 创建并编辑备份脚本1.2 给予脚本执行权限2. 设置 Cron 任务2

一文详解如何在idea中快速搭建一个Spring Boot项目

《一文详解如何在idea中快速搭建一个SpringBoot项目》IntelliJIDEA作为Java开发者的‌首选IDE‌,深度集成SpringBoot支持,可一键生成项目骨架、智能配置依赖,这篇文... 目录前言1、创建项目名称2、勾选需要的依赖3、在setting中检查maven4、编写数据源5、开启热

一文全面详解Python变量作用域

《一文全面详解Python变量作用域》变量作用域是Python中非常重要的概念,它决定了在哪里可以访问变量,下面我将用通俗易懂的方式,结合代码示例和图表,带你全面了解Python变量作用域,需要的朋友... 目录一、什么是变量作用域?二、python的四种作用域作用域查找顺序图示三、各作用域详解1. 局部作

一文彻底搞懂Java 中的 SPI 是什么

《一文彻底搞懂Java中的SPI是什么》:本文主要介绍Java中的SPI是什么,本篇文章将通过经典题目、实战解析和面试官视角,帮助你从容应对“SPI”相关问题,赢得技术面试的加分项,需要的朋... 目录一、面试主题概述二、高频面试题汇总三、重点题目详解✅ 面试题1:Java 的 SPI 是什么?如何实现一个

Kotlin Compose Button 实现长按监听并实现动画效果(完整代码)

《KotlinComposeButton实现长按监听并实现动画效果(完整代码)》想要实现长按按钮开始录音,松开发送的功能,因此为了实现这些功能就需要自己写一个Button来解决问题,下面小编给大... 目录Button 实现原理1. Surface 的作用(关键)2. InteractionSource3.