虽然我使用Compose已经有了一段时间的, 但我还是觉得使用起来束手束脚的. 究其原因, 大概是coding时的思路还没有完全转换过来, 还没有沉浸在”Compose is Function”之中. 和我们熟悉的View不同, 当我们调用Compose之后, 我们就失去了它的修改器, 而Compose也只能按照我们之前设计好的功能去响应我们的操作.

除此之外阶段(Phases), 也是一个可以使得你的Compose变的得心应手的入口, 虽然这篇文章不会进行相关介绍, 但我也会在后续的文章中进行介绍.

在了解Side Effects之前, 我们需要先简单了解一下Lifecycle of composables

Lifecycle of composables

相信大家对Lifecycle都十分的熟悉, 在我们的Android项目开发时, Activity及Fragment的Lifecycle对我们的功能实现提供了极大的帮助. 通过Lifecyle我们可以很便捷的处理页面不同时期的状态. 想象一下, 如果将Compose变为一个Activity, 那么许多的功能我们都可以通过Lifecycle来完成. 当然, Compose的Lifecyle并不如Activity丰富, 而且设计的思路也不尽相同, 但这都不能阻止Compose的Lifecycle来帮助我们控制Compose.

Lifecycle of composables

先看一下官方提供的Lifecycle of composables说明, 简单来说, Lifecycle of composables包含以下三个部分:

  • 进入组合(创建)
  • 执行 0 次或多次重组(重组)
  • 退出组合(销毁)

如果想要了解Side Effects的话, 对于Lifecycle of composables了解到这些就足够了.(写完文章再看这里, 不, 完全不够.)

和Activity及Fragment的Lifecycle不同的是, Side Effects并没有Lifecycle这么的泾渭分明, 很多的功能可以用不同的Side Effects来实现, 也使得Side Effects用起来既顺手又疑惑.(当然, 这都是现阶段大家都还在探索中的情况)

下面就让我们来看一看Side Effects到底是什么, 又有着什么样的功能.

Side Effects

Side Effects有时被译为”副作用”(如果你打开官方文档, 你会发现左侧的列表中还是副作用), 当然, 大部分情况下都被翻译为”附带效应.”(个人认为附带效应是更加准确的, 毕竟如果是副作用的话, 那说明其作用都是不应该出现的, 但是作为附带效应的话, 其作用是否应该出现, 取决于我们如何去操作).

借由官方文档对其的定义和说明.可以看到以下两个需要关注的地方.

一是附带效应”应从可组合项生命周期的受控环境中调用”, 也就是Lifecycle of composables中调用. 这也是需要先了解Lifecycle of composables的原因.

二是应该”以可预测的方式执行这些附带效应”, 这是不应该翻译为副作用的原因, Side Effects的操作应该是可控的, 有效的.

可能在你的了解或实践的过程中, 你会不止一次的疑惑, “这个功能我为什么不能直接在OnClick中完成? 他们之中有什么区别?” 这也是我在整个过程中经常疑惑的地方, 答案也是很简单, 所有的Side Effects都是处理果的动作, 它们都是由其它因引起的,

说了这么多的定义, 不如来看看这些Side Effects都是如何通过可组合项生命周期的受控环境中调用来达到以可预测的方式执行这些附带效应并帮助我们的Compose变得得心应手.

LaunchedEffect 在某个可组合项的作用域内运行挂起函数

“如需从可组合项内安全调用挂起函数,请使用 LaunchedEffect 可组合项。当 LaunchedEffect 进入组合时,它会启动一个协程,并将代码块作为参数传递。如果 LaunchedEffect 退出组合,协程将取消。如果使用不同的键重组 LaunchedEffect(请参阅下方的重启效应部分),系统将取消现有协程,并在新的协程中启动新的挂起函数。”

可以看到LaunchedEffect具有以下特点:

  • 运行suspend functions
  • 进入组合时候执行
  • 退出组合时候取消
  • 具有重启效应

运行suspend functions

关于suspend functions, 相信大家都还是十分熟悉的, 其中一个十分重要的特点就是, 只能通过suspend functions来调用suspend functions. 下面我们尝试下在不同的位置调用suspend functions的效果.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//https://gist.github.com/clwater/5454deed3ae258ba3980d260a0ff3299
suspend fun suspendFunTest() {
Log.d("clwater", "suspendFunTest Start")
delay(3000)
Log.d("clwater", "suspendFunTest Finish")
}

@Composable
fun TestLifecycleCompose() {
LaunchedEffect(Unit) {
// 1️⃣ Success
suspendFunTest()
}
// 2️⃣ Error: Suspend function 'suspendFunTest' should be called only from a coroutine or another suspend function
suspendFunTest()
Button(
onClick = {
// 3️⃣ Error: Suspend function 'suspendFunTest' should be called only from a coroutine or another suspend function
suspendFunTest()
}) {
Text(text = "suspendFunTest")
}
}

我们可以看到以上三个位置仅有1️⃣的位置是有效的, 放到2️⃣和3️⃣都会报错. 很明显2️⃣和3️⃣的位置都不能调用suspend functions.

进入组合时候执行

想到我们前面提到的Lifecycle of composables, 我们就可以感知到我们的Composeables进入组合的事件了!

1
2
3
4
5
6
7
8
//https://gist.github.com/clwater/4b9b1bedca4365732de7d8e2c8519190
@Composable
fun TestLifecycleCompose() {
LaunchedEffect(Unit) {
Log.d("clwater", "TestLifecycleCompose Enter")
}
Text(text = "TestLifecycleCompose")
}

当我们调用这个Composeables时, 我们就可以看到以下的log信息了

1
2023-05-18 16:10:27.432 30584-30584 clwater                 com.clwater.compose_learn_1       D  TestLifecycleCompose Enter

退出组合时候取消

我们知道suspend functions都是可以取消的, 同样LaunchedEffect既可以感知Composeables进入的事件, 也会在Composeables离开的时候取消.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// https://gist.github.com/clwater/440af58717e699d4963f915f520d494a
@Composable
fun TestLifecycleCompose() {
var isShow by remember {
mutableStateOf(true)
}

Column {
if (isShow) {
TestLifecycleComposeText()
}

Button(
onClick = { isShow = !isShow }
) {
Text(text = "TestLifecycleCompose show: $isShow")
}
}
}

@Composable
fun TestLifecycleComposeText() {
LaunchedEffect(Unit) {
Log.d("clwater", "TestLifecycleCompose Enter")
try {
delay(10 * 1000)
Log.d("clwater", "TestLifecycleCompose Finish")
} catch (e: Exception) {
Log.d("clwater", "TestLifecycleCompose Error: $e")
}
}
Text(text = "TestLifecycleCompose")
}

如果我们启动后不做任何操作, 或者超过10s后再次点击, 我们会看到如下的log

1
2
3
2023-05-18 16:15:27.449 30584-30584 clwater                 com.clwater.compose_learn_1          D  TestLifecycleCompose Enter
2023-05-18 16:15:37.453 30584-30584 clwater com.clwater.compose_learn_1 D TestLifecycleCompose Finish

但是当我们在10s再次点击按钮使得上面的Composeables不在显示的时候, 我们就可以看到出现了以下的log

1
2
2023-05-18 16:22:03.698 31930-31930 clwater                 com.clwater.compose_learn_1          D  TestLifecycleCompose Enter
2023-05-18 16:22:04.895 31930-31930 clwater com.clwater.compose_learn_1 D TestLifecycleCompose Error: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@67197cb

我们可以很清除的看到我们在LaunchedEffect运行的内容, 因为对应Composeables离开而被取消.

重启效应

“Compose 中有一些效应(如 LaunchedEffect、produceState 或 DisposableEffect)会采用可变数量的参数和键来取消运行效应,并使用新的键启动一个新的效应。”

重启效应不单单只有LaunchedEffect具有, 但是相关的效果都表现一致, 这里我们针对LaunchedEffect的重启效应进行详细分析.

值得注意的是, 当我们通过重启效应来启动新的效应的时候, 我们旧的效应(同一个键)会被取消. 类似前面退出组合时候取消的效果.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Composable
fun TestLifecycleCompose() {
var clickCount by remember {
mutableStateOf(0)
}

LaunchedEffect(clickCount) {
Log.d("clwater", "TestLifecycleCompose clickCount: $clickCount")
}

Column {
Button(onClick = { clickCount++ }) {
Text("clickCount $clickCount")
}
}
}

当我们点击按钮的时候, 我们可以在日志中看到如下的输出.当然, 你可以通过在onClick中打印这些内容来实现同样的功能. 不过这两种实现的方式侧重点是不同的. 在onClick中, 你侧重的内容是点击按钮, 而在LaunchedEffect中, 你侧重的是clickCount值的变化.

1
2
3
4
5
6
2023-05-19 10:21:18.864 30841-30841 clwater                 com.clwater.compose_learn_1          D  TestLifecycleCompose clickCount: 0
2023-05-19 10:21:26.114 30841-30841 clwater com.clwater.compose_learn_1 D TestLifecycleCompose clickCount: 1
2023-05-19 10:21:26.387 30841-30841 clwater com.clwater.compose_learn_1 D TestLifecycleCompose clickCount: 2
2023-05-19 10:21:26.594 30841-30841 clwater com.clwater.compose_learn_1 D TestLifecycleCompose clickCount: 3
2023-05-19 10:21:26.806 30841-30841 clwater com.clwater.compose_learn_1 D TestLifecycleCompose clickCount: 4
2023-05-19 10:21:27.005 30841-30841 clwater com.clwater.compose_learn_1 D TestLifecycleCompose clickCount: 5

简单的, 如果我们增加对取消时的异常捕获, 我们就能看到下面类似的log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Composable
fun TestLifecycleCompose() {
var clickCount by remember {
mutableStateOf(0)
}

LaunchedEffect(clickCount) {
try {
Log.d("clwater", "TestLifecycleCompose clickCount: $clickCount")
delay(1 * 1000)
Log.d("clwater", "TestLifecycleCompose clickCount: $clickCount finish")
} catch (e: Exception) {
Log.d("clwater", "TestLifecycleCompose Error: $e")
}
}

Column {
Button(onClick = { clickCount++ }) {
Text("clickCount $clickCount")
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2023-06-05 14:30:38.948 17377-17377 clwater                 com.clwater.compose_learn_1          D  TestLifecycleCompose clickCount: 0
2023-06-05 14:30:39.951 17377-17377 clwater com.clwater.compose_learn_1 D TestLifecycleCompose clickCount: 0 finish
2023-06-05 14:30:46.362 17377-17377 clwater com.clwater.compose_learn_1 D TestLifecycleCompose clickCount: 1
2023-06-05 14:30:47.363 17377-17377 clwater com.clwater.compose_learn_1 D TestLifecycleCompose clickCount: 1 finish
2023-06-05 14:30:47.432 17377-17377 clwater com.clwater.compose_learn_1 D TestLifecycleCompose clickCount: 2
2023-06-05 14:30:47.597 17377-17377 clwater com.clwater.compose_learn_1 D TestLifecycleCompose Error: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@ac45c38
2023-06-05 14:30:47.597 17377-17377 clwater com.clwater.compose_learn_1 D TestLifecycleCompose clickCount: 3
2023-06-05 14:30:47.776 17377-17377 clwater com.clwater.compose_learn_1 D TestLifecycleCompose Error: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@ed9b9fe
2023-06-05 14:30:47.776 17377-17377 clwater com.clwater.compose_learn_1 D TestLifecycleCompose clickCount: 4
2023-06-05 14:30:47.965 17377-17377 clwater com.clwater.compose_learn_1 D TestLifecycleCompose Error: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@d0a879d
2023-06-05 14:30:47.965 17377-17377 clwater com.clwater.compose_learn_1 D TestLifecycleCompose clickCount: 5
2023-06-05 14:30:48.968 17377-17377 clwater com.clwater.compose_learn_1 D TestLifecycleCompose clickCount: 5 finish

rememberCoroutineScope:获取组合感知作用域,以便在可组合项外启动协程

“由于 LaunchedEffect 是可组合函数,因此只能在其他可组合函数中使用。为了在可组合项外启动协程,但存在作用域限制,以便协程在退出组合后自动取消,请使用 rememberCoroutineScope。 此外,如果您需要手动控制一个或多个协程的生命周期,请使用 rememberCoroutineScope,例如在用户事件发生时取消动画。

rememberCoroutineScope 是一个可组合函数,会返回一个 CoroutineScope,该 CoroutineScope 绑定到调用它的组合点。调用退出组合后,作用域将取消。”

可以看到rememberCoroutineScope具有以下特点:

  • 在composable之外启动协程
  • 可手动控制一个或多个协程的生命周期
  • 是一个composable function

在composable之外启动协程

最核心的作用就是这个, 在composable之外启动协程, 我们还是尝试在不同的部分来启动一个suspend functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Composable
fun TestLifecycleCompose() {
// 1️⃣ Error: Suspend function 'suspendFunTest' should be called only from a coroutine or another suspend function
suspendFunTest()

val scope = rememberCoroutineScope()

Button(onClick = {
// 2️⃣ Error: Suspend function 'suspendFunTest' should be called only from a coroutine or another suspend function
suspendFunTest()

scope.launch {
// 3️⃣ Success
suspendFunTest()
}
}) {
Text(text = "suspendFunTest")
}
}

和前面的例子类似的, 在位置1️⃣和2️⃣都会报错, 仅在3️⃣的位置才能正常使用. 当我通过手动触发某些协程的时候, 这个方法就变得十分的好用. 比如在官方说明中点击某个按钮后再Scaffold中调用showSnackbar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {

// Creates a CoroutineScope bound to the MoviesScreen's lifecycle
val scope = rememberCoroutineScope()

Scaffold(scaffoldState = scaffoldState) {
Column {
/* ... */
Button(
onClick = {
// Create a new coroutine in the event handler to show a snackbar
scope.launch {
scaffoldState.snackbarHostState.showSnackbar("Something happened!")
}
}
) {
Text("Press me")
}
}
}
}

可手动控制一个或多个协程的生命周期/是一个composable function

前言中我们有提及, 不论是哪种Side Effects, 都是某种来处理果的过程, 但是实际的开发过程中, 我们不可避免的需要更加精细的对协程进行控制, 虽然前面提及到的点有两个, 但是我认为这两个放在一个例子中可以更好的帮助大家进行理解.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@Composable
fun TestLifecycleCompose() {
var showChild by remember {
mutableStateOf(true)
}

var showParent by remember {
mutableStateOf(true)
}

val scope = rememberCoroutineScope()

Column {
Row {
Button(onClick = {
showParent = false
}) {
Text(text = "Hide Parent")
}

Button(onClick = {
showChild = false
}) {
Text(text = "Hide Child")
}
}

if (showParent) {
Button(onClick = {
scope.launch {
try {
Log.d("clwater", "TestLifecycleCompose")
delay(1000 * 1000)
} catch (e: Exception) {
Log.d("clwater", "TestLifecycleCompose Error: $e")
}
}
}) {
Text(text = "Parent")
}
}

if (showChild) {
TestLifecycleComposeChild()
}
}
}

@Composable
fun TestLifecycleComposeChild() {
val scope = rememberCoroutineScope()

Button(onClick = {
scope.launch {
try {
Log.d("clwater", "TestLifecycleComposeChild")
delay(1000 * 1000)
} catch (e: Exception) {
Log.d("clwater", "TestLifecycleComposeChild Error: $e")
}
}
}) {
Text(text = "Child")
}
}

通过log我们可以看到, 不论先关闭哪一个按钮, log的结果都是一样的(子composables被取消), 因为rememberCoroutineScope返回的CoroutineScope被绑定到调用它的组合点, 所以虽然看起来TestLifecycleCompose没有内容被显示, 但是这个function还没有退出, 所以调用的协程还一直在执行, 而TestLifecycleComposeChild却完完全全的被执行退出, 所以绑定在TestLifecycleComposeChild的scope就会被取消.

1
2
3
4
5
6
7
8
9
10
2023-06-05 16:31:28.529 22058-22058 clwater                 com.clwater.compose_learn_1          D  TestLifecycleCompose
2023-06-05 16:31:29.341 22058-22058 clwater com.clwater.compose_learn_1 D TestLifecycleComposeChild
2023-06-05 16:31:34.309 22058-22058 clwater com.clwater.compose_learn_1 D TestLifecycleComposeChild Error: kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@2fc6b1d


2023-06-05 16:31:53.457 22151-22151 clwater com.clwater.compose_learn_1 D TestLifecycleCompose
2023-06-05 16:31:53.936 22151-22151 clwater com.clwater.compose_learn_1 D TestLifecycleComposeChild
2023-06-05 16:31:58.985 22151-22151 clwater com.clwater.compose_learn_1 D TestLifecycleComposeChild Error: kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@8d47f01


rememberUpdatedState:在效应中引用某个值,该效应在值改变时不应重启

“当其中一个键参数发生变化时,LaunchedEffect 会重启。不过,在某些情况下,您可能希望在效应中捕获某个值,但如果该值发生变化,您不希望效应重启。为此,需要使用 rememberUpdatedState 来创建对可捕获和更新的该值的引用。这种方法对于包含长期操作的效应十分有用,因为重新创建和重启这些操作可能代价高昂或令人望而却步。”

说实话, 第一次看到这个解释的时候我感觉我更加的不理解了, 不过从官方的介绍中, 我们可以看到rememberUpdatedState解决的主要问题是在效应中捕获某个值,不希望效应重启.

这里的我们之间看一下示例代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
https://gist.github.com/clwater/214e28128f93d8e491afd189618fea36
@Composable
fun DelayCompose(click: Int) {
val rememberClick = rememberUpdatedState(newValue = click)
LaunchedEffect(Unit) {
delay(5000)
Log.d("clwater", "TestLifecycleCompose click: $click")
Log.d("clwater", "TestLifecycleCompose rememberClick: ${rememberClick.value}")
}
}

@Composable
fun TestLifecycleCompose() {
var lastClick by remember {
mutableStateOf(-1)
}

Column {
Button(onClick = {
Log.d("clwater", "onClick 0")
lastClick = 0
}) {
Text(text = "0")
}

Button(onClick = {
Log.d("clwater", "onClick 1")
lastClick = 1
}) {
Text(text = "1")
}
}

DelayCompose(click = lastClick)
}

代码还是比较简单的, 我们分别尝试先点击两次”按钮0”, 后点击两次”按钮1”, 已经先点击两次”按钮1”, 后点击两次”按钮0”. 我们先预测下最后的log中会是什么样子的, 再来看看实际上log的情况.

1
2
3
4
5
6
2023-06-07 13:23:21.551  6400-6400  clwater                 com.clwater.compose_learn_1          D  onClick 0
2023-06-07 13:23:21.826 6400-6400 clwater com.clwater.compose_learn_1 D onClick 0
2023-06-07 13:23:22.314 6400-6400 clwater com.clwater.compose_learn_1 D onClick 1
2023-06-07 13:23:22.649 6400-6400 clwater com.clwater.compose_learn_1 D onClick 1
2023-06-07 13:23:24.415 6400-6400 clwater com.clwater.compose_learn_1 D TestLifecycleCompose click: -1
2023-06-07 13:23:24.415 6400-6400 clwater com.clwater.compose_learn_1 D TestLifecycleCompose rememberClick: 1
1
2
3
4
5
6
2023-06-07 13:23:42.094  6711-6711  clwater                 com.clwater.compose_learn_1          D  onClick 1
2023-06-07 13:23:42.462 6711-6711 clwater com.clwater.compose_learn_1 D onClick 1
2023-06-07 13:23:42.960 6711-6711 clwater com.clwater.compose_learn_1 D onClick 0
2023-06-07 13:23:43.198 6711-6711 clwater com.clwater.compose_learn_1 D onClick 0
2023-06-07 13:23:46.236 6711-6711 clwater com.clwater.compose_learn_1 D TestLifecycleCompose click: -1
2023-06-07 13:23:46.236 6711-6711 clwater com.clwater.compose_learn_1 D TestLifecycleCompose rememberClick: 0

我们可以看到, 没有通过rememberUpdatedState获得值是默认值, 也可以理解为我们首次调用时传入的值, 即使DelayCompose方法没有做任何修饰, 并且入参的内容都不一样. (个人理解是由于Compose对重启的优化, 避免页面被重新绘制多次没有变化的元素), 实际上我们的DelayCompose实际上只执行了一次(你可以在DelayCompose的LaunchedEffect中添加Log来认证一下).

再回来看我们的代码, DelayCompose这个方法实际上在-1入参后就进入了延时, 并不对新的入参做反应了, 这也就导致了我们Log中输出入参信息只有-1, 但通过rememberUpdatedState获取的值, 他会在使用的时候更新当前新的值, 也是的我们在Log中可以看到我们实际点击的情况.

DisposableEffect:需要清理的效应

“对于需要在键发生变化或可组合项退出组合后进行清理的附带效应,请使用 DisposableEffect。如果 DisposableEffect 键发生变化,可组合项需要处理(执行清理操作)其当前效应,并通过再次调用效应进行重置。”

简单来说, 当我们退出组合时会触发DisposableEffect, 我们可以在这里解绑或注销一些资源. 需要注意的是在DisposeableEffect中是无法直接调用suspend functions的, 也就是说DisoposeableEffect并不属于Compose state. 使用起来的话代码也比较简单.

1
2
3
4
5
6
7
8
9
10
fun TestLifecycleCompose(obsever: TestObserver) {
LaunchedEffect(Unit){
obsever.start()
}
DisposableEffect(Unit) {
onDispose {
obsever.stop()
}
}
}

上述只是一个最简单的使用, 同时我们主要到, DisposeableEffect也有一个key1, 那么它和LaunchedEffect直接的区别是什么? 还是说DisposeableEffect只是LaunchedEffect加一个onDispose并且不能调用suspend functions的修改版本?

我们试一下以下代码, 并在屏幕中点击按钮, 我们不妨先想象一下最终的Log是什么样子的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Composable
fun ControlCompose() {
LaunchedEffect(Unit) {
Log.d("clwater", "LaunchedEffect(Unit)")
}
DisposableEffect(Unit) {
Log.d("clwater", "DisposableEffect(Unit) out onDispose")
onDispose {
Log.d("clwater", "DisposableEffect(Unit) in onDispose")
}
}

var count by remember {
mutableStateOf(0)
}

Button(onClick = { count++ }) {
Text(text = "count $count")
}

LaunchedEffect(count) {
Log.d("clwater", "LaunchedEffect(count)")
}
DisposableEffect(count) {
Log.d("clwater", "DisposableEffect(count) out onDispose")
onDispose {
Log.d("clwater", "DisposableEffect(count) in onDispose")
}
}
}

@Composable
fun TestLifecycleCompose() {
var show by remember {
mutableStateOf(true)
}

Column {
Button(onClick = {
show = false
}) {
Text(text = "Hide")
}

if (show) {
ControlCompose()
}
}
}

Log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2023-06-07 14:12:20.443 21111-21111 clwater                 com.clwater.compose_learn_1          D  DisposableEffect(Unit) out onDispose
2023-06-07 14:12:20.443 21111-21111 clwater com.clwater.compose_learn_1 D DisposableEffect(count) out onDispose
2023-06-07 14:12:20.503 21111-21111 clwater com.clwater.compose_learn_1 D LaunchedEffect(Unit)
2023-06-07 14:12:20.504 21111-21111 clwater com.clwater.compose_learn_1 D LaunchedEffect(count)


2023-06-07 14:12:27.923 21111-21111 clwater com.clwater.compose_learn_1 D DisposableEffect(count) in onDispose
2023-06-07 14:12:27.923 21111-21111 clwater com.clwater.compose_learn_1 D DisposableEffect(count) out onDispose
2023-06-07 14:12:27.929 21111-21111 clwater com.clwater.compose_learn_1 D LaunchedEffect(count)
2023-06-07 14:12:28.309 21111-21111 clwater com.clwater.compose_learn_1 D DisposableEffect(count) in onDispose
2023-06-07 14:12:28.310 21111-21111 clwater com.clwater.compose_learn_1 D DisposableEffect(count) out onDispose
2023-06-07 14:12:28.314 21111-21111 clwater com.clwater.compose_learn_1 D LaunchedEffect(count)
2023-06-07 14:12:28.608 21111-21111 clwater com.clwater.compose_learn_1 D DisposableEffect(count) in onDispose
2023-06-07 14:12:28.609 21111-21111 clwater com.clwater.compose_learn_1 D DisposableEffect(count) out onDispose
2023-06-07 14:12:28.614 21111-21111 clwater com.clwater.compose_learn_1 D LaunchedEffect(count)


2023-06-07 14:12:32.043 21111-21111 clwater com.clwater.compose_learn_1 D DisposableEffect(count) in onDispose
2023-06-07 14:12:32.044 21111-21111 clwater com.clwater.compose_learn_1 D DisposableEffect(Unit) in onDispose

通过Log我们可以看出, DisposableEffect和LaunchedEffect一样, 在监听统一个值的变化的时候表现基本一致. 但是DisposableEffect却优先于LaunchedEffect触发(这里的话只能通过Log得到此结论, 得到这个结论的时候我也有点疑惑, 可能是Compose state导致性能开销大所以总慢一点? 查找相关文章的时候也没有提及的相关内容.)

当然, 我们还可以看到LaunchedEffect(Unit)只在进入组合时触发, DisposableEffect(Unit)中的onDispose 也只在退出组合时触发. 如果你想监听组件的Lifecycle, 不妨通过这两个位置来实现.

SideEffect:将 Compose 状态发布为非 Compose 代码

“如需与非 Compose 管理的对象共享 Compose 状态,请使用 SideEffect 可组合项,因为每次成功重组时都会调用该可组合项。”

关于SideEffect, 可以将其理解为非Compose代码的LaunchedEffect(Unit), 而且其在每次重组成功时调用.

其理解和实际使用都有点困难, 虽然其一般被建议在来组合生命周期无关功能中使用. 但是也没有发现在这种情况下的不可替代性.

不过, 从如需与非 Compose 管理的对象共享 Compose 状态这里, 我们可以将其理解为可以将SideEffect{}内的元素/功能/代码变为当前作用域下重组的参考.

我们先来看一下以下的代码

1
2
3
4
5
6
7
@Composable
fun TestLifecycleCompose() {
var text by remember { mutableStateOf("Common") }
Text(text = "text $text")
Thread.sleep(3 * 1000)
text = "Delay text"
}

你认为, 3秒后Text中的内容会变化么? 实际上不会的.

但是当我们加入SideEffect之后

1
2
3
4
5
6
7
8
9
@Composable
fun TestLifecycleCompose() {
var text by remember { mutableStateOf("Common") }
Text(text = "text $text")
SideEffect {
Thread.sleep(3 * 1000)
text = "Delay text"
}
}

我们可以发现其在3s后, Text中的内容后发生变化.

(关于发生这样区别的原因, 以下均为我个人理解与想法, 重组这个动作更注重与对观察变量”读”的变化, 没有 SideEffect的时候, Text”读””text”的动作没有变化, 所以不发生重组. 而加入SideEffect后, 将”text”的中”读”的操作进行了触发, 最终引起了重组)

同样的, 我们在官网中还可以看到

  • produceState:将非 Compose 状态转换为 Compose 状态
  • derivedStateOf:将一个或多个状态对象转换为其他状态
  • snapshotFlow:将 Compose 的 State 转换为 Flow

篇幅有限, 加上学艺不精, 这次就不能过多的介绍了.

后记

其实在写这篇的文章之前, 我已经把相关的官方文章查看多遍, 还写了一些Demo来帮助理解和写文.

不过在实际完成的过程中, 遇到的问题越来越多, 想展现给大家的也越来越多, 以至于远超预计的内容. 本以为写完此文后, 对于Side Effects不能说是登堂入室, 但也能说的上是略有小成. 但现实确实我发现我的疑问更多了. SideEffect官方文档中是重组时触发, 那么其和LaunchedEffect和DisposableEffect直接的前后关系是怎么样, 它们三个直接这个顺序的原因又到底是为什么? rememberUpdatedState是否不可替代(如果你尝试不在子Compose中处理, 完全可以不使用rememberUpdatedState而达到相同的效果)? 如果不是的话, 其不可替代的使用场景应用是什么样子?

同时关于Lifecycle of composables重组相关的内容, 在这里也有了更多的问题. 真可谓牵一发而动全身啊.

也希望后面可以把对这些问题的理解分析给大家.

相关参考文章

官方文档 Lifecycle:https://developer.android.com/jetpack/compose/lifecycle, 写这篇文章的时候这里的地址官方竟然进行了修改

官方文档 Side Effects:https://developer.android.com/jetpack/compose/side-effects

https://blog.appcircle.io/article/jetpack-compose-side-effects-with-examples

https://medium.com/@callmeryan/jetpack-compose-effect-apis-f572712004a4

https://medium.com/mobile-app-development-publication/jetpack-compose-side-effects-made-easy-a4867f876928