【Compose】附带作用

本文介绍了Jetpack Compose里附带作用的使用及特性
Side Effect, 官方有一段时间翻译为副作用,后来终于改为了更为精准的附带作用。
设计目的
官方定义:
附带效应是指发生在可组合函数作用域之外的应用状态的变化。由于可组合项的生命周期和属性(例如不可预测的重组、以不同顺序执行可组合项的重组或可以舍弃的重组),可组合项在理想情况下应该是无附带效应的。不过,有时附带效应是必要的,例如,触发一次性事件(例如显示信息提示控件),或在满足特定状态条件时进入另一个屏幕。这些操作应从能感知可组合项生命周期的受控环境中调用。在本页中,您将了解 Jetpack Compose 提供的不同附带效应 API。
简单点说,就是Composable函数设计之初就已经规定好了,每一次Composable的函数的重组会尽量以最小重组范围来进行。而且,顺序写在一起的Composable函数,他们之间的调用顺序不是确定的。理想情况不需要跳脱到这个规则之外,否则可能会产生未知的数据问题。
但是很多情况,我们用这个设计理念来写代码,会产生很多不优雅的情况,比如超长的变量传递链,不合理不直观的架构设计。
所以就设计了一些附带作用的API,在某些情况下代码块里面的跳脱到重组的范围之外,比如只有初次进入时调用一次,后面都不参与重组;还比如每次退出Composable函数时调用一次。
LauncheEffect
如需在可组合项的 生命周期内执行工作并能够调用挂起函数 ,请使用 LaunchedEffect 可组合项。
当它的内部逻辑要触发时,它会启动一个协程,并将代码块作为参数传递。
如果 LaunchedEffect 退出组合,协程将取消。
源码:
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block) }
}
- key参数是其监听变化状态的值
- block就是丢给协程执行的代码块。
当这个外部的key的值发生变化时,协程会被取消,重新启动。并且,当LaunchedEfftect的所属可组合项退出时,协程会被直接取消。
最常用用法是用来触发网络数据获取,在挂起函数执行完毕数据返回来之后,更新界面的状态。
如下面例子所示:
@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])
}
}
}
}
}
remeberCoroutineScope
由于 LaunchedEffect 是可组合函数,因此只能在其他可组合函数中使用。
remeberCoroutineScope 可以在可组合项外启动协程,但存在作用域限制,以便协程在退出组合后自动取消,请使用 rememberCoroutineScope。 此外,如果您需要手动控制一个或多个协程的生命周期,请使用 rememberCoroutineScope,例如在用户事件发生时取消动画。
rememberCoroutineScope 是一个可组合函数,会返回一个 CoroutineScope,该 CoroutineScope 绑定到调用它的组合点。调用退出组合后,作用域将取消。
@Composable
fun SideEffectPage() {
val isShowButtton = remember {
mutableStateOf(true)
}
LaunchedEffect(Unit) {
delay(3000)
isShowButtton.value = false
}
if (isShowButtton.value) {
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
delay(2000)
Log.i("rememberCoroutineScope", "fdgbfv")
}
}) {
Text(text = "SidsfgvdcsPage ")
}
}
}
在isShowButtton作用域代码块里面使用这个附加作用获取一个协程对象,在点击按钮时,启动一个协程,在协程里面执行一些耗时操作。
同时外部的 LaunchedEffect 作用下,3s后,这个协程所属的可组合项会退出生命周期,这个协程里的所有任务也会被取消。
SideEffect
想象这样一个需求,要求用户进入某个界面之后,记录他们的某个操作的埋点数据。
如果直接在Composable方法中执行这个记录工作,那么即使重组失败,数据仍然会更新。需要确保Composable重组成功的情况下,才会记录埋点数据。
@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
FirebaseAnalytics()
}
// On every successful composition, update FirebaseAnalytics with
// the userType from the current User, ensuring that future analytics
// events have this metadata attached
SideEffect {
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
DisposableEffect
DisposableEffect 可组合项会在每次重新组合时调用其效果,并且在每次重新组合时,都会在退出组合时调用其 onDispose 回调。可以在 onDispose 回调中执行清理工作。回收该Composable函数中使用的所有资源。
看这样一个场景,在isShowButton.value为true时,在作用域内启动一个协程任务,在3s后更新Text的数据。
注意直接在Composable函数中启动协程任务,会导致协程任务的生命周期和Composable函数的生命周期不一致,导致协程任务无法被正确的回收。这里仅作演示。
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun SideEffectPage() {
val isShowButton = remember { mutableStateOf(true) }
LaunchedEffect (Unit){
delay(1000)
isShowButton.value = false
}
if(isShowButton.value) {
val text = remember { mutableStateOf("Hello") }
val scope = CoroutineScope(Dispatchers.Main)
val job = scope.launch {
delay(3000)
Log.d("SideEffectPage", "SideEffectPage: ")
text.value = "Hello World"
}
Text(text = text.value)
}
}
1s后尽管已经隐藏了Button,退出了可组合项的生命周期,但是协程任务依然在运行,导致内存泄漏。
这时候我们可以使用DisposableEffect,在onDispose回调中,取消协程任务。
更改后如下:
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun SideEffectPage() {
val isShowButton = remember { mutableStateOf(true) }
//
LaunchedEffect (Unit){
delay(1000)
isShowButton.value = false
}
if(isShowButton.value) {
val text = remember { mutableStateOf("Hello") }
DisposableEffect(Unit) {
val scope = CoroutineScope(Dispatchers.Main)
val job = scope.launch {
delay(3000)
Log.d("SideEffectPage", "SideEffectPage: ")
text.value = "Hello World"
}
onDispose {
job.cancel()
}
}
Text(text = text.value)
}
}
rememberUpdateState
如果我们使用LaunchedEffect时要引用一个值,但是如果该值发生更改,则不应重新启动,请使用 rememberUpdatedState。
当关键参数的值之一更新时, LaunchedEffect 会重新启动,但有时我们希望在不重新启动的情况下捕获效果中更改的值。
如果我们有长时间运行的选项,重新启动成本很高,则此过程会很有帮助。
@Composable
fun SideEffectPage() {
val netText = remember { mutableStateOf("") }
LaunchedEffect(Unit) {
delay(3000L)
netText.value = "got a new String from network"
}
UpdateText(netText.value)
}
@Composable
fun UpdateText(test: String) {
val uiText = remember { mutableStateOf("initial local text") }
val updateText = rememberUpdatedState(test)
LaunchedEffect(Unit) {
delay(5000L)
uiText.value = updateText.value
}
Box(
modifier = Modifier.fillMaxSize(1f),
contentAlignment = Alignment.Center
) {
Text(text = uiText.value)
}
}
例如在5s后更新一次Text的数据,但是在3s后,网络请求已经返回了新的数据,这个时候我们就可以使用 rememberUpdateState 先将这个更新的数据存储起来。5s后就可以使用更新的正确的数据。
另外还有几个附带作用的函数,我日常使用的时候几乎没有使用过,暂时先不做记录。