Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9ee8f8f2 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge changes I2cc16c38,Ie1282739 into main

* changes:
  Introduce {LaunchedEffect,DisposableEffect,produceState}WithLifecycle()
  Override lifecycle of always composed contents
parents 51b84c48 602ecafb
Loading
Loading
Loading
Loading
+166 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.compose.lifecycle

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffectResult
import androidx.compose.runtime.DisposableEffectScope
import androidx.compose.runtime.NonRestartableComposable
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.CoroutineScope

// This deprecated-error function shadows the varargs overload so that the varargs version
// is not used without key parameters.
@Deprecated(DisposableEffectNoParamError, level = DeprecationLevel.ERROR)
@Composable
fun DisposableEffectWithLifecycle(block: suspend CoroutineScope.() -> Unit) {
    error(DisposableEffectNoParamError)
}

private const val DisposableEffectNoParamError =
    "DisposableEffectWithLifecycle must provide one or more 'key' parameters that define the " +
        "identity of the DisposableEffect and determine when its previous effect coroutine should" +
        " be cancelled and a new effect Disposable for the new key."

/**
 * A [DisposableEffect] that is lifecycle-aware.
 *
 * This effect is triggered every time the [lifecycle] reaches the [minActiveState], and will be
 * **re-launched** whenever [DisposableEffectWithLifecycle] is recomposed with a different [key1],
 * [lifecycle] or [minActiveState].
 *
 * @see androidx.compose.runtime.DisposableEffect
 */
@Composable
@NonRestartableComposable
fun DisposableEffectWithLifecycle(
    key1: Any?,
    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    effect: DisposableEffectScope.() -> DisposableEffectResult,
) {
    DisposableEffect(key1, lifecycle, minActiveState) {
        disposableEffectWithLifecycle(lifecycle, minActiveState, effect)
    }
}

/**
 * A [DisposableEffect] that is lifecycle-aware.
 *
 * This effect is triggered every time the [lifecycle] reaches the [minActiveState], and will be
 * **re-launched** whenever [DisposableEffectWithLifecycle] is recomposed with a different [key1],
 * [key2], [lifecycle] or [minActiveState].
 *
 * @see androidx.compose.runtime.DisposableEffect
 */
@Composable
@NonRestartableComposable
fun DisposableEffectWithLifecycle(
    key1: Any?,
    key2: Any?,
    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    effect: DisposableEffectScope.() -> DisposableEffectResult,
) {
    DisposableEffect(key1, key2, lifecycle, minActiveState) {
        disposableEffectWithLifecycle(lifecycle, minActiveState, effect)
    }
}

/**
 * A [DisposableEffect] that is lifecycle-aware.
 *
 * This effect is triggered every time the [lifecycle] reaches the [minActiveState], and will be
 * **re-launched** whenever [DisposableEffectWithLifecycle] is recomposed with a different [key1],
 * [key2], [lifecycle] or [minActiveState].
 *
 * @see androidx.compose.runtime.DisposableEffect
 */
@Composable
@NonRestartableComposable
fun DisposableEffectWithLifecycle(
    key1: Any?,
    key2: Any?,
    key3: Any?,
    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    effect: DisposableEffectScope.() -> DisposableEffectResult,
) {
    DisposableEffect(key1, key2, key3, lifecycle, minActiveState) {
        disposableEffectWithLifecycle(lifecycle, minActiveState, effect)
    }
}

/**
 * A [DisposableEffect] that is lifecycle-aware.
 *
 * This effect is triggered every time the [lifecycle] reaches the [minActiveState], and will be
 * **re-launched** whenever [DisposableEffectWithLifecycle] is recomposed with a different [key1],
 * [key2], [key3], [lifecycle] or [minActiveState].
 *
 * @see androidx.compose.runtime.DisposableEffect
 */
@Composable
@NonRestartableComposable
fun DisposableEffectWithLifecycle(
    vararg keys: Any?,
    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    effect: DisposableEffectScope.() -> DisposableEffectResult,
) {
    DisposableEffect(keys, lifecycle, minActiveState) {
        disposableEffectWithLifecycle(lifecycle, minActiveState, effect)
    }
}

private fun DisposableEffectScope.disposableEffectWithLifecycle(
    lifecycle: Lifecycle,
    minActiveState: Lifecycle.State,
    effect: DisposableEffectScope.() -> DisposableEffectResult,
): DisposableEffectResult {
    var effectResult: DisposableEffectResult? = null

    fun maybeLaunch() {
        if (effectResult != null) return
        effectResult = effect()
    }

    fun maybeDispose() {
        effectResult?.dispose()
        effectResult = null
    }

    fun update() {
        if (lifecycle.currentState.isAtLeast(minActiveState)) {
            maybeLaunch()
        } else {
            maybeDispose()
        }
    }

    val observer = LifecycleEventObserver { _, _ -> update() }
    lifecycle.addObserver(observer)
    update()

    return onDispose {
        lifecycle.removeObserver(observer)
        maybeDispose()
    }
}
+129 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.compose.lifecycle

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.NonRestartableComposable
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.CoroutineScope

// This deprecated-error function shadows the varargs overload so that the varargs version
// is not used without key parameters.
@Deprecated(LaunchedEffectNoParamError, level = DeprecationLevel.ERROR)
@Composable
fun LaunchedEffectWithLifecycle(block: suspend CoroutineScope.() -> Unit) {
    error(LaunchedEffectNoParamError)
}

private const val LaunchedEffectNoParamError =
    "LaunchedEffectWithLifecycle must provide one or more 'key' parameters that define the " +
        "identity of the LaunchedEffect and determine when its previous effect coroutine should " +
        "be cancelled and a new effect launched for the new key."

/**
 * A [LaunchedEffect] that is lifecycle-aware.
 *
 * This effect is triggered every time the [lifecycle] reaches the [minActiveState], and will be
 * **re-launched** whenever [LaunchedEffectWithLifecycle] is recomposed with a different [key1],
 * [lifecycle] or [minActiveState].
 *
 * @see androidx.compose.runtime.LaunchedEffect
 */
@Composable
@NonRestartableComposable
fun LaunchedEffectWithLifecycle(
    key1: Any?,
    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    block: suspend CoroutineScope.() -> Unit,
) {
    LaunchedEffect(key1, lifecycle, minActiveState) {
        lifecycle.repeatOnLifecycle(minActiveState, block)
    }
}

/**
 * A [LaunchedEffect] that is lifecycle-aware.
 *
 * This effect is triggered every time the [lifecycle] reaches the [minActiveState], and will be
 * **re-launched** whenever [LaunchedEffectWithLifecycle] is recomposed with a different [key1],
 * [key2], [lifecycle] or [minActiveState].
 *
 * @see androidx.compose.runtime.LaunchedEffect
 */
@Composable
@NonRestartableComposable
fun LaunchedEffectWithLifecycle(
    key1: Any?,
    key2: Any?,
    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    block: suspend CoroutineScope.() -> Unit,
) {
    LaunchedEffect(key1, key2, lifecycle, minActiveState) {
        lifecycle.repeatOnLifecycle(minActiveState, block)
    }
}

/**
 * A [LaunchedEffect] that is lifecycle-aware.
 *
 * This effect is triggered every time the [lifecycle] reaches the [minActiveState], and will be
 * **re-launched** whenever [LaunchedEffectWithLifecycle] is recomposed with a different [key1],
 * [key2], [lifecycle] or [minActiveState].
 *
 * @see androidx.compose.runtime.LaunchedEffect
 */
@Composable
@NonRestartableComposable
fun LaunchedEffectWithLifecycle(
    key1: Any?,
    key2: Any?,
    key3: Any?,
    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    block: suspend CoroutineScope.() -> Unit,
) {
    LaunchedEffect(key1, key2, key3, lifecycle, minActiveState) {
        lifecycle.repeatOnLifecycle(minActiveState, block)
    }
}

/**
 * A [LaunchedEffect] that is lifecycle-aware.
 *
 * This effect is triggered every time the [lifecycle] reaches the [minActiveState], and will be
 * **re-launched** whenever [LaunchedEffectWithLifecycle] is recomposed with a different [key1],
 * [key2], [key3], [lifecycle] or [minActiveState].
 *
 * @see androidx.compose.runtime.LaunchedEffect
 */
@Composable
@NonRestartableComposable
fun LaunchedEffectWithLifecycle(
    vararg keys: Any?,
    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    block: suspend CoroutineScope.() -> Unit,
) {
    LaunchedEffect(keys, lifecycle, minActiveState) {
        lifecycle.repeatOnLifecycle(minActiveState, block)
    }
}
+179 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.compose.lifecycle

import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.ProduceStateScope
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.suspendCancellableCoroutine

/**
 * A [produceState] that is lifecycle-aware.
 *
 * [producer] is launched when the [lifecycle] reaches the [minActiveState], and will be
 * **re-launched** whenever [produceStateWithLifecycle] is recomposed with a different [lifecycle]
 * or [minActiveState].
 *
 * @see androidx.compose.runtime.produceState
 */
@Composable
fun <T> produceStateWithLifecycle(
    initialValue: T,
    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    producer: suspend ProduceStateScope<T>.() -> Unit,
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffectWithLifecycle(lifecycle = lifecycle, minActiveState = minActiveState) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}

/**
 * A [produceState] that is lifecycle-aware.
 *
 * [producer] is launched when the [lifecycle] reaches the [minActiveState], and will be
 * **re-launched** whenever [produceStateWithLifecycle] is recomposed with a different [key1],
 * [lifecycle] or [minActiveState].
 *
 * @see androidx.compose.runtime.produceState
 */
@Composable
fun <T> produceStateWithLifecycle(
    initialValue: T,
    key1: Any?,
    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    producer: suspend ProduceStateScope<T>.() -> Unit,
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffectWithLifecycle(key1, lifecycle = lifecycle, minActiveState = minActiveState) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}

/**
 * A [produceState] that is lifecycle-aware.
 *
 * [producer] is launched when the [lifecycle] reaches the [minActiveState], and will be
 * **re-launched** whenever [produceStateWithLifecycle] is recomposed with a different [key1],
 * [key2], [lifecycle] or [minActiveState].
 *
 * @see androidx.compose.runtime.produceState
 */
@Composable
fun <T> produceStateWithLifecycle(
    initialValue: T,
    key1: Any?,
    key2: Any?,
    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    producer: suspend ProduceStateScope<T>.() -> Unit,
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffectWithLifecycle(
        key1,
        key2,
        lifecycle = lifecycle,
        minActiveState = minActiveState,
    ) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}

/**
 * A [produceState] that is lifecycle-aware.
 *
 * [producer] is launched when the [lifecycle] reaches the [minActiveState], and will be
 * **re-launched** whenever [produceStateWithLifecycle] is recomposed with a different [key1],
 * [key2], [key3], [lifecycle] or [minActiveState].
 *
 * @see androidx.compose.runtime.produceState
 */
@Composable
fun <T> produceStateWithLifecycle(
    initialValue: T,
    key1: Any?,
    key2: Any?,
    key3: Any?,
    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    producer: suspend ProduceStateScope<T>.() -> Unit,
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffectWithLifecycle(
        key1,
        key2,
        key3,
        lifecycle = lifecycle,
        minActiveState = minActiveState,
    ) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}

/**
 * A [produceState] that is lifecycle-aware.
 *
 * [producer] is launched when the [lifecycle] reaches the [minActiveState], and will be
 * **re-launched** whenever [produceStateWithLifecycle] is recomposed with a different [keys],
 * [lifecycle] or [minActiveState].
 *
 * @see androidx.compose.runtime.produceState
 */
@Composable
fun <T> produceStateWithLifecycle(
    initialValue: T,
    vararg keys: Any?,
    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    producer: suspend ProduceStateScope<T>.() -> Unit,
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffectWithLifecycle(
        keys = keys,
        lifecycle = lifecycle,
        minActiveState = minActiveState,
    ) {
        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()
        }
    }
}
+127 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.compose.lifecycle

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.awaitCancellation
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class EffectWithLifecycleTest {
    @get:Rule val rule = createComposeRule()

    @Test
    fun launchedEffect() {
        testEffectWithLifecycle { minActiveState, isEffectRunning ->
            LaunchedEffectWithLifecycle(Unit, minActiveState = minActiveState) {
                try {
                    isEffectRunning(true)
                    awaitCancellation()
                } finally {
                    isEffectRunning(false)
                }
            }
        }
    }

    @Test
    fun disposableEffect() {
        testEffectWithLifecycle { minActiveState, isEffectRunning ->
            DisposableEffectWithLifecycle(minActiveState = minActiveState) {
                isEffectRunning(true)
                onDispose { isEffectRunning(false) }
            }
        }
    }

    @Test
    fun produceState() {
        testEffectWithLifecycle { minActiveState, isEffectRunning ->
            produceStateWithLifecycle(initialValue = 0, minActiveState = minActiveState) {
                try {
                    isEffectRunning(true)
                    awaitCancellation()
                } finally {
                    isEffectRunning(false)
                }
            }
        }
    }

    private fun testEffectWithLifecycle(
        effect:
            @Composable
            (minActiveState: Lifecycle.State, isEffectRunning: (Boolean) -> Unit) -> Unit
    ) {
        val lifecycle =
            rule.runOnUiThread {
                object : LifecycleOwner {
                    override val lifecycle = LifecycleRegistry(this)

                    init {
                        lifecycle.currentState = Lifecycle.State.CREATED
                    }
                }
            }

        var isEffectRunning = false
        var minActiveState by mutableStateOf(Lifecycle.State.STARTED)

        rule.setContent {
            CompositionLocalProvider(LocalLifecycleOwner provides lifecycle) {
                effect(minActiveState) { isEffectRunning = it }
            }
        }

        // currentState = CREATED / minActivateState = STARTED.
        assertThat(isEffectRunning).isFalse()

        // currentState = CREATED / minActivateState = CREATED.
        minActiveState = Lifecycle.State.CREATED
        rule.waitForIdle()
        assertThat(isEffectRunning).isTrue()

        // currentState = STARTED / minActivateState = RESUMED.
        minActiveState = Lifecycle.State.RESUMED
        rule.runOnUiThread { lifecycle.lifecycle.currentState = Lifecycle.State.STARTED }
        rule.waitForIdle()
        assertThat(isEffectRunning).isFalse()

        // currentState = RESUMED / minActivateState = RESUMED.
        rule.runOnUiThread { lifecycle.lifecycle.currentState = Lifecycle.State.RESUMED }
        rule.waitForIdle()
        assertThat(isEffectRunning).isTrue()

        // currentState = DESTROYED / minActivateState = RESUMED.
        rule.runOnUiThread { lifecycle.lifecycle.currentState = Lifecycle.State.DESTROYED }
        rule.waitForIdle()
        assertThat(isEffectRunning).isFalse()
    }
}
+71 −9
Original line number Diff line number Diff line
@@ -24,6 +24,8 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
@@ -43,6 +45,11 @@ import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.android.compose.animation.scene.Ancestor
import com.android.compose.animation.scene.AnimatedState
import com.android.compose.animation.scene.ContentKey
@@ -167,6 +174,9 @@ internal sealed class Content(
        // automatically used when calling rememberOverscrollEffect().
        val isElevationPossible =
            layoutImpl.state.isElevationPossible(content = key, element = null)

        val content =
            @Composable {
                Box(
                    modifier.then(ContentElement(this, isElevationPossible, isInvisible)).thenIf(
                        layoutImpl.implicitTestTags
@@ -180,6 +190,28 @@ internal sealed class Content(
                }
            }

        if (alwaysCompose) {
            AlwaysComposedContent(isInvisible, content)
        } else {
            content()
        }
    }

    @Composable
    private fun AlwaysComposedContent(isInvisible: Boolean, content: @Composable () -> Unit) {
        val maxState = if (isInvisible) Lifecycle.State.CREATED else Lifecycle.State.RESUMED
        val parentLifecycle = LocalLifecycleOwner.current.lifecycle
        val lifecycleOwner =
            remember(parentLifecycle) { RestrictedLifecycleOwner(parentLifecycle, maxState) }
        DisposableEffect(lifecycleOwner) { onDispose { lifecycleOwner.destroy() } }

        if (maxState != lifecycleOwner.maxLifecycleState) {
            SideEffect { lifecycleOwner.maxLifecycleState = maxState }
        }

        CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner, content)
    }

    fun areNestedSwipesAllowed(): Boolean = nestedScrollControlState.isOuterScrollAllowed

    fun maybeUpdateEffects(effectFactory: OverscrollFactory) {
@@ -378,3 +410,33 @@ internal class ContentScopeImpl(
        )
    }
}

/** A [LifecycleOwner] that follows its [parentLifecycle] but is capped at [maxLifecycleState]. */
private class RestrictedLifecycleOwner(
    val parentLifecycle: Lifecycle,
    maxLifecycleState: Lifecycle.State,
) : LifecycleOwner {
    override val lifecycle = LifecycleRegistry(this)

    var maxLifecycleState = maxLifecycleState
        set(value) {
            field = value
            updateState()
        }

    private val observer = LifecycleEventObserver { _, _ -> updateState() }

    init {
        updateState()
        parentLifecycle.addObserver(observer)
    }

    private fun updateState() {
        lifecycle.currentState = minOf(this.maxLifecycleState, parentLifecycle.currentState)
    }

    fun destroy() {
        parentLifecycle.removeObserver(observer)
        lifecycle.currentState = Lifecycle.State.DESTROYED
    }
}
Loading