Jetpack Compose | 可组合项生命周期及其常见Effect副作用API

可组合项生命周期

上图描述了组合中可组合项的生命周期:进入组合,执行 0 次或多次重组,然后退出组合。在可组合项执行过程中,影响操作结果的可以认为是一种副作用,如在Composable中执行网络请求、Dialog弹窗、弹Toast、页面跳转等,不管重组多少次,有些操作只需要执行一次即可,如果多次执行就会出现意想不到的结果。针对上述情况,Compose 有一系列专门的副作用 API 来处理。

LaunchedEffect

LaunchedEffect 可以在可组合项的作用域内开启协程,如果副作用中需要处理异步相关的任务,就可以选择LaunchedEffect。

kotlin 复制代码
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1) { LaunchedEffectImpl(applyContext, block) }
}

LaunchedEffect 进入可组合项时,它会启动一个协程。当LaunchedEffect 退出组合时,协程也将会取消。LaunchedEffect() 中可以传入一个key,如果重组时key发生改变,现有协程会被取消,并在新的协程中启动新的挂起函数。

注:如果是LaunchedEffect(true) 或 LaunchedEffect(Unit), 可以保证LaunchedEffect()中的key永远不发生变化,从而保证后面的lambda不参与重组。

使用示例:

kotlin 复制代码
@Composable
fun MyScreen(
    state: UiState<List<Movie>>,
    scaffoldState: ScaffoldState = rememberScaffoldState()
) {

    // 第一次组合时如果state.hasError为true,那么在LaunchedEffect开启协程并显示Snackbar;当重组state.hasError变为false后,协程结束,Snackbar也会消失
    if (state.hasError) {

        // 如果重组时scaffoldState.snackbarHostState发生变化,协程会被取消并重新创建并执行
        LaunchedEffect(scaffoldState.snackbarHostState) {
            // 展示Snackbar
            scaffoldState.snackbarHostState.showSnackbar(
                message = "Error message",
                actionLabel = "Retry message"
            )
        }
    }

    Scaffold(scaffoldState = scaffoldState) {
        /* ... */
    }
}

state.hasError状态为 true时,会触发LaunchedEffect() 通过协程处理副作用;如果重组时state.hasError变成 false,协程会被取消,Snackbar也会消失。

rememberCoroutineScope()

LaunchedEffect 是由@Composable 标记的可组合函数,因此只能在其他可组合函数中使用(类似协程中的suspend)。当需要在可组合项外启动协程时(如onClick中),可以使用rememberCoroutineScope ,源码如下:

kotlin 复制代码
@Composable
inline fun rememberCoroutineScope(
    getContext: @DisallowComposableCalls () -> CoroutineContext = { EmptyCoroutineContext }
): CoroutineScope {
    val composer = currentComposer
    val wrapper = remember {
        CompositionScopedCoroutineScopeCanceller(
            createCompositionCoroutineScope(getContext(), composer)
        )
    }
    return wrapper.coroutineScope
}

rememberCoroutineScope() 返回 CoroutineScope 协程作用域,可以在可组合项外启动协程,该 CoroutineScope 绑定到调用它的组合点。调用退出组合后,作用域将取消

使用示例:

kotlin 复制代码
@Composable
fun ComposeEffect() {
    var txt by remember { mutableStateOf("") }
    //rememberCoroutineScope在非重组作用域启动协程任务
    val coroutineScope = rememberCoroutineScope()

    Column(
        modifier = Modifier.fillMaxSize()wrapContentSize(Alignment.Center)
    ) {
        Text(text = txt)
        Button(onClick = {
            //在onClick中启动了协程
            coroutineScope.launch {
                delay(3000)
                txt = "修改文案"
            }
        }) {
            Text(text = "rememberCoroutineScope")
        }
    }
}

点击Button后,启动协程,在协程中延迟3s重组并刷新Text文案。

DisposableEffect

kotlin 复制代码
@Composable
@NonRestartableComposable
fun DisposableEffect(
    key1: Any?,
    effect: DisposableEffectScope.() -> DisposableEffectResult
) {
    remember(key1) { DisposableEffectImpl(effect) }
}

对于需要在键发生变化或可组合项退出组合后进行清理的副作用,可以使用 DisposableEffect。如果 DisposableEffect 中的key发生变化,那么就会执行onDispose()资源释放等操作,并重新执行DisposableEffect。

DisposableEffect中强制以onDispose进行收尾,不加就会编译报错。因此DisposableEffect适合有资源需要收尾的场景下。

使用示例:

kotlin 复制代码
@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit, // Send the 'started' analytics event
    onStop: () -> Unit // Send the 'stopped' analytics event
) {
    // Safely update the current lambdas when a new one is provided
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    // If `lifecycleOwner` changes, dispose and reset the effect
    DisposableEffect(lifecycleOwner) {
        // Create an observer that triggers our remembered callbacks
        // for sending analytics events
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                currentOnStart()
            } else if (event == Lifecycle.Event.ON_STOP) {
                currentOnStop()
            }
        }

        // Add the observer to the lifecycle
        lifecycleOwner.lifecycle.addObserver(observer)

        // When the effect leaves the Composition, remove the observer
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    /* Home screen content */
}

LaunchedEffect 内部开启了协程,并在退出组合时取消协程,DisposableEffect + rememberCoroutineScope 的方式可以模拟 LaunchedEffect,如下:

scss 复制代码
//LaunchedEffect
LaunchedEffect(key1 = Unit) {
}

//DisposableEffect + rememberCoroutineScope
val scope = rememberCoroutineScope()
DisposableEffect(key1 = Unit) {
    val job = scope.launch { }
    onDispose {
        job.cancel()
    }
}

rememberUpdatedState

LaunchedEffect(key) 会在key发生变化时会重启协程,如果不希望key变化时协程中断而是继续执行,可以通过 rememberUpdatedState 获取最新状态即可。

kotlin 复制代码
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
    mutableStateOf(newValue)
}.apply { value = newValue }

//MutableState
@Stable
interface MutableState<T> : State<T> {
    override var value: T //rememberUpdatedState每次重组时都会对该值进行更新
    operator fun component1(): T
    operator fun component2(): (T) -> Unit
}

rememberUpdatedState 源码很简单,内部通过 remember + mutableStateOf 的方式,保证每次重组时都会将newValue 值进行更新到MutableState.value中,最终副作用里访问的也是最新的newValue了,从而实现副作用在不中断的前提下访问到最新传入的值。使用示例如下:

kotlin 复制代码
@Composable
fun LandingScreen(onTimeout: () -> Unit) {

    // LandingScreen重组时会引用到最新的onTimeout函数
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    // 如果发生重组,delay函数不会再次执行了;而currentOnTimeout却是最新的onTimeout函数。
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }

    /* Landing screen content */
}

SideEffect

SideEffect 在每次成功重组时都会执行,这么看好像SideEffect 并不能处理什么副作用呀。其实还是有点区别的,SideEffect只会在重组成功之后才会执行,如果重组失败,那么SideEffect中的lambda将不再执行,以免在当前组合操作失败时使对象处于不一致状态。

使用示例:

kotlin 复制代码
@Composable
fun SideEffectStudy() {
    Log.e("Tag", "SideEffect launch")
    throw IllegalArgumentException("exception throw")
}

上述可组合项执行时会抛出异常,在抛异常之前会先输出Log。如果我们想重组成功之后再输出Log该怎么办呢?这时候就可以使用SideEffect了:

kotlin 复制代码
@Composable
fun SideEffectStudy() {
    SideEffect {
        Log.e("Tag", "SideEffect launch")
    }
    throw IllegalArgumentException("exception throw")
}

可以看到将Log日志放到了SideEffect内部,这样就可以保证只有成功重组后才会执行SideEffec t内部的lambda。

总结

  • LaunchedEffect 内部会启动协程,所以适合当副作用中有耗时任务时的场景下;
  • rememberCoroutineScope当需要在可组合项外启动协程时(如onClick中),可以使用rememberCoroutineScope
  • DisposableEffect 适合有资源需要收尾的场景下,内部一定要重写onDispose(),用于释放资源、解注册等操作;
  • rememberUpdatedState 对于包含长期操作的副作用很有效,尤其适合用在重启这些操作时代价高昂的场景中;
  • SideEffect 每次重组时都会执行内部的lambda,不过SideEffect 能确保可组合项成功重组之后才会执行内部的 Lambda。

资料

【1】可组合项的生命周期:https://developer.android.com/jetpack/compose/lifecycle?hl=zh-cn

【2】Compose中的附带效应(副作用):https://developer.android.com/jetpack/compose/side-effects?hl=zh-cn

相关推荐
大白要努力!1 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟2 小时前
Android音频采集
android·音视频
小白也想学C3 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程4 小时前
初级数据结构——树
android·java·数据结构
闲暇部落6 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX8 小时前
Android 分区相关介绍
android
大白要努力!9 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee9 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood9 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-12 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记