可组合项生命周期
上图描绘了组合中可组合项的生命周期:进入组合,履行 0 次或屡次重组,然后退出组合。在可组合项履行过程中,影响操作结果的能够认为是一种副效果,如在Composable中履行网络请求、Dialog弹窗、弹Toast、页面跳转等,不管重组多少次,有些操作只需求履行一次即可,假如屡次履行就会呈现意想不到的结果。针对上述情况,Compose 有一系列专门的副效果 API 来处理。
LaunchedEffect
LaunchedEffect 能够在可组合项的效果域内敞开协程,假如副效果中需求处理异步相关的使命,就能够挑选LaunchedEffect。
@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不参加重组。
运用示例:
@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 ,源码如下:
@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 绑定到调用它的组合点。调用退出组合后,效果域将撤销。
运用示例:
@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
@Composable
@NonRestartableComposable
fun DisposableEffect(
key1: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1) { DisposableEffectImpl(effect) }
}
关于需求在键产生变化或可组合项退出组合后进行整理的副效果,能够运用 DisposableEffect。假如 DisposableEffect 中的key产生变化,那么就会履行onDispose()资源开释等操作,偏从头履行DisposableEffect。
DisposableEffect中强制以onDispose进行收尾,不加就会编译报错。因而DisposableEffect合适有资源需求收尾的场景下。
运用示例:
@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,如下:
//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 获取最新状况即可。
@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了,然后实现副效果在不中止的前提下拜访到最新传入的值。运用示例如下:
@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将不再履行,以免在当前组合操作失利时使对象处于不一致状况。
运用示例:
@Composable
fun SideEffectStudy() {
Log.e("Tag", "SideEffect launch")
throw IllegalArgumentException("exception throw")
}
上述可组合项履行时会抛出反常,在抛反常之前会先输出Log。假如咱们想重组成功之后再输出Log该怎么办呢?这时候就能够运用SideEffect了:
@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