2024年HarmonyOS鸿蒙最新如何通过Side Effects来使得你使用Compose变的得心应手 (1),面试复盘怎么复盘

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

@Composable
fun TestLifecycleCompose() {

img
img

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

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

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

onDispose {
Log.d(“clwater”, “DisposableEffect(count) in onDispose”)
}
}
}

@Composable
fun TestLifecycleCompose() {

[外链图片转存中…(img-Cr1UptNz-1715635921162)]
[外链图片转存中…(img-S1GJ0LZH-1715635921163)]

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值