How ComponentContext Manages Lifecycle Events in Decompose: A Deep Dive into the Source Code

ComponentContext acts as the central lifecycle gateway in Decompose, using a MergedLifecycle mechanism to synchronize parent and child component states, ensuring children never exceed the lifecycle state of their parents.

In the arkivanov/decompose library, every component receives a ComponentContext through its constructor, making it the single source of truth for lifecycle management, state preservation, and back-press handling. Understanding how this context propagates lifecycle events—particularly to child components—is essential for building robust, memory-leak-free navigation stacks.

Understanding the ComponentContext Interface

The Core Abstraction

At the heart of the system lies GenericComponentContext, defined in GenericComponentContext.kt. This interface bundles four essential cross-cutting concerns that every Decompose component needs:

interface GenericComponentContext<out T> :
    LifecycleOwner,
    StateKeeperOwner,
    InstanceKeeperOwner,
    BackHandlerOwner,
    ComponentContextFactoryOwner<T>

The ComponentContext interface itself simply extends this generic version:

interface ComponentContext : GenericComponentContext<ComponentContext>

Source: ComponentContext.kt

The Four Essential Owners

Each owner in GenericComponentContext handles a specific concern:

  • LifecycleOwner: Provides access to the Essenty Lifecycle for observing create, start, resume, pause, stop, and destroy events.
  • StateKeeperOwner: Manages StateKeeper for Android-like saved state handling across configuration changes.
  • InstanceKeeperOwner: Retains instances across navigation changes via InstanceKeeper.
  • BackHandlerOwner: Handles back-press events through BackHandler.

DefaultComponentContext: The Production Implementation

The concrete implementation used in production and tests is DefaultComponentContext, located in DefaultComponentContext.kt:

class DefaultComponentContext(
    override val lifecycle: Lifecycle,
    stateKeeper: StateKeeper? = null,
    instanceKeeper: InstanceKeeper? = null,
    backHandler: BackHandler? = null,
) : ComponentContext

When auxiliary objects are not supplied, the constructor automatically creates:

  • A StateKeeperDispatcher for state preservation
  • An InstanceKeeperDispatcher attached to the lifecycle
  • A BackDispatcher for handling back presses

Source: DefaultComponentContext.kt

How Lifecycle Events Propagate to Child Components

Creating Child Contexts with ComponentContextFactory

When a parent component creates a child (such as a stack destination), it uses the componentContextFactory to generate a new context. This factory is defined in DefaultComponentContext as:

override val componentContextFactory: ComponentContextFactory<ComponentContext> =
    ComponentContextFactory(::DefaultComponentContext)

The parent invokes it with child-specific instances:

componentContext.componentContextFactory(
    lifecycle = mergedLifecycle,
    stateKeeper = childStateKeeper,
    instanceKeeper = childInstanceKeeper,
    backHandler = childBackHandler,
)

Source: ComponentContextFactory.kt

The MergedLifecycle Mechanism

The core of lifecycle management in Decompose is MergedLifecycle, located in MergedLifecycle.kt. This class ensures that a child component never exceeds the lifecycle state of its parent.

When creating a child, the ChildController (in ChildController.kt) constructs a MergedLifecycle from two sources:

val mergedLifecycle = MergedLifecycle(
    componentContext.lifecycle,
    componentLifecycleRegistry,
)

Here, componentLifecycleRegistry is a fresh LifecycleRegistry for the child component.

State Synchronization Logic

MergedLifecycle subscribes to both lifecycles simultaneously and maintains the child at the minimum state of the two. The implementation works as follows:

  1. Initial State Capture: The constructor captures the initial states of both parent and child lifecycles.

  2. Dual Observation: It registers CallbacksImpl observers on both lifecycles, tracking state1 (parent) and state2 (child).

  3. Minimum State Enforcement: Whenever either lifecycle changes, it invokes moveTo(minOf(state1, state2)), ensuring the merged lifecycle never exceeds the more restrictive of the two.

  4. Cleanup: When the merged lifecycle reaches DESTROYED, both observers are automatically unsubscribed to prevent memory leaks.

This guarantees that if the parent pauses or stops, the child immediately follows, even if the child's own registry indicates it should be resumed.

Source: MergedLifecycle.kt

Practical Implementation Examples

Subscribing to Lifecycle Events in a Component

Components typically delegate to ComponentContext to gain direct access to lifecycle methods:

class MyComponent(
    private val componentContext: ComponentContext,
) : ComponentContext by componentContext {

    init {
        // React to lifecycle events
        componentContext.lifecycle.doOnDestroy {
            // cleanup resources
        }
    }
}

This pattern is common throughout the Decompose samples, such as in DefaultTabsComponent.kt.

Source: DefaultTabsComponent.kt

Creating a Child Stack Item

When implementing navigation with ChildStack, the factory method uses the context factory to propagate lifecycles correctly:

val childItem = childFactory(
    config,
    componentContext.componentContextFactory(
        lifecycle = MergedLifecycle(componentContext.lifecycle, childRegistry),
        stateKeeper = StateKeeperDispatcher(),
        instanceKeeper = InstanceKeeperDispatcher(),
        backHandler = BackDispatcher()
    )
)

The childRegistry is a fresh LifecycleRegistry for the child, automatically synchronized with the parent's lifecycle via MergedLifecycle.

Source: ChildController.kt

Summary

  • ComponentContext serves as the lifecycle gateway for every Decompose component, bundling LifecycleOwner, StateKeeperOwner, InstanceKeeperOwner, and BackHandlerOwner.
  • DefaultComponentContext provides the production implementation, automatically creating dispatchers for state, instances, and back-press handling when not explicitly provided.
  • MergedLifecycle is the critical mechanism ensuring child components never exceed their parent's lifecycle state by maintaining both at the minimum of the two states.
  • ComponentContextFactory enables type-safe creation of child contexts, used internally by ChildController when navigating between stack destinations.
  • Lifecycle observers are typically managed through delegation (by componentContext), allowing components to react to doOnDestroy and other lifecycle callbacks directly.

Frequently Asked Questions

What is the relationship between ComponentContext and LifecycleOwner?

ComponentContext extends GenericComponentContext, which itself extends LifecycleOwner from the Essenty library. This means every component context exposes a lifecycle property that components can observe for create, start, resume, pause, stop, and destroy events. The lifecycle is typically provided by DefaultComponentContext or a custom implementation.

How does MergedLifecycle handle parent destruction?

When the parent lifecycle reaches the DESTROYED state, MergedLifecycle immediately moves the merged state to DESTROYED regardless of the child's state. The class automatically unsubscribes its observers from both parent and child lifecycles upon destruction to prevent memory leaks. This ensures that child components are properly cleaned up when their parent is removed from the navigation stack.

Can I create a ComponentContext without DefaultComponentContext?

Yes, you can implement the ComponentContext interface directly or use the GenericComponentContext interface if you need custom behavior. However, DefaultComponentContext is the recommended implementation as it correctly wires together StateKeeperDispatcher, InstanceKeeperDispatcher, and BackDispatcher. If you provide custom implementations of these dispatchers, you can pass them directly to the DefaultComponentContext constructor.

Where does the actual lifecycle state management happen in Decompose?

The actual state synchronization logic resides in MergedLifecycle.kt, specifically within the CallbacksImpl inner class that observes both parent and child lifecycles. The moveTo function updates the internal LifecycleRegistry to reflect the minimum state of the two sources. This file is imported and used by ChildController.kt when creating child components in navigation stacks.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →