Mobile Development 11 min read

New Composable LifecycleOwner in AndroidX Lifecycle 2.10: Why It Matters

AndroidX Lifecycle 2.10.0‑alpha01 adds a composable LifecycleOwner, explaining how the existing LocalLifecycleOwner derives from view tree owners, when separate owners are needed during screen transitions, and provides code examples for implementing and integrating this new composable into navigation and other components.

AndroidPub
AndroidPub
AndroidPub
New Composable LifecycleOwner in AndroidX Lifecycle 2.10: Why It Matters

On July 30, 2025, AndroidX Lifecycle 2.10.0‑alpha01 was released, featuring a brand‑new composable LifecycleOwner.

What does the existing LocalLifecycleOwner provide?

First, understand what value LocalLifecycleOwner obtains from Compose. It is exposed by the ProvideAndroidCompositionLocals function:

@Composable
@OptIn(ExperimentalComposeUiApi::class)
internal fun ProvideAndroidCompositionLocals(
    owner: AndroidComposeView,
    content: @Composable () -> Unit,
) {
    constant(@Composable { -> Unit },
    val viewTreeOwners = owner.viewTreeOwners
    val lifecycleOwner = viewTreeOwners?.lifecycleOwner
    owner.lifecycleOwner =
        lifecycleOwner ?: throw IllegalStateException(
            "Called when the ViewTreeLifecycleOwnerAvailability is not yet in Available state"
        )
    // ...
}

Here, LocalLifecycleOwner comes from viewTreeOwners.lifecycleOwner. If you inspect viewTreeOwners, you will find it originates from the View method findViewTreeLifecycleOwner():

internal class AndroidComposeView(context: Context, coroutineContext: CoroutineContext) : ViewGroup(context) {
    @OptIn(ExperimentalComposeUiApi::class)
    @MainThread
    fun attachToWindow() {
        val lifecycleOwner = findViewTreeLifecycleOwner()
        if (lifecycleOwner == null) {
            if (Lifecycle.TRACE_ENABLED) {
                throw IllegalStateException(
                    "Composed into the view which doesn't propagate ViewTreeLifecycleOwner"
                )
            }
        }
        lifecycleOwner.lifecycle.addObserver(this)
        viewTreeOwners = ViewTreeOwners(
            lifecycleOwner = lifecycleOwner,
            savedStateRegistryOwner = savedStateRegistryOwner,
            isInBackStack = isInBackStack
        )
        _viewTreeOwners = viewTreeOwners
    }
}

This means the obtained LifecycleOwner is set on the Activity or Fragment's window.decorView. In practice, all composables in an Activity share the same LifecycleOwner.

When is a separate LifecycleOwner needed?

Sharing a single LifecycleOwner across an entire Activity can cause problems, especially during screen transitions. The Android team mentioned this in a PR (https://android-review.googlesource.com/c/platform/frameworks/support/+/3517756).

Diagram illustrating lifecycle issues during transition
Diagram illustrating lifecycle issues during transition

SinglePaneNavDisplay is a component in Android's navigation architecture that manages screen display. In a single‑pane navigation scenario, only one screen should be in the RESUMED state at a time. However, during transitions (e.g., switching from one Fragment to another), both the old and new screens coexist in the view hierarchy and receive lifecycle events. If the shared LifecycleOwner logic is used, both screens may be RESUMED simultaneously, leading to duplicated data loading, animations, and unnecessary CPU/memory consumption.

To solve this, the Android team extracted the logic into an independent composable LifecycleOwner that determines its maximum state based on the current transition and back‑stack status.

Using NavLocalProvider, a LocalLifecycleOwner is installed whose maximum state is decided by the current transition and back‑stack:

// Screen data class
 data class Screen(val id: String)

 // Navigation local provider, core logic
 @Composable
 fun NavLocalProvider(
     backStackManager: BackStackManager,
     content: @Composable () -> Unit
 ) {
     // Get current screen
     val currentScreen = backStackManager.currentScreen
     // Is the screen in the back stack?
     val isInBackStack = currentScreen?.let { backStackManager.backStack.contains(it) } ?: false
     // Is the screen stable (no transition)?
     val isStable = !backStackManager.isTransitioning

     // Determine max lifecycle
     val maxLifecycle = when {
         isInBackStack && isStable -> Lifecycle.State.RESUMED
         isInBackStack -> Lifecycle.State.STARTED
         else -> Lifecycle.State.CREATED
     }

     // Install new LocalLifecycleOwner with calculated max lifecycle
     LifecycleOwner(maxLifecycle = maxLifecycle) {
         content()
     }
 }

State determination : NavLocalProvider checks whether the current screen is in the back stack and whether it is in a stable state.

Dynamic max lifecycle calculation :

If the screen is fully displayed and stable, use RESUMED for normal interaction.

If the screen is in the back stack but transitioning (e.g., just pushed or still entering), use STARTED to limit resource usage.

If the screen is being popped and still exiting, use CREATED to prepare for resource release.

Local lifecycle injection : A new LifecycleOwner is created with the calculated max state and provided to child composables via CompositionLocalProvider, ensuring each screen’s composables follow the appropriate lifecycle.

This mechanism guarantees that each screen’s lifecycle precisely matches its actual visibility during transitions, avoiding resource contention and interaction conflicts.

Because the independent LifecycleOwner is useful beyond navigation, it can be applied to any component that needs its own lifecycle scope (e.g., MapView, video players) with an upper bound defined by a specific maximum state.

Analyzing the LifecycleOwner composable

@Composable
fun LifecycleOwner(
    maxLifecycle: State = RESUMED,
    parentLifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    content: @Composable () -> Unit,
) {
    val childLifecycleOwner = remember(parentLifecycleOwner) { ChildLifecycleOwner() }
    // Propagate lifecycle events from parent to child.
    DisposableEffect(childLifecycleOwner, parentLifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            childLifecycleOwner.handleLifecycleEvent(event)
        }
        parentLifecycleOwner.lifecycle.addObserver(observer)
        onDispose { parentLifecycleOwner.lifecycle.removeObserver(observer) }
    }
    // Ensure child lifecycle does not exceed maxLifecycle.
    LaunchedEffect(childLifecycleOwner, maxLifecycle) {
        childLifecycleOwner.maxLifecycle = maxLifecycle
    }
    // Provide the child LifecycleOwner to the composition.
    CompositionLocalProvider(LocalLifecycleOwner provides childLifecycleOwner, content = content)
}

/**
 * Private LifecycleOwner controlled by the parent lifecycle and capped by a maximum state.
 */
private class ChildLifecycleOwner : LifecycleOwner {
    private val lifecycleRegistry = LifecycleRegistry(this)
    override val lifecycle = lifecycleRegistry

    private var parentLifecycleState: State = State.INITIALIZED
    var maxLifecycle: State = State.INITIALIZED
        set(value) {
            field = value
            updateLifecycleState()
        }

    fun parentLifecycleState(event: LifecycleEvent) {
        parentLifecycleState = event.targetState
        updateLifecycleState()
    }

    private fun updateLifecycleState() {
        // Child state is the lesser of parent state and max lifecycle.
        if (parentLifecycleState.ordinal < maxLifecycle.ordinal) {
            lifecycleRegistry.currentState = parentLifecycleState
        } else {
            lifecycleRegistry.currentState = maxLifecycle
        }
    }
}

Inside LifecycleOwner, a new ChildLifecycleOwner is created and linked to the parent via DisposableEffect. The child’s state is capped by maxLifecycle, and the combined owner is injected into the composition.

How to use it in Navigation 3’s NavDisplay

@Composable
fun NavDisplay(
    backStack: List<NavEntry>,
    modifier: Modifier = Modifier,
) {
    val isSettled by remember { mutableStateOf(true) }
    val transitionAwareLifecycleNavDecorator =
        TransitionAwareLifecycleNavDecorator(backStack, isSettled)
    DecoratedBackStack(
        backStack = backStack,
        entryDecorators = entryDecorators + transitionAwareLifecycleNavDecorator,
    ) { entries ->
        // ...
    }
}

The key is TransitionAwareLifecycleNavDecorator, which installs our LifecycleOwner based on transition state:

@Composable
internal fun TransitionAwareLifecycleNavDecorator(
    backStack: List<NavEntry>,
    isSettled: Boolean,
) {
    val navEntry = entry -> backStack.backStack
    val maxLifecycle =
        if (isInBackStack && isSettled) Lifecycle.State.RESUMED
        else if (isInBackStack) Lifecycle.State.STARTED
        else Lifecycle.State.CREATED
    LifecycleOwner(maxLifecycle = maxLifecycle) { entry.Content() }
}

During a transition, a screen is limited to CREATED or STARTED state, reproducing the behavior of traditional view‑based navigation but with per‑screen granularity.

AndroidKotlinJetpack ComposenavigationLifecycleOwner
AndroidPub
Written by

AndroidPub

Senior Android Developer & Interviewer, regularly sharing original tech articles, learning resources, and practical interview guides. Welcome to follow and contribute!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.