Jetpack Compose中的LaunchedEffect与rememberCoroutineScope

news2024/12/24 20:42:49

Jetpack Compose中的LaunchedEffect与rememberCoroutineScope

深入了解Compose副作用API LaunchedEffectrememberCoroutineScope
探索使用LaunchedEffectrememberCoroutineScope的区别和使用场景。

什么是副作用?
副作用是指在可组合函数范围之外发生的任何事情,最终会影响可组合函数,可能是一些状态的改变或在用户界面上发生的与可组合有关的用户操作。这两个API都是为了在受控环境中处理这些影响而构建的。

首先,让我们详细了解LaunchedEffect

LaunchedEffect副作用API

LaunchedEffect是一个可组合函数,只能从另一个可组合函数中执行。LaunchedEffect至少需要一个参数和一个挂起函数。它通过在容器可组合的范围内启动一个协程来执行该挂起函数。当第一次进入组合时,LaunchedEffect会立即执行该挂起函数,以及当其传递的变量之一的值发生改变时。当LaunchedEffect必须执行一个新的挂起函数以处理副作用时,它会取消先前正在运行的协程,并使用新的挂起函数启动一个新的协程。当离开组合本身时,LaunchedEffect也会取消已启动的协程。协程始终在容器可组合函数的范围内启动。

LaunchedEffect底层实现

让我们看一下LaunchedEffect的函数声明之一。

@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1) { LaunchedEffectImpl(applyContext, block) }
}
internal class LaunchedEffectImpl(
    parentCoroutineContext: CoroutineContext,
    private val task: suspend CoroutineScope.() -> Unit
) : RememberObserver {
    private val scope = CoroutineScope(parentCoroutineContext)
    private var job: Job? = null

    override fun onRemembered() {
        job?.cancel("Old job was still running!")
        job = scope.launch(block = task)
    }

    override fun onForgotten() {
        job?.cancel()
        job = null
    }

    override fun onAbandoned() {
        job?.cancel()
        job = null
    }
}

通过查看上面的代码,以下是要点回顾:

  • LaunchedEffect 是一个可组合函数,因此只能在另一个可组合函数内执行。
  • LaunchedEffect 接受一个参数和一个必须执行的挂起函数。
  • LaunchedEffect 将当前可组合的协程上下文传递给 LaunchedEffectImpl,并传递一个将要执行的挂起函数,显示协程将在父可组合函数范围内启动。
  • LaunchedEffectImpl 将挂起函数作为代码块,启动协程,如果存在先前运行的协程,则取消它。
  • LaunchedEffect 期望至少传递一个参数。如果您不想传递任何参数,您可以传递 null Unit。在这种情况下,我选择传递 Unit 作为参数。

如果您传递 Unit null 作为参数,则挂起函数将在组合阶段仅执行一次。
LaunchedEffect 在可组合函数范围内的代码块中启动协程,在 LaunchedEffect 离开组合或任何 LaunchedEffect 参数变化时,正在执行的协程将被取消。

LaunchedEffect 示例

让我们来看一下下面的代码示例,以了解 LaunchedEffect 的一些特点

@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LaunchedEffectTestScreen (
    snackbarHostState: SnackbarHostState,
    viewModel: LaunchedEffectTestViewModel
) {
    val snackbarCount =  viewModel.snackbarCount.collectAsState()
    LaunchedEffect(snackbarCount.value) {
        Log.d("launched-effect","displaying launched effect for count ${snackbarCount.value}")
        try {
            snackbarHostState.showSnackbar("LaunchedEffect snackbar", "ok")
        } catch(e: Exception){
            Log.d("launched-effect","launched Effect coroutine cancelled exception $e")
        }
    }

    Scaffold(
        snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
    ) {
        Column {
            Text(text = "LaunchedEffect Test")
        }
    }

}

在上面的代码示例中,LaunchedEffectTestScreen组合使用LaunchedEffect来在第一次和传递的参数snackbarCount更改时显示一个snackbar。相应的viewModel代码如下所示。

class LaunchedEffectTestViewModel : ViewModel() {

    private var _snackbarCount = MutableStateFlow(1)
    val snackbarCount: StateFlow<Int> get() = _snackbarCount

    init {
        viewModelScope.launch {
            var displayCount = 1
            while (displayCount < 3) {
                delay(1000L)
                displayCount += 1
                _snackbarCount.value = displayCount
            }
        }
    }
}

在 ViewModel 中,snackbarCount StateFlow 的初始值为1。ViewModel 进一步启动一个协程,以每秒更新 snackbarCount StateFlow 的值,最多更新3次。由于 snackbarCount 的值将会改变,LaunchedEffect 将在每次值变化时执行,并且会启动一个新的协程,取消之前的协程。以上代码的日志输出如下所示。

D/launched-effect: displaying launched effect for count 1
D/launched-effect: launched Effect coroutine cancelled exception kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@28abbfe
D/launched-effect: displaying launched effect for count 2
D/launched-effect: launched Effect coroutine cancelled exception kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@14b5985
D/launched-effect: displaying launched effect for count 3

显示LaunchedEffect在启动时使用snackbarCount值为1执行协程,并在下次启动时使用snackbarCount值为2启动新的协程,取消前一个协程。你可以在日志中看到协程1和协程2的JobCancellationException

LaunchedEffect的应用

当我们希望在组合阶段开始时执行与UI相关的任务(挂起函数),LaunchedEffect通常非常有效。但是当传递的状态参数值发生变化时,它也会执行。以下是一些LaunchedEffect的应用场景。

  1. 滚动到特定位置的惰性列表:在聊天应用程序中,当用户第一次加载应用程序或聊天屏幕时,我们希望用户看到最新的消息,所以我们将聊天消息滚动到列表底部,可以使用以下代码实现,使用LaunchedEffect
LaunchedEffect(Unit, block = {
    lazyListState.scrollToItem(messages.size - 1)
})

我们正在将Unit作为参数传递,这意味着我们只想在用户首次进入屏幕即合成阶段时调用此suspend块。一旦用户进入屏幕,它将滚动到列表底部。

以下是包含此示例的Github项目存储库。

https://github.com/saqib-github-commits/BasicCompose

  1. 在组合中添加 Composable 时立即执行动画。有一篇关于在 Jetpack Compose 中使用动画的文章,你可以从那里阅读 -> 自定义画布动画在 JetpackCompose 中的使用

https://medium.com/androiddevelopers/custom-canvas-animations-in-jetpack-compose-e7767e349339

  1. 应用程序加载屏幕:在应用程序启动时显示加载屏幕也是 LaunchedEffect 的用例之一。我们如何实现它?看下面的代码。
    我们将创建一个 LoadingScreen 的组合函数。
@Composable
fun LoadingScreen(onTimeout: () -> Unit) {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        LaunchedEffect(Unit) {
            delay(5000L)
            onTimeout()
        }

        CircularProgressIndicator()
    }
}

LoadingScreen的可组合项(composable)显示一个全屏的可组合项(composable),其中央显示一个CircularProgressIndicator,以在UI中显示加载状态。LoadingScreen还使用Api LauncedEffect,并将Unit作为参数传递,因为我们希望仅在LoadingScreen进入屏幕,即在组合阶段期间,才启动传递的块。LaunchedEffect将执行一个暂停函数,该函数使用延迟(delay)模拟后端响应(我们尚未拥有后端),并在调用onTimeOut方法之前等待5秒钟来显示加载屏幕。

现在,我们需要更改MainActivity中的初始代码,以添加一个用于LoadingScreen的开关,如下所示。

var showLoading by remember {
      mutableStateOf(true)
}
  
if (showLoading) {
      LoadingScreen { showLoading = false }
} else {
      val snackbarHostState = SnackbarHostState()
      LaunchedEffectTestScreen(snackbarHostState, LaunchedEffectTestViewModel())
}

MainActivity中,我们记住了一个布尔状态,用来存储关于何时显示LoadingScreen的信息。初始值为true,所以会调用LoadingScreen,并传入一个lambda表达式,将showLoading标志设置为false。这个方法将在LoadingScreenLaunchedEffect内部的5秒后调用,正如我们之前看到的代码一样。所以在5秒后,showLoading标志变为false,然后进入else部分,显示LaunchedEffectTestScreen

完整的代码如下。

https://github.com/saqib-github-commits/JetpackComposeSuspendFunctions

  1. 当网络不可用时显示 Snackbar 消息:在实际项目中,通常我们希望在页面中显示一个自定义的通知视图,以显示网络状态(已连接/离线),通常在应用栏下面的页面顶部。但为了展示LaunchedEffect的示例,我在这里使用了 Snackbar

让我们来看一下 Composable。

@Composable
fun LaunchedEffectNetworkState(
    snackbarHostState: SnackbarHostState,
    viewModel: LaunchedEffectNetworkStateViewModel
) {
    val showNetworkUnavailable by viewModel.networkUnavailable.collectAsState()
    if (showNetworkUnavailable) {
        LaunchedEffect(Unit) {
            snackbarHostState.showSnackbar("Network Unavailable")
        }
    }

    Scaffold(
        snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
    ) {
        Text(text = "Network State using LaunchedEffect")

    }
}

Composable正在观察showNetworkUnavailable中来自viewModel的状态。如果值为true,它将执行LaunchedEffect,显示一个关于网络不可用的snackbar消息,当值变为false时,LaunchedEffect将离开组合并取消之前启动的协程。

让我们看一下ViewModel以获得完整的信息。

class LaunchedEffectNetworkStateViewModel: ViewModel() {

    private var _networkUnavailable = MutableStateFlow(false)
    val networkUnavailable get() = _networkUnavailable.asStateFlow()

    init {
        viewModelScope.launch {
            delay(2000L)
            _networkUnavailable.value = true
        }
    }

}

ViewModel 模仿了网络不可用的效果,因为我们不需要为了示例而实现完整的网络状态监听器。ViewModel 使用初始值为 false 的 networkUnavailable StateFlow,并在协程中经过 2 秒后将 networkUnavailable 的值更改为 true。由于值在 2 秒后发生了变化,Composable 将在 2 秒后执行挂起函数,显示一个 Snackbar 消息。

就关于 LaunchedEffect 而言,就是这些。LaunchedEffect 有许多其他实际应用,但希望这些例子可以帮助您了解 LaunchedEffect 的一般用法。

rememberCoroutineScope 是副作用 API

LaunchedEffect 的副作用 API 有助于在组合阶段通过协程调用挂起函数。但是,在某些情况下,我们希望执行一些操作,但不是在组合中立即执行,而是在以后的某个时间点执行,例如当用户在 UI 上执行某些操作时。为此,我们需要一个作用域来启动协程,而 rememberCoroutineScope 提供了一个协程作用域,与调用它的 Composable 的作用域绑定,以便了解 Composable 的生命周期,并在离开组合时取消协程。通过该作用域,我们可以在不在组合内部时调用协程,即可以在用户操作期间在非 Composable 的作用域内启动协程。

rememberCoroutineScope 的底层实现

让我们来看看 rememberCoroutineScope 函数。

@Composable
inline fun rememberCoroutineScope(
    getContext: @DisallowComposableCalls () -> CoroutineContext = { EmptyCoroutineContext }
): CoroutineScope {
    val composer = currentComposer
    val wrapper = remember {
        CompositionScopedCoroutineScopeCanceller(
            createCompositionCoroutineScope(getContext(), composer)
        )
    }
    return wrapper.coroutineScope
}

一些要验证的要点如下:

  • rememberCoroutineScope是一个可组合函数。
  • 它创建了一个与当前可组合相关联的协程作用域,因此它将知道可组合的生命周期,并且在可组合离开组合时会自动取消。

rememberCoroutineScope示例

让我们看一下下面代码中使用rememberCoroutineScope的基本示例。

@Composable
fun RememberCoroutineScopeTestScreen ( ) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        val coroutineScope = rememberCoroutineScope()
        var counter by remember { mutableStateOf(0) }

        Text(text = counter.toString())
        Spacer(modifier = Modifier.height(8.dp))
        Button(
            onClick = {
                coroutineScope.launch {
                    counter += 1
                }
            }
        ) {
            Text(text = "Button")
        }
    }
}

上述代码在屏幕上显示了一个文本和一个按钮。我们使用rememberCoroutineScope获取一个协程范围,并在按钮的onClick事件监听器中使用它来启动一个协程。该协程在每次用户按钮按下事件发生时递增计数器。onClick事件监听器不在组合范围内,它是一个事件监听器,所以我们需要显式地使用协程范围来在组合范围之外启动协程,但它与组合生命周期有关。

rememberCoroutineScope的应用

rememberCoroutineScope有许多实际应用。我们将看到一些我已经使用过的应用。

  1. 带有“返回顶部/底部”按钮的懒加载列表:通常情况下,我们会在用户执行特定操作时,通过UI上的按钮将列表内容滚动到底部或顶部。下面的代码展示了使用rememberCoroutineScope和启动协程执行那些挂起函数的情况,并应用在懒加载列表上。
// Button to Go To Bottom of the list
Button(onClick = {
  coroutineScope.launch { lazyListState.animateScrollToItem(messages.size - 1) }
}) {
  Text(text = "Go To Bottom")
}

// Button to Go To Top of the list
Button(onClick = {
  coroutineScope.launch { lazyListState.animateScrollToItem(0) }
}) {
  Text(text = "Go To Top")
}
  1. 使用Next和Prev按钮的ViewPager:滚动ViewPager以响应Next和Prev按钮操作也是使用rememberCoroutineScope的理想应用,如下面的代码所示。
 Button(
                enabled = prevButtonVisible.value,
                onClick = {
                    val prevPageIndex = pagerState.currentPage - 1
                    coroutineScope.launch { pagerState.animateScrollToPage(prevPageIndex) }
                },
            ) {
                Text(text = "Prev")
            }

            Button(
                enabled = nextButtonVisible.value ,
                onClick = {
                    val nextPageIndex = pagerState.currentPage + 1
                    coroutineScope.launch { pagerState.animateScrollToPage(nextPageIndex) }
                },
            ) {
                Text(text = "Next")
            }

下面是ViewPager实现示例的完整代码。

https://github.com/saqib-github-commits/JetpackComposeViewPager

LaunchedEffectrememberCoroutineScope的比较

以下是两者比较的重要点总结:

  • LaunchedEffectrememberCoroutineScope都是副作用API,用于以受控且可预测的方式执行副作用操作。
  • LaunchedEffect在可组合项的范围内执行挂起函数,而rememberCoroutineScope在可组合项的范围之外执行,但仍然受到可组合项生命周期的影响。
  • LaunchedEffectrememberCoroutineScope这两个API都在生命周期感知的方式下运行,并且在所创建的可组合项离开组合时立即取消启动的协程。
  • 通常在想要在可组合项的组合阶段执行操作(即用户第一次进入屏幕)或者当传递给它的任何状态参数发生变化时,会使用LaunchedEffect。 而当我们不在组合中,通常是用户执行某些操作,比如按钮点击,我们希望通过副作用来更新UI状态时,会使用rememberCoroutineScope
  • LaunchedEffectrememberCoroutineScope应只执行与UI相关的任务,不应违反单向数据流原则。

参考源码

https://github.com/saqib-github-commits/JetpackComposeSuspendFunctions

参考

https://developer.android.com/jetpack/compose/side-effects
https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects#0

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/652875.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024考研408-计算机组成原理第三章-存储系统

文章目录 前言一、存储器概述1.1、层次结构1.2、存储器分类1.2.1、层次分类1.2.2、存储介质分类1.2.3、存取方式1.2.4、按照信息的可更改性&#xff08;读写、只读区别&#xff09; 1.3、存储器性能指标知识回顾 二、主存储器2.1、主存储器的基本组成&#xff08;介绍DRAM&…

Axios和Spring MVC[前端和后端的请求和响应处理]

在前后端交互中&#xff0c;Axios和Spring MVC扮演着不同的角色&#xff0c;分别负责前端和后端的请求和响应处理。它们之间的作用如下&#xff1a; Axios&#xff08;前端&#xff09;&#xff1a; 发送HTTP请求&#xff1a;前端使用Axios库发送HTTP请求到后端。可以使用Axi…

HarmonyOS 极客马拉松2023 正式启动,诚邀极客们用键盘码出无限可能!

原文&#xff1a;HarmonyOS 极客马拉松2023 正式启动&#xff0c;诚邀极客们用键盘码出无限可能&#xff01;&#xff0c;点击链接查看原文进入报名通道。 2023年6月15日&#xff0c; HarmonyOS极客马拉松2023开赛&#xff01;期待各位开发者极客朋友一起&#xff0c;探索移动应…

IT自动化运维工具优势与劣势分析-行云管家

随着自动化技术的进步&#xff0c;目前市面上出现了不少IT自动化运维工具。但很多人对于IT自动化运维工具优劣势不是很清楚&#xff0c;这里我们就来一起简单分析分析。 IT自动化运维工具优势 1、提高IT运维工作人员工作效率&#xff0c;提升整体项目效率&#xff1b; 2、提…

HQChart使用教程30-K线图如何对接第3方数据37-如何绘制圆点

HQChart使用教程30-K线图如何对接第3方数据37-如何绘制圆点 效果图步骤json格式nametypeDrawDraw.DrawTypeDraw.DrawData[] HQChart代码地址 效果图 步骤 步骤和第3方指标替换一样&#xff0c; 通过网络协议回调&#xff0c;知道需要执行的指标&#xff0c; 替换自己的数据。这…

chatgpt赋能python:Python中的整数筛选与小数筛选

Python中的整数筛选与小数筛选 Python是一种高级编程语言&#xff0c;常见于各种领域的编程工作中。在Python中&#xff0c;我们可以很方便地筛选整数和小数&#xff0c;用于数据分析、算法计算、图像处理等多个方向。本文将从Python中的整数筛选和小数筛选两个方面进行介绍和…

90%电子工程师都没学透的3W规则,这篇文章一次性告诉你

在PCB设计中&#xff0c;了解和应用3W规则是非常重要的&#xff0c;能帮助工程师在设计阶段时考虑到电路板的功率分布、热管理和可靠性&#xff0c;但很多电子工程师对3W规则只处于皮毛学习阶段&#xff0c;没有做到真正掌握&#xff0c;导致电子设计技术没长进&#xff0c;所以…

​一些常见网络安全术语​

1、黑帽 为非法目的进行黑客攻击的人&#xff0c;通常是为了经济利益。他们进入安全网络以销毁&#xff0c;赎回&#xff0c;修改或窃取数据&#xff0c;或使网络无法用于授权用户。这个名字来源于这样一个事实&#xff1a;老式的黑白西部电影中的恶棍很容易被电影观众识别&…

Mac中开发常用的软件

Mac中开发常用的软件 https://pan.baidu.com/ 开发软件 IDEA2021IntelliJ IDEA 2022 for Mac(最好用的Java开发工具)永久激活版SnailSVNRedisDesktopManagerTermius终端SourceTree&#xff08;Git&#xff09;Microsoft Remote Desktop&#xff08;远程桌面&#xff09; 数据…

flutter系列之:做一个图像滤镜

文章目录 简介我们的目标带滤镜的图片打造filter按钮打造可滑动按钮最后要解决的问题 简介 很多时候&#xff0c;我们需要一些特效功能&#xff0c;比如给图片做个滤镜什么的&#xff0c;如果是h5页面&#xff0c;那么我们可以很容易的通过css滤镜来实现这个功能。 那么如果在…

复合垂直纳米线中硅锗的选择性湿法蚀刻

引言 目前&#xff0c;对高效能和高性能微电子组件的持续需求是晶体管小型化和芯片密度增加的关键驱动因素。这些纳米级晶体管的性能取决于其架构和材料特性。由于更好的静电性能&#xff0c;全栅场效应晶体管(GAAFET)有望取代当前的FinFET架构。 垂直GAAFET很难集成到当前的…

vue 树形结构,后端返回平级数据 一个页面向另一个页面传递数据

代码 Index.vue页面 methods: {/** 查询会议列表 */getList() {// this.loading true;listMeeting(this.queryParams).then(response > {this.meetingList response.rowsthis.total response.totalthis.loading false})//后端请求树形数据listNoPage().then(response &…

Vue- mixin(混入)

mixin(混入) mixin&#xff1a;翻译过来叫混入&#xff0c;也有人叫混合&#xff0c;官方叫混入 作用&#xff1a;两个组件共享一个配置(配置必须是一样的)&#xff0c;本质就是复用配置 通过案例理解学习如何使用以及作用 1 编写案例 提出需求&#xff0c;新增一个组件&a…

css平面转换

目录 平面转换transform(2D转换) CSS3 过渡&#xff08;重点&#xff09; 平面转换-平移 平移实现居中效果 平移转换-旋转 平面多重转换 旋转-改变转换原点 平移转换-缩放 平移转换-倾斜 平面转换transform(2D转换) 作用&#xff1a;给元素添加动态效果&#xff0c;一…

阿里云ECS弹性公网IP临时升级带宽

背景 618到来&#xff0c;公司服务器近一周内会出现IO暴增&#xff0c;需要临时升级带宽以满足需求。但由于使用的是EIP&#xff0c;无法直接在实例控制台进行升配。 方案 在弹性公网IP进行扩容 阿里云登录 - 欢迎登录阿里云&#xff0c;安全稳定的云计算服务平台

redis面试题详解

缓存 缓存穿透 缓存穿透&#xff1a;查询一个不存在的数据&#xff0c;MySQL查询不到数据也不会直接写入缓存&#xff0c;就会导致每次请求都查数据库。 解决方案一&#xff1a;缓存空数据&#xff0c;查询返回的数据为空&#xff0c;仍把这个空结果进行缓存 解决方案二&am…

WSL2安装Ubuntu及一些问题

文章目录 安装wsl2设置wsl版本安装Linux发行版问题问题1问题2 迁移导出注销原系统导入 windows和linux互传文件解决raw.githubusercontent.com无法访问的问题 安装wsl2 安装条件 内部版本 19041 及以上 (win10 2004以上或者win11) 查看方法&#xff1a;按 Windows健 R -->…

mathtype7.4数学计算工具下载安装教程

很多朋友使用word写文档或者论文&#xff0c;但是又要插入很多数学计算公式的情况&#xff0c;使用word本身的公式和符号并不是很方便用&#xff0c;今天分享给大家一个实用的数学工具mathtype。 MathType软件可以选择下载Windows版本MathType 7或Mac版本MathType 7。MathType…

OpenGL超级第11章学习笔记:高级数据管理

前言 本篇在讲什么 OpenGL蓝宝书第十一章学习笔记之高级数据管理 本篇适合什么 适合初学OpenGL的小白 本篇需要什么 对C语法有简单认知 对OpenGL有简单认知 最好是有OpenGL超级宝典蓝宝书 依赖Visual Studio编辑器 本篇的特色 具有全流程的图文教学 重实践&#x…

export 和 export default 的区别

一、铺垫 ES6模块化主要包含以下3种用法&#xff1a; ① 默认导出和默认导入---export default ② 按需导出和按需导入---export ③ 直接导入并执行模块中的代码 二、默认导出 语法&#xff1a;export default 需要共享的成员 创建一个共享的数据 order.js let name…