# How Decompose Retains Component Instances Across Configuration Changes

> Discover how Decompose retains component instances across configuration changes using Essenty InstanceKeeper, ensuring state persistence and a smooth user experience. Learn more

- Repository: [Arkadii Ivanov/decompose](https://github.com/arkivanov/decompose)
- Tags: internals
- Published: 2026-02-25

---

**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`:

```kotlin
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.

## Navigation-Level Retention Architecture

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`](https://github.com/arkivanov/decompose/blob/main/ComponentContextFactoryOwner.kt)). Inside [`ChildController.kt`](https://github.com/arkivanov/decompose/blob/main/ChildController.kt) (lines 29-33), the child obtains a `RetainedInstance` that stores a map of `configuration → InstanceKeeperDispatcher`:

```kotlin
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`](https://github.com/arkivanov/decompose/blob/main/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`](https://github.com/arkivanov/decompose/blob/main/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`](https://github.com/arkivanov/decompose/blob/main/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:

```kotlin
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:

```kotlin
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:

```kotlin
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`](https://github.com/arkivanov/decompose/blob/main/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`](https://github.com/arkivanov/decompose/blob/main/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`](https://github.com/arkivanov/decompose/blob/main/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.