Modo docs Help

Lifecycle

This article covers the lifetime of screen instances and their integration with the Android Lifecycle.

Screen Instance Lifecycle

Screen instances in Modo have a long lifetime:

  • Screen instances live as long as your app process (not tied to Activity or Fragment lifecycle)

  • The same instance survives recomposition and configuration changes (rotation, language change, etc.)

  • Safe to inject into your DI container if its lifetime is shorter or equal to the screen

This is guaranteed when using Modo.rememberRootScreen() and similar built-in functions.

Lifecycle Basics

Modo provides seamless integration with Android Lifecycle. Each screen gets its own LifecycleOwner that can be retrieved by using LocalLifecycleOwner:

// Access via LocalLifecycleOwner (standard Compose API) val lifecycleOwner = LocalLifecycleOwner.current val lifecycleState by lifecycleOwner.lifecycle.currentStateAsState() // Observe lifecycle events DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_RESUME -> { // Screen is ready for interactions } Lifecycle.Event.ON_PAUSE -> { // Screen is hiding } else -> {} } } lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } }

Screen lifecycle is controlled by three main factors:

Each screen progresses through the standard Android Lifecycle states:

State

Meaning

INITIALIZED

The screen is constructed (instance created) but has never been displayed.

CREATED

The screen was displayed at least once.

STARTED

The screen is in composition.

RESUMED

Ready for user interaction. The screen is STARTED, visible, and all transitions are complete.

DESTROYED

The screen is removed from the navigation graph, and all resources are cleaned up.

Screens move through states sequentially. Each transition happens when something changes in the screen's lifecycle:

DESTROYEDINITIALIZEDCREATEDSTARTEDRESUMEDFirst compositionEntering compositionShow animation completes(immediate if no animation)Hide transition startsLeaving compositionRemoved from navigationDESTROYEDINITIALIZEDCREATEDSTARTEDRESUMED

Parent-Child Lifecycle Coordination

The lifecycle of parent and child screens follows strict rules to ensure consistency:

Rules

Rule: A child's lifecycle state never exceeds its parent's state.

Parent: RESUMED → Child can reach: RESUMED Parent: STARTED → Child can reach: STARTED (blocked from RESUMED) Parent: CREATED → Child can reach: CREATED (blocked from STARTED)

Event Propagation from Parent:

Parent Event

Propagation Behavior

ON_CREATE

Never propagated (child subscribes after its own creation)

ON_START

Always propagated → Child moves to STARTED

ON_RESUME

Propagated but gated by transitions and activation → Child moves to RESUMED only if all conditions are met

ON_PAUSE

Always propagated → Child immediately moves to STARTED

ON_STOP

Always propagated → Child immediately moves to CREATED

ON_DESTROY

Conditionally propagated (blocked during config changes to preserve SavedStateRegistry)

Examples

Parent with transition, child without transition

ParentChildCREATEDEntering compositionSTARTEDAnimating...CREATED → STARTEDWaiting for parent...Animation completesRESUMEDRESUMEDChild enters compositionParent resumedParentChild

Even though the child has no animation, it waits at STARTED until the parent reaches RESUMED.

Screen Rotation (Configuration Change)

ActivityStackScreenScreenAll RESUMEDRotation startsSTARTEDSTARTEDSTARTEDCREATEDCREATEDCREATEDDESTROYEDON_DESTROY NOT propagated(screens preserved)Activity recreatedCREATEDSTARTEDSTARTEDSTARTEDRESUMEDRESUMEDRESUMEDON_PAUSEON_PAUSEON_STOPON_STOPON_STARTON_STARTON_RESUMEON_RESUMEActivityStackScreenScreen

During configuration changes, screens are preserved (not destroyed) and reattach to the new Activity instance.

Transition Lifecycle Control

Screens animated by ScreenTransition cannot reach RESUMED state until the animation completes:

  • Without transition: STARTED → RESUMED (immediate)

  • With transition: STARTED → (animation playing) → RESUMED (after animation completes)

This indirectly affects nested screens because of parent-child lifecycle propagation.

Practical Examples

Managing Keyboard

Show/hide keyboard based on screen visibility:

@Composable fun LoginScreenContent(modifier: Modifier) { val lifecycleOwner = LocalLifecycleOwner.current val keyboardController = LocalSoftwareKeyboardController.current val focusRequester = remember { FocusRequester() } DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_RESUME -> { // Screen is fully visible, focus input and show keyboard focusRequester.requestFocus() } Lifecycle.Event.ON_PAUSE -> { // Screen is hiding, clear focus and hide keyboard focusRequester.freeFocus() keyboardController?.hide() } else -> {} } } lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } TextField( value = text, onValueChange = { text = it }, modifier = Modifier.focusRequester(focusRequester) ) }

Debugging Screens Lifecycle

Enable logging to see lifecycle events:

ModoDevOptions.onScreenPreDisposeListener = { screen -> Log.d("Modo", "Screen pre-dispose: ${screen.screenKey}") } ModoDevOptions.onScreenDisposeListener = { screen -> Log.d("Modo", "Screen disposed: ${screen.screenKey}") }
Last modified: 17 February 2026