Android Jetpack Compose基础之Side-Effect副作用

什么是副作用Side Effect

官方解释:发生在可组合函数作用域之外的应用状态的变化叫做副作用
由于可组合项的生命周期和属性(例如不可预测的重组、以不同顺序执行可组合项的重组或可以舍弃的重组),可组合项在理想情况下应该是无副作用的。

Composable在执行过程中,凡事会影响外界的操作都属于副作用,比很多一次性的事件如Toast、保存文件、获取远程或本地数据。因重组会造成Composabl频繁执行,显然这些事件不应该反复被执行,所以我们需要使用Compose提供的副作用API;
它作用是让副作用API只发生在Composable生命周期的特定阶段,从而实现行为可预期。【实现监控Compsable的生命周期变化】

DisposableEffect

源码

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

SideEffect

源码

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

LaunchedEffect

源码

@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时,副作用会被移除组合,协程会被取消

rememberCoroutineScope

由于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

rememberUpdatedState

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可以总会打印出最新的值

snapshotFlow

通过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依赖状态。

创建状态的副作用API

produceState

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

derivedStateOf

可以实现将一个或者多个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———枯燥

  • 18
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值