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

相关文章

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

Python:豆瓣电影商业数据分析-爬取全数据【附带爬虫豆瓣,数据处理过程,数据分析,可视化,以及完整PPT报告】

**爬取豆瓣电影信息,分析近年电影行业的发展情况** 本文是完整的数据分析展现,代码有完整版,包含豆瓣电影爬取的具体方式【附带爬虫豆瓣,数据处理过程,数据分析,可视化,以及完整PPT报告】   最近MBA在学习《商业数据分析》,大实训作业给了数据要进行数据分析,所以先拿豆瓣电影练练手,网络上爬取豆瓣电影TOP250较多,但对于豆瓣电影全数据的爬取教程很少,所以我自己做一版。 目

docker-compose安装和简单使用

本文介绍docker-compose的安装和使用 新版docker已经默认安装了docker-compose 可以使用docker-compose -v 查看docker-compose版本 如果没有的话可以使用以下命令直接安装 sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-c

2024年AMC10美国数学竞赛倒计时两个月:吃透1250道真题和知识点(持续)

根据通知,2024年AMC10美国数学竞赛的报名还有两周,正式比赛还有两个月就要开始了。计划参赛的孩子们要记好时间,认真备考,最后冲刺再提高成绩。 那么如何备考2024年AMC10美国数学竞赛呢?做真题,吃透真题和背后的知识点是备考AMC8、AMC10有效的方法之一。通过做真题,可以帮助孩子找到真实竞赛的感觉,而且更加贴近比赛的内容,可以通过真题查漏补缺,更有针对性的补齐知识的短板。

12C 新特性,MOVE DATAFILE 在线移动 包括system, 附带改名 NID ,cdb_data_files视图坏了

ALTER DATABASE MOVE DATAFILE  可以改名 可以move file,全部一个命令。 resue 可以重用,keep好像不生效!!! system照移动不误-------- SQL> select file_name, status, online_status from dba_data_files where tablespace_name='SYSTEM'

Docker Compose使用手册

Docker Compose是一个比较简单的docker容器的编配工具,以前的名称叫Fig,由Orchard团队开发的开源Docker编配工具,在2014年被Docker公司收购,Docker Compose是使用Python语言开发的一款docker编配工具。使用Docker Compose,可以用一个yml文件定义一组要启动的容器,以及容器运行时的属性。Docker Compose称这些容器为

【docker】基于docker-compose 安装elasticsearch + kibana + ik分词器(8.10.4版本)

记录下,使用 docker-compose 安装 Elasticsearch 和 Kibana,并配置 IK 分词器,你可以按照以下步骤进行。此过程适用于 Elasticsearch 和 Kibana 8.10.4 版本。 安装 首先,在你的工作目录下创建一个 docker-compose.yml 文件,用于配置 Elasticsearch 和 Kibana 的服务。 version:

8阶段项目:五子棋(附带源码)

8阶段项目:五子棋 8.1-技术实现 1.静态变量 静态变量只能定义在类中,不能定义在方法中。静态变量可以在static修饰的方法中使用,也可以在非静态的方法中访问。主要解决在静态方法中不能访问非静态的变量。 2.静态方法 静态方法就相当于一个箱子,只是这个箱子中装的是代码,需要使用这些代码的时候,就把这个箱子放在指定的位置即可。   /*** 静态变量和静态方法*/public cl

Docker-Compose for Linux安装

Docker-Compose for Linux安装 1.前言2.安装Docker-Compose 1.前言 我们为什么要使用docker-compose? 我们运行一个docker镜像,需要添加大量的参数。 现在我们可以通过docker-compose编写这些参数。 Docker-Compose可以帮助我们批量管理这些容器。 我们只需要通过一个docker-compos

Post-Training有多重要?一文带你了解全部细节

1. 简介 随着LLM学界和工业界日新月异的发展,不仅预训练所用的算力和数据正在疯狂内卷,后训练(post-training)的对齐和微调方法也在不断更新。InstructGPT、WebGPT等较早发布的模型使用标准RLHF方法,其中的数据管理风格和规模似乎已经过时。近来,Meta、谷歌和英伟达等AI巨头纷纷发布开源模型,附带发布详尽的论文或报告,包括Llama 3.1、Nemotron 340