How Decompose Retains Component Instances Across Configuration Changes

Decompose retains component instances by leveraging the Essenty InstanceKeeper API embedded in every ComponentContext, allowing objects to survive configuration changes through a retained instance delegate that stores them outside the normal component lifecycle.

Decompose, the Kotlin Multiplatform library maintained in the arkivanov/decompose repository, provides a robust solution for instance retaining over configuration changes. Through tight integration with the Essenty library, Decompose ensures that heavy objects and navigation state survive Android activity recreations without memory leaks. This mechanism centers on the ComponentContext interface, which exposes an instanceKeeper property that components use to persist objects across the destroy/create cycle.

The Core Mechanism: Essenty InstanceKeeper API

At the foundation of Decompose's retention strategy is the Essenty InstanceKeeper API. Every ComponentContext provides an instanceKeeper property that acts as a storage container for objects that must outlive the component itself.

The retainedInstance Delegate

Components access this storage through the retainedInstance delegate, which creates or retrieves an existing InstanceKeeper.Instance:

val myRetained = componentContext.retainedInstance { MyRetained() }

This delegate stores objects outside the component's normal lifecycle. During a configuration change, while the top-level ComponentContext is recreated, the underlying InstanceKeeper hierarchy remains preserved because it attaches to a ViewModelStoreOwner (Jetpack) or the Android SavedStateRegistry via Essenty. Consequently, any objects obtained through instanceKeeper.getOrCreate survive recreation and are supplied to the new component instance.

Decompose implements a layered retention strategy for navigation models such as childStack, childSlot, and childPages. The system retains not just simple objects, but entire child component hierarchies and their associated state.

ChildController and Per-Configuration Retention

When a navigation model instantiates a child component, it uses a ComponentContextFactory (see ComponentContextFactoryOwner.kt). Inside ChildController.kt (lines 29-33), the child obtains a RetainedInstance that stores a map of configuration → InstanceKeeperDispatcher:

private val retainedInstance = componentContext.retainedInstance<RetainedInstance<C>>(
    key = key,
    factory = ::RetainedInstance
)

This ensures each child configuration maintains its own isolated retention scope.

ChildrenNavigator Aggregation

The ChildrenNavigator class (see ChildrenNavigator.kt, lines 285-292) maintains a MutableList<ChildItem<C, T>> inside a RetainedInstance<C, T> that implements InstanceKeeper.Instance. When the navigator is destroyed, the onDestroy() method disposes all retained dispatchers, guaranteeing proper cleanup of resources that would otherwise leak across configuration changes.

Android Integration with ViewModelStore

On Android, the top-level retention bridges to the Jetpack ViewModel system. The DefaultJetpackComponentContext (see DefaultJetpackComponentContext.kt, line 44) creates a ViewModelStore as a retained instance itself, using retainedInstance with key KEY.

When an activity recreates, the same ViewModelStore is retrieved, the InstanceKeeper hierarchy reconstructs, and componentContext.retainedInstance { ... } returns the previously stored object rather than creating a new one.

Optional Persistence Control

Components can opt out of retention by declaring isStateSavingAllowed = false or discardSavedState = true. The RetainedComponentSingleTest.kt suite verifies this behavior, including test cases such as GIVEN_isStateSavingAllowed_is_false_on_save_WHEN_configuration_changed_THEN_instances_not_retained, confirming that Decompose skips retention when explicitly disabled.

Practical Code Examples

Retaining a Simple Counter

The following component retains a mutable counter across configuration changes:

class CounterComponent(
    componentContext: ComponentContext
) : Component {
    // Retain a mutable counter across configuration changes
    private val counter = componentContext.retainedInstance {
        mutableStateOf(0)
    }

    fun increment() {
        counter.value++
    }

    fun value(): Int = counter.value
}

After recreation, the new component instance accesses the same counter:

val rootContext = DefaultComponentContext()               // top-level context
val counter = CounterComponent(rootContext)               // instance is retained
counter.increment()
println(counter.value()) // → 1
// After Android configuration change, a new CounterComponent is created
val recreated = CounterComponent(rootContext)              // same retained counter
println(recreated.value()) // → 1 (value preserved)

Retaining Heavy Objects in Child Navigation

When using childStack, each child component receives its own ComponentContext with retention capabilities:

val childStack = childStack(
    source = navigationSource,
    initialStack = { listOf(ConfigA) },
    childFactory = { config, ctx ->
        when (config) {
            is ConfigA -> ScreenA(ctx)          // ctx is a child ComponentContext
            is ConfigB -> ScreenB(ctx)
        }
    }
)

// Inside a child component
class ScreenA(componentContext: ComponentContext) {
    // Retain a heavy object (e.g., a database helper)
    private val dbHelper = componentContext.instanceKeeper.getOrCreate {
        DatabaseHelper(componentContext)
    }
}

Even when the host activity recreates, the dbHelper instance is fetched from the same InstanceKeeper and is not recreated, preventing expensive re-initialization of database connections.

Summary

  • Essenty InstanceKeeper provides the generic retention API that powers Decompose's instance survival mechanism according to the arkivanov/decompose source code.
  • ComponentContext exposes instanceKeeper to component code through the retainedInstance delegate, storing objects outside the normal lifecycle.
  • Navigation models store per-child dispatchers inside RetainedInstance holders managed by ChildController and aggregated by ChildrenNavigator.
  • The Android integration attaches the keeper to a ViewModelStore, enabling process-level configuration change survival.
  • Retention can be selectively disabled using isStateSavingAllowed = false for components that should not survive recreation.

Frequently Asked Questions

How does Decompose differ from standard Android ViewModel retention?

Decompose abstracts retention through the InstanceKeeper interface rather than requiring direct ViewModel inheritance. While DefaultJetpackComponentContext uses a ViewModelStore internally (as seen in DefaultJetpackComponentContext.kt), components interact with a platform-agnostic ComponentContext.instanceKeeper API. This allows the same retention code to work across Android, iOS, Desktop, and Web without platform-specific modifications.

What happens to retained instances when the user fully destroys the activity?

When the activity is finished (not just recreated), the InstanceKeeper hierarchy invokes onDestroy() on all retained InstanceKeeper.Instance objects. In ChildrenNavigator.kt, this triggers disposal of all InstanceKeeperDispatcher instances associated with child components, ensuring that resources like database connections or coroutine scopes are properly cleaned up rather than leaked.

Can I retain instances in nested child components?

Yes. Each child component created by childStack, childSlot, or childPages receives its own isolated ComponentContext via the ComponentContextFactory. According to ChildController.kt, each configuration key maps to its own RetainedInstance, creating independent retention scopes. This allows granular control where parent and child components retain different objects with independent lifecycles.

Does instance retaining work on platforms other than Android?

Yes. While the analysis focuses on Android's DefaultJetpackComponentContext and ViewModelStore, the InstanceKeeper API is part of Essenty's common multiplatform code. On non-Android platforms, the retaining mechanism depends on the platform-specific ComponentContext implementation, but the retainedInstance delegate and instanceKeeper API remain consistent, allowing you to write retention logic that compiles across all Kotlin Multiplatform targets.

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 →