2024年鸿蒙最全如何通过Side Effects来使得你使用Compose变的得心应手 (2),2024年最新15个经典面试问题薪资

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

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

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的重启效应进行详细分析.

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

    @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值的变化.

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

    @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")
            }
        }
    }


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.

    @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

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

    @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就会被取消.

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解决的主要问题是在效应中捕获某个值,不希望效应重启.

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

    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的情况.

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


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. 使用起来的话代码也比较简单.

    fun TestLifecycleCompose(obsever: TestObserver) {
        LaunchedEffect(Unit){
            obsever.start()
        }
        DisposableEffect(Unit) {
            onDispose {
                obsever.stop()
            }
        }
    }

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

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

    @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")
            }
        }


![img](https://img-blog.csdnimg.cn/img_convert/93cbfe5ce28063b1b4138c78067d7dd2.png)
![img](https://img-blog.csdnimg.cn/img_convert/db9682369692a4695c1674367489d3b4.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618636735)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

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


[外链图片转存中...(img-0pfihzXM-1715731406605)]
[外链图片转存中...(img-XWyig43C-1715731406605)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618636735)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 48
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值