关于Compose中的SideEffect

1、什么是Compsoe副作用

根据官方文档https://developer.android.com/jetpack/compose/side-effects?hl=zh-cn

副作用是指发生在Composeable可组合函数作用域之外的应用状态的变化。由于可组合项的生命周期和属性(例如不可预测的重组、以不同顺序执行可组合项的重组或可以舍弃的重组),可组合项在理想情况下应该是无副作用的

但是很多时候副作用操作又是必要的,比如:

1、在Composeable进入的时候进行曝光上报,然后在退出的时候进行反曝光
2、收到onClick之后使用协程运行挂起函数,发送网络请求
3、在Composeable进入的时候开始监听Lifecycle生命周期,然后在退出的时候移除Lifecycle生命周期监听

当涉及到以上操作的时候,就需要涉及到Compose的SideEffect相关API的使用了,以便以可预测的方式执行这些SideEffect

下面讲一下这些API的实际使用场景:

2、LaunchedEffect

当我们想在组合进入的时候使用协程运行挂起函数,然后组合退出的时候取消协程,就可以使用LaunchedEffect:

1
2
3
4
5
6
7
8
@Composable
private inline fun Report1(noinline impl: suspend () -> Unit,
content: @Composable () -> Unit) {
LaunchedEffect(Unit) {
impl()
}
content()
}

上述代码中,在LaunchedEffect组合进入的时候就会调用impl挂起函数执行曝光逻辑,并且因为传入的key是Unit,因此就算多次Recompose,这里代码也只会执行一次。再修改一下:

1
2
3
4
5
6
7
8
9
@Composable
private inline fun Report2(reportParams: Map<String, Any?>,
noinline impl: suspend () -> Unit,
content: @Composable () -> Unit) {
LaunchedEffect(reportParams) {
impl()
}
content()
}

上述代码中,只要上报参数发生了改变,当Recompose的时候LaunchedEffect里面的代码就会重新走一次

3、rememberCoroutineScope

上面说的LaunchedEffect是一个组合函数,只能在组合函数里面调用,那如果想在非组合函数作用域里面运行挂起函数,比如onClick里面运行挂起函数,就可以使用rememberCoroutineScope,并且这个scope也是会在组合退出的时候自动取消协程:

1
2
3
4
5
6
7
8
9
10
11
@Composable
private inline fun Report3(noinline impl: suspend () -> Unit,
content: @Composable () -> Unit) {
val scope = rememberCoroutineScope()
Text(text = "hello", modifier = Modifier.clickable {
scope.launch {
impl()
}
})
content()
}

4、DisposableEffect

前面说的LaunchedEffect是说在进入组合的时候执行的操作,那如果我们想在退出组合的时候做一些逻辑,就可以使用DisposableEffect了。还是以前面的曝光为例子,使用LaunchedEffect可以在组合进入的时候进行曝光,如果想同时在退出组合的时候进行反曝光,就可以使用DisposableEffect:

1
2
3
4
5
6
7
8
9
10
11
12
@Composable
private inline fun Report4(noinline onImpl: () -> Unit,
noinline onImplEnd: () -> Unit,
content: @Composable () -> Unit) {
DisposableEffect(Unit) {
onImpl()
onDispose {
onImplEnd()
}
}
content()
}

以上代码使用DisposableEffect就可以同时做到曝光和反曝光

5、rememberUpdatedState

对上面的DisposableEffect进行改造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Composable
private inline fun Report5(noinline onImpl: () -> Unit,
noinline onImplEnd: () -> Unit,
content: @Composable () -> Unit) {
val scope = rememberCoroutineScope()
DisposableEffect(Unit) {
scope.launch {
delay(50)
onImpl()
}
onDispose {
onImplEnd()
}
}
content()
}

在DisposableEffect里面,先delay一段时间,再执行曝光上报,因为onImpl上报接口是通过参数传入的,如果在这个delay期间重组更改了onImpl参数,但是DisposableEffectt(Unit)因为不会重新执行,因此真正执行onImpl()的时候就会一直是最开始的那个onImpl接口!对应的onImplEnd()也是有同样的问题。那这个问题怎么解决呢?又不能重启DisposableEffect。针对这个问题,官方给了一个API:rememberUpdatedState,改造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Composable
private inline fun Report6(noinline onImpl: () -> Unit,
noinline onImplEnd: () -> Unit,
content: @Composable () -> Unit) {
// 使用rememberUpdatedState保证在不重启DisposableEffect情况下使用最新的参数
val currentOnImpl by rememberUpdatedState(onImpl)
val currentOnImplEnd by rememberUpdatedState(onImplEnd)
val scope = rememberCoroutineScope()
DisposableEffect(Unit) {
scope.launch {
delay(50)
currentOnImpl()
}
onDispose {
currentOnImplEnd()
}
}
content()
}

使用rememberUpdatedState对传入的参数进行包裹,这样当真正执行impl和implEnd的时候就能使用最新传入的参数!

6、SideEffect

根据官方文档说明:

1
2
3
4
5
6
7
8
9
10
Schedule [effect] to run when the current composition completes successfully and applies changes

@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun SideEffect(
effect: () -> Unit
) {
currentComposer.recordSideEffect(effect)
}

意思是需要在每次重组成功之后都执行的逻辑,就可以使用SideEffect,并且因为这个effect: () -> Unit 并不是一个组合函数,因此当需要在组合函数里面运行非组合函数,也可以使用这个SideEffect,相当于把Compose转化为非Compose代码,比如看官方给的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
/* ... */
}

// 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
}

6、总结

以上就是Compose Side Effect 相关API的使用介绍,我们可以根据具体使用场景使用对应的API来处理Side Effect