赞
踩
官方解释:发生在可组合函数作用域之外的应用状态的变化叫做副作用
由于可组合项的生命周期和属性(例如不可预测的重组、以不同顺序执行可组合项的重组或可以舍弃的重组),可组合项在理想情况下应该是无副作用的。
Composable在执行过程中,凡事会影响外界的操作都属于副作用,比很多一次性的事件如Toast、保存文件、获取远程或本地数据。因重组会造成Composabl频繁执行,显然这些事件不应该反复被执行,所以我们需要使用Compose提供的副作用API;
它作用是让副作用API只发生在Composable生命周期的特定阶段,从而实现行为可预期。【实现监控Compsable的生命周期变化】
@Composable @NonRestartableComposable fun DisposableEffect( key1: Any?, effect: DisposableEffectScope.() -> DisposableEffectResult ) { remember(key1) { DisposableEffectImpl(effect) } } interface DisposableEffectResult { fun dispose() } private val InternalDisposableEffectScope = DisposableEffectScope() private class DisposableEffectImpl( private val effect: DisposableEffectScope.() -> DisposableEffectResult ) : RememberObserver { private var onDispose: DisposableEffectResult? = null //Called when this object is successfully remembered by a composition. //This method is called on the composition's apply thread. override fun onRemembered() { onDispose = InternalDisposableEffectScope.effect() } //Called when this object is forgotten by a composition. //This method is called on the composition's apply thread. override fun onForgotten() { onDispose?.dispose() onDispose = null } //Called when this object is returned by the callback to remember //but is not successfully remembered by a composition. override fun onAbandoned() { // Nothing to do as [onRemembered] was not called. } } /** * Receiver scope for [DisposableEffect] that offers the [onDispose] clause that should be * the last statement in any call to [DisposableEffect]. */ class DisposableEffectScope { /** * Provide [onDisposeEffect] to the [DisposableEffect] to run when it leaves the composition * or its key changes. */ //【方法3】:这个方法必须实现啦啦啦啦 inline fun onDispose( crossinline onDisposeEffect: () -> Unit ): DisposableEffectResult = object : DisposableEffectResult { override fun dispose() { onDisposeEffect() } } }
他可以感知Composable生命周期中的加入组合和退出组合的变化,加入组合它会启动一个协程,并将代码块作为参数传递,当键发生变化或退出组合协程将自动取消并回调OnDispose(标注方法3),从而实现注销回调,避免泄漏。
/** * DisposableEffect 对于需要在键发生变化或可组合项退出组合后进行清理的附带效应 */ @Composable fun onObserverLifecycleScreen( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onStart: () -> Unit, onStop: () -> Unit ) { //为了确保 onStart lambda 始终包含重组 onObserverLifecycleScreen 时使用的最新值 val currentOnStart by rememberUpdatedState(newValue = onStart) //可监听组合项退出组合时的回调,及控件销毁使的挂起销毁的回调操作 val currentOnStop by rememberUpdatedState(newValue = onStop) DisposableEffect(key1 = lifecycleOwner) { val observer = LifecycleEventObserver { source, event -> if (event == Lifecycle.Event.ON_START) { currentOnStart() } else if (event == Lifecycle.Event.ON_STOP) { currentOnStop() } } Log.i("SideEffect","onObserverLifecycleScreen DisposableEffect addObserver $observer") lifecycleOwner.lifecycle.addObserver(observer) onDispose { Log.i("SideEffect","onObserverLifecycleScreen DisposableEffect onDispose") lifecycleOwner.lifecycle.removeObserver(observer) } } }
@Composable
@NonRestartableComposable
@ExplicitGroupsComposable
@OptIn(InternalComposeApi::class)
fun SideEffect(
effect: () -> Unit
) {
/* A Compose internal function. DO NOT call directly.
Record a function to call when changes to the corresponding tree are applied to the applier.
This is used to implement SideEffect.
*/
currentComposer.recordSideEffect(effect)
}
如需与非 Compose 管理的对象共享 Compose 状态,请使用 SideEffect 可组合项。
1、Composable重组不一定会成功结束,有的重组可能会中途失败,SideEffec只会在每次【成功重组】时才会执行,如果重组失败则不会被触发。
2、因会重组会频繁发生,所以它不能用来处理耗时或者异步的副作用逻辑
官方提供的示例代码,分析用户行为数据
@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
FirebaseAnalytics()
}
// On every successful composition, update FirebaseAnalytics with
// the userType from the current User, ensuring that future analytics
// events have this metadata attached
SideEffect {
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
@Composable @NonRestartableComposable @OptIn(InternalComposeApi::class) fun LaunchedEffect( key1: Any?, block: suspend CoroutineScope.() -> Unit ) { val applyContext = currentComposer.applyCoroutineContext remember(key1) { LaunchedEffectImpl(applyContext, block) } } internal class LaunchedEffectImpl( parentCoroutineContext: CoroutineContext, private val task: suspend CoroutineScope.() -> Unit ) : RememberObserver { private val scope = CoroutineScope(parentCoroutineContext) private var job: Job? = null override fun onRemembered() { // This should never happen but is left here for safety job?.cancel("Old job was still running!") job = scope.launch(block = task) } override fun onForgotten() { job?.cancel(LeftCompositionCancellationException()) job = null } override fun onAbandoned() { job?.cancel(LeftCompositionCancellationException()) job = null } }
1、支持在副作用内执行异步任务,block作用域为CoroutineScope(注意:副作用通常时在主线程中执行的,如果需要在副作用中执行耗时任务时,优先选择LaunchedEffect处理副作用)
2、从LaunchedEffectImpl方法中可以看到,当Composable进入组合时会启动协程执行block中的内容,当Composable被移除组合时,协程会被自动取消
3、当被观察的key发生变化时,当前协程会自动结束,同时开启新协程,见onRemembered方法
官方示例
@Composable fun MyScreen( state: UiState<List<Movie>>, snackbarHostState: SnackbarHostState ) { // If the UI state contains an error, show snackbar if (state.hasError) { // `LaunchedEffect` will cancel and re-launch if // `scaffoldState.snackbarHostState` changes LaunchedEffect(snackbarHostState) { // Show snackbar using a coroutine, when the coroutine is cancelled the // snackbar will automatically dismiss. This coroutine will cancel whenever // `state.hasError` is false, and only start when `state.hasError` is true // (due to the above if-check), or if `scaffoldState.snackbarHostState` changes. snackbarHostState.showSnackbar( message = "Error message", actionLabel = "Retry message" ) } } Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) } ) { contentPadding -> // ... } }
当state hasError为 true时,显示一个SnackBar(属于挂起函数),当snackbarHostState变化时,将启动一个新协程,SnackBar重新显示一次内容,当state hasError 为false时,副作用会被移除组合,协程会被取消
由于LaunchedEffect是composable函数,它只能在其他composable函数中被调用。因此想从非composable函数中创建coroutine时需要另寻他法。谷歌提供了rememberCoroutineScope用于在非composable函数中创建coroutine
@Composable inline fun rememberCoroutineScope( crossinline getContext: @DisallowComposableCalls () -> CoroutineContext = { EmptyCoroutineContext } ): CoroutineScope { val composer = currentComposer val wrapper = remember { CompositionScopedCoroutineScopeCanceller( createCompositionCoroutineScope(getContext(), composer) ) } return wrapper.coroutineScope } @PublishedApi internal class CompositionScopedCoroutineScopeCanceller( val coroutineScope: CoroutineScope ) : RememberObserver { override fun onRemembered() { // Nothing to do } override fun onForgotten() { coroutineScope.cancel(LeftCompositionCancellationException()) } override fun onAbandoned() { coroutineScope.cancel(LeftCompositionCancellationException()) } } @PublishedApi @OptIn(InternalComposeApi::class) internal fun createCompositionCoroutineScope( coroutineContext: CoroutineContext, composer: Composer ) = if (coroutineContext[Job] != null) { CoroutineScope( Job().apply { completeExceptionally( IllegalArgumentException( "CoroutineContext supplied to " + "rememberCoroutineScope may not include a parent job" ) ) } ) } else { val applyContext = composer.applyCoroutineContext CoroutineScope(applyContext + Job(applyContext[Job]) + coroutineContext) }
1、rememberCoroutineScope可以返回一个coroutineScope,便于开发者手动控制该coroutine的生命周期,例如有用户点击事件时启动该coroutine。
2、rememberCoroutineScope返回的coroutineScope会和其调用点的生命周期保持一致,当调用点所在的Composition退出时,该coroutineScope会被取消。
@Composable fun onRememberCoroutineScope() { val scope = rememberCoroutineScope() Button(onClick = { scope.launch { flow { delay(5000) emit("rememberCoroutineScope") } .flowOn(Dispatchers.IO) .collect { Logger.i("onRememberCoroutineScope scope launch $it") } } }) { Text(text = "rememberCoroutineScope") } }
Button的OnClick方法Compsable,如果需要在点击事件里面启动协程,则需要使用rememberCoroutineScope
LaunchedEffect当key发生变化时就会取消当前协程并开启新的协程,但有时候我们不希望当前协程被中断,只要能够实现实时获取最新状态时,可以使用rememberUpdatedState
###源码
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
mutableStateOf(newValue)
}.apply { value = newValue }
remember确保实例可以跨越重组,副作用里访问的是mutableState中最新的值
@Composable
fun OnRememberUpdatedSate(lastClickColor: String = "UNKNOW") {
val lastClickColorUpdate by rememberUpdatedState(newValue = lastClickColor)
LaunchedEffect(key1 = Unit) {
delay(5000)
Logger.i("OnRememberUpdatedSate lastClickColorUpdate= $lastClickColorUpdate")
}
}
Logger可以总会打印出最新的值
通过rememberUpdatedState我们可以实现获取最新状态,但是状态发生改变时,LaunchedEffect无法第一时间收到通知,如果通过key的变化来通知状态变化,则会中断当前的协程,成本过高。
我们可以通过snapshotFlow实现将 Compose 的 State 转换为 Flow
fun <T> snapshotFlow( block: () -> T ): Flow<T> = flow { // Objects read the last time block was run val readSet = MutableScatterSet<Any>() val readObserver: (Any) -> Unit = { if (it is StateObjectImpl) { it.recordReadIn(ReaderKind.SnapshotFlow) } readSet.add(it) } // This channel may not block or lose data on a trySend call. val appliedChanges = Channel<Set<Any>>(Channel.UNLIMITED) // Register the apply observer before running for the first time // so that we don't miss updates. val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ -> val maybeObserved = changed.any { it !is StateObjectImpl || it.isReadIn(ReaderKind.SnapshotFlow) } if (maybeObserved) { appliedChanges.trySend(changed) } } try { var lastValue = Snapshot.takeSnapshot(readObserver).run { try { enter(block) } finally { dispose() } } emit(lastValue) while (true) { var found = false var changedObjects = appliedChanges.receive() // Poll for any other changes before running block to minimize the number of // additional times it runs for the same data while (true) { // Assumption: readSet will typically be smaller than changed set found = found || readSet.intersects(changedObjects) changedObjects = appliedChanges.tryReceive().getOrNull() ?: break } if (found) { readSet.clear() val newValue = Snapshot.takeSnapshot(readObserver).run { try { enter(block) } finally { dispose() } } if (newValue != lastValue) { lastValue = newValue emit(newValue) } } } } finally { unregisterApplyObserver.dispose() } }
内部在对State访问的同时,通过快照系统订阅变化,当State发生变化时,flow就会发送新数据,如果没有变化则不发送,
###示例
官方示例:系统在用户滚动经过要分析的列表的首个项目时记录下来的
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
注意:如果LaunchedEffect中的State会频繁发生变化时,不应该使用State的值作为key(实现当State本身发生改变时重启副作用),而应该将State本身最为key,然后在内部启动snapshotFlow依赖状态。
SideState通常用来将Compose的State暴露给外部使用,而produceState则是将一个外部数据源(如LiveData、rxjava或普通数据)转换成State
@Composable fun <T> produceState( initialValue: T, producer: suspend ProduceStateScope<T>.() -> Unit ): State<T> { val result = remember { mutableStateOf(initialValue) } LaunchedEffect(Unit) { ProduceStateScopeImpl(result, coroutineContext).producer() } return result } private class ProduceStateScopeImpl<T>( state: MutableState<T>, override val coroutineContext: CoroutineContext ) : ProduceStateScope<T>, MutableState<T> by state { override suspend fun awaitDispose(onDispose: () -> Unit): Nothing { try { suspendCancellableCoroutine<Nothing> { } } finally { onDispose() } } }
produceState中的协程任务会随着LaunchedEffect的onDispose被自动停止,LaunchedEffect内部的awaitDispose方法中可以处理不基于协程的逻辑,比如注册一个回调来实现释放资源。
官方示例:使用 produceState 从网络加载图像。loadNetworkImage 可组合函数会返回可以在其他可组合项中使用的 State。
@Composable fun loadNetworkImage( url: String, imageRepository: ImageRepository = ImageRepository() ): State<Result<Image>> { // Creates a State<T> with Result.Loading as initial value // If either `url` or `imageRepository` changes, the running producer // will cancel and will be re-launched with the new inputs. return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) { // In a coroutine, can make suspend calls val image = imageRepository.load(url) // Update State with either an Error or Success result. // This will trigger a recomposition where this State is read value = if (image == null) { Result.Error } else { Result.Success(image) } } }
可以实现将一个或者多个State转换成另一个State,derivedStateOf 中的block可以依赖其它State创建并返回一个新的State,当block中的state发生改变时,也会同时更新,并实现相关的组合进行重组
注意当一个计算结果依赖较多的State时,使用derivedStateOf有助于减少重组次数,提高性能,否则可以使用remeber(key1,key2)实现来提高性能
当副作用中的block存在可变化的值,但是没有指定为key,有可能会出现没有及时响应变化而出现bug,
应该遵循如下原则:当一个状态的变化需要造成副作用终止时,才将其添加为观察的key,否则应该使用rememberUpdatedState包装后,在副作用中使用,以避免中断执行中的副作用。
示例:详见DisposableEffect中的示例代码,通过rememberUpdatedState实现currentOnStart和currentOnStop只要保证在回调时它们是最新的值,而不是终止副作用
/** * DisposableEffect 对于需要在键发生变化或可组合项退出组合后进行清理的附带效应 */ @Composable fun onObserverLifecycleScreen( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onStart: () -> Unit, onStop: () -> Unit ) { //为了确保 onStart lambda 始终包含重组 onObserverLifecycleScreen 时使用的最新值 val currentOnStart by rememberUpdatedState(newValue = onStart) //可监听组合项退出组合时的回调,及控件销毁使的挂起销毁的回调操作 val currentOnStop by rememberUpdatedState(newValue = onStop) DisposableEffect(key1 = lifecycleOwner) { val observer = LifecycleEventObserver { source, event -> if (event == Lifecycle.Event.ON_START) { currentOnStart() } else if (event == Lifecycle.Event.ON_STOP) { currentOnStop() } } Log.i("SideEffect","onObserverLifecycleScreen DisposableEffect addObserver $observer") lifecycleOwner.lifecycle.addObserver(observer) onDispose { Log.i("SideEffect","onObserverLifecycleScreen DisposableEffect onDispose") lifecycleOwner.lifecycle.removeObserver(observer) } } }
额外介绍详见官方文档:https://developer.android.google.cn/jetpack/compose/side-effects?hl=zh-cn#derivedstateof
——note end———枯燥
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。