Repository: arkivanov/Decompose
Pull request #241 (2022-10-23 20:50:38)

Added Children generic navigation model
Resolves #234.


Patch:
diff --git a/decompose/api/android/decompose.api b/decompose/api/android/decompose.api
index 7bdc249e..0344efa5 100644
--- a/decompose/api/android/decompose.api
+++ b/decompose/api/android/decompose.api
@@ -7,6 +7,7 @@ public final class com/arkivanov/decompose/BuildConfig {
 
 public abstract class com/arkivanov/decompose/Child {
 	public abstract fun getConfiguration ()Ljava/lang/Object;
+	public abstract fun getInstance ()Ljava/lang/Object;
 }
 
 public final class com/arkivanov/decompose/Child$Created : com/arkivanov/decompose/Child {
@@ -17,7 +18,7 @@ public final class com/arkivanov/decompose/Child$Created : com/arkivanov/decompo
 	public static synthetic fun copy$default (Lcom/arkivanov/decompose/Child$Created;Ljava/lang/Object;Ljava/lang/Object;ILjava/lang/Object;)Lcom/arkivanov/decompose/Child$Created;
 	public fun equals (Ljava/lang/Object;)Z
 	public fun getConfiguration ()Ljava/lang/Object;
-	public final fun getInstance ()Ljava/lang/Object;
+	public fun getInstance ()Ljava/lang/Object;
 	public fun hashCode ()I
 	public fun toString ()Ljava/lang/String;
 }
@@ -29,6 +30,8 @@ public final class com/arkivanov/decompose/Child$Destroyed : com/arkivanov/decom
 	public static synthetic fun copy$default (Lcom/arkivanov/decompose/Child$Destroyed;Ljava/lang/Object;ILjava/lang/Object;)Lcom/arkivanov/decompose/Child$Destroyed;
 	public fun equals (Ljava/lang/Object;)Z
 	public fun getConfiguration ()Ljava/lang/Object;
+	public synthetic fun getInstance ()Ljava/lang/Object;
+	public fun getInstance ()Ljava/lang/Void;
 	public fun hashCode ()I
 	public fun toString ()Ljava/lang/String;
 }
@@ -70,6 +73,40 @@ public final class com/arkivanov/decompose/lifecycle/MergedLifecycle : com/arkiv
 	public fun unsubscribe (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V
 }
 
+public abstract interface class com/arkivanov/decompose/router/children/ChildNavState {
+	public abstract fun getConfiguration ()Ljava/lang/Object;
+	public abstract fun getStatus ()Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
+}
+
+public final class com/arkivanov/decompose/router/children/ChildNavState$Status : java/lang/Enum {
+	public static final field ACTIVE Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
+	public static final field DESTROYED Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
+	public static final field INACTIVE Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
+	public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
+	public static fun values ()[Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
+}
+
+public final class com/arkivanov/decompose/router/children/ChildrenFactoryKt {
+	public static final fun children (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/children/NavigationSource;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value;
+}
+
+public abstract interface class com/arkivanov/decompose/router/children/NavState {
+	public abstract fun getChildren ()Ljava/util/List;
+}
+
+public abstract interface class com/arkivanov/decompose/router/children/NavigationSource {
+	public abstract fun subscribe (Lkotlin/jvm/functions/Function1;)V
+	public abstract fun unsubscribe (Lkotlin/jvm/functions/Function1;)V
+}
+
+public final class com/arkivanov/decompose/router/children/SavedState$Creator : android/os/Parcelable$Creator {
+	public fun <init> ()V
+	public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arkivanov/decompose/router/children/SavedState;
+	public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
+	public final fun newArray (I)[Lcom/arkivanov/decompose/router/children/SavedState;
+	public synthetic fun newArray (I)[Ljava/lang/Object;
+}
+
 public final class com/arkivanov/decompose/router/overlay/ChildOverlay {
 	public fun <init> ()V
 	public fun <init> (Lcom/arkivanov/decompose/Child$Created;)V
diff --git a/decompose/api/jvm/decompose.api b/decompose/api/jvm/decompose.api
index 55cd07ca..aaad04cf 100644
--- a/decompose/api/jvm/decompose.api
+++ b/decompose/api/jvm/decompose.api
@@ -1,5 +1,6 @@
 public abstract class com/arkivanov/decompose/Child {
 	public abstract fun getConfiguration ()Ljava/lang/Object;
+	public abstract fun getInstance ()Ljava/lang/Object;
 }
 
 public final class com/arkivanov/decompose/Child$Created : com/arkivanov/decompose/Child {
@@ -10,7 +11,7 @@ public final class com/arkivanov/decompose/Child$Created : com/arkivanov/decompo
 	public static synthetic fun copy$default (Lcom/arkivanov/decompose/Child$Created;Ljava/lang/Object;Ljava/lang/Object;ILjava/lang/Object;)Lcom/arkivanov/decompose/Child$Created;
 	public fun equals (Ljava/lang/Object;)Z
 	public fun getConfiguration ()Ljava/lang/Object;
-	public final fun getInstance ()Ljava/lang/Object;
+	public fun getInstance ()Ljava/lang/Object;
 	public fun hashCode ()I
 	public fun toString ()Ljava/lang/String;
 }
@@ -22,6 +23,8 @@ public final class com/arkivanov/decompose/Child$Destroyed : com/arkivanov/decom
 	public static synthetic fun copy$default (Lcom/arkivanov/decompose/Child$Destroyed;Ljava/lang/Object;ILjava/lang/Object;)Lcom/arkivanov/decompose/Child$Destroyed;
 	public fun equals (Ljava/lang/Object;)Z
 	public fun getConfiguration ()Ljava/lang/Object;
+	public synthetic fun getInstance ()Ljava/lang/Object;
+	public fun getInstance ()Ljava/lang/Void;
 	public fun hashCode ()I
 	public fun toString ()Ljava/lang/String;
 }
@@ -57,6 +60,32 @@ public final class com/arkivanov/decompose/lifecycle/MergedLifecycle : com/arkiv
 	public fun unsubscribe (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V
 }
 
+public abstract interface class com/arkivanov/decompose/router/children/ChildNavState {
+	public abstract fun getConfiguration ()Ljava/lang/Object;
+	public abstract fun getStatus ()Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
+}
+
+public final class com/arkivanov/decompose/router/children/ChildNavState$Status : java/lang/Enum {
+	public static final field ACTIVE Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
+	public static final field DESTROYED Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
+	public static final field INACTIVE Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
+	public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
+	public static fun values ()[Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
+}
+
+public final class com/arkivanov/decompose/router/children/ChildrenFactoryKt {
+	public static final fun children (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/children/NavigationSource;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value;
+}
+
+public abstract interface class com/arkivanov/decompose/router/children/NavState {
+	public abstract fun getChildren ()Ljava/util/List;
+}
+
+public abstract interface class com/arkivanov/decompose/router/children/NavigationSource {
+	public abstract fun subscribe (Lkotlin/jvm/functions/Function1;)V
+	public abstract fun unsubscribe (Lkotlin/jvm/functions/Function1;)V
+}
+
 public final class com/arkivanov/decompose/router/overlay/ChildOverlay {
 	public fun <init> ()V
 	public fun <init> (Lcom/arkivanov/decompose/Child$Created;)V
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Child.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Child.kt
index 44c4652d..8d14111a 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Child.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Child.kt
@@ -3,13 +3,16 @@ package com.arkivanov.decompose
 sealed class Child<out C : Any, out T : Any> {
 
     abstract val configuration: C
+    abstract val instance: T?
 
     data class Created<out C : Any, out T : Any>(
         override val configuration: C,
-        val instance: T
+        override val instance: T,
     ) : Child<C, T>()
 
     data class Destroyed<out C : Any>(
-        override val configuration: C
-    ) : Child<C, Nothing>()
+        override val configuration: C,
+    ) : Child<C, Nothing>() {
+        override val instance: Nothing? = null
+    }
 }
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/backhandler/DefaultChildBackHandler.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/backhandler/DefaultChildBackHandler.kt
index 10d87aa4..96b05bb7 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/backhandler/DefaultChildBackHandler.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/backhandler/DefaultChildBackHandler.kt
@@ -14,13 +14,20 @@ internal class DefaultChildBackHandler(
     private val enabledChangedListener: (Boolean) -> Unit = { updateParentCallbackEnabledState() }
 
     override var isEnabled: Boolean by observable(isEnabled) { _, _, _ -> updateParentCallbackEnabledState() }
+    private var isStarted = false
 
     override fun start() {
-        parent.register(parentCallback)
+        if (!isStarted) {
+            isStarted = true
+            parent.register(parentCallback)
+        }
     }
 
     override fun stop() {
-        parent.unregister(parentCallback)
+        if (isStarted) {
+            isStarted = false
+            parent.unregister(parentCallback)
+        }
     }
 
     override fun register(callback: BackCallback) {
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildItem.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildItem.kt
new file mode 100644
index 00000000..ea6d6c3a
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildItem.kt
@@ -0,0 +1,35 @@
+package com.arkivanov.decompose.router.children
+
+import com.arkivanov.decompose.backhandler.ChildBackHandler
+import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
+import com.arkivanov.essenty.lifecycle.LifecycleRegistry
+import com.arkivanov.essenty.parcelable.ParcelableContainer
+import com.arkivanov.essenty.statekeeper.StateKeeperDispatcher
+
+internal sealed interface ChildItem<out C : Any, out T : Any> {
+
+    val configuration: C
+    val instance: T?
+    val savedState: ParcelableContainer?
+
+    data class Created<out C : Any, out T : Any>(
+        override val configuration: C,
+        override val savedState: ParcelableContainer? = null,
+        override val instance: T,
+        val lifecycleRegistry: LifecycleRegistry,
+        val stateKeeperDispatcher: StateKeeperDispatcher,
+        val instanceKeeperDispatcher: InstanceKeeperDispatcher,
+        val backHandler: ChildBackHandler,
+    ) : ChildItem<C, T> {
+        enum class ChildStatus {
+            INACTIVE, ACTIVE
+        }
+    }
+
+    data class Destroyed<out C : Any>(
+        override val configuration: C,
+        override val savedState: ParcelableContainer? = null
+    ) : ChildItem<C, Nothing> {
+        override val instance: Nothing? = null
+    }
+}
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildItemFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildItemFactory.kt
new file mode 100644
index 00000000..e7d2d0ea
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildItemFactory.kt
@@ -0,0 +1,13 @@
+package com.arkivanov.decompose.router.children
+
+import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
+import com.arkivanov.essenty.parcelable.ParcelableContainer
+
+internal interface ChildItemFactory<C : Any, out T : Any> {
+
+    operator fun invoke(
+        configuration: C,
+        savedState: ParcelableContainer? = null,
+        instanceKeeperDispatcher: InstanceKeeperDispatcher? = null,
+    ): ChildItem.Created<C, T>
+}
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildNavState.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildNavState.kt
new file mode 100644
index 00000000..10e72300
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildNavState.kt
@@ -0,0 +1,39 @@
+package com.arkivanov.decompose.router.children
+
+/**
+ * Represents a child navigation state.
+ */
+interface ChildNavState<out C : Any> {
+
+    /**
+     * A configuration of the child. Must be unique within the [NavState].
+     */
+    val configuration: C
+
+    /**
+     * Required lifecycle status of the child.
+     */
+    val status: Status
+
+    /**
+     * Enumerates all possible child lifecycle statuses.
+     */
+    enum class Status {
+        /**
+         * The child is destroyed but still managed, e.g. it's state may be saved and restored later.
+         */
+        DESTROYED,
+
+        /**
+         * The child is created but not started. The state is saved when the child switches from
+         * [ACTIVE] to [INACTIVE]. Back button handling is disabled.
+         */
+        INACTIVE,
+
+        /**
+         * The child is created and may be resumed, depending on the lifecycle of the hosting component.
+         * Back button handling is enabled.
+         */
+        ACTIVE,
+    }
+}
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenFactory.kt
new file mode 100644
index 00000000..96d6b838
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenFactory.kt
@@ -0,0 +1,137 @@
+package com.arkivanov.decompose.router.children
+
+import com.arkivanov.decompose.Child
+import com.arkivanov.decompose.ComponentContext
+import com.arkivanov.decompose.backhandler.child
+import com.arkivanov.decompose.value.MutableValue
+import com.arkivanov.decompose.value.Value
+import com.arkivanov.essenty.backhandler.BackCallback
+import com.arkivanov.essenty.instancekeeper.getOrCreate
+import com.arkivanov.essenty.lifecycle.doOnDestroy
+import com.arkivanov.essenty.parcelable.Parcelable
+import com.arkivanov.essenty.parcelable.ParcelableContainer
+import com.arkivanov.essenty.parcelable.Parcelize
+import com.arkivanov.essenty.statekeeper.consume
+
+/**
+ * Initialised and manages a generic list of components. This is an API for custom navigation models.
+ * Please consider the existing navigation models first:
+ * [childStack][com.arkivanov.decompose.router.stack.childStack],
+ * [childOverlay][com.arkivanov.decompose.router.overlay.childOverlay].
+ *
+ * The API is based around [NavState] and [ChildNavState] interfaces that should be implemented by
+ * clients. [NavState] represents a persistent state of the navigation. It also holds a navigation
+ * state for each child - [ChildNavState]. Both [NavState] and [ChildNavState] must be immutable, and
+ * correctly implement `equals` and `hashCode` methods (or just be data classes). There must be no
+ * duplicated (be equality) [ChildNavState.configuration] within a [NavState].
+ *
+ * The navigation is performed by transforming the current [NavState] to a new one. The implementation
+ * calculates diffs between the old and the new [NavState], and manipulates child components as needed.
+ *
+ * @param C a type of component configurations.
+ * @param T a type of components.
+ * @param E a type of navigation events.
+ * @param N a type of navigation state, must implement [NavState] interface.
+ * @param S a type of the resulting children state.
+ * @param source a source of navigation events.
+ * @param key a key of this `children` collection, must be unique if there are multiple
+ * `children` used in the same component.
+ * @param initialNavState an initial navigation state that should be used if there is no previously saved state.
+ * @param saveNavState a function that saves the provided navigation state into [ParcelableContainer].
+ * @param restoreNavState a function that restores the navigation state from the provided [ParcelableContainer].
+ * The restored navigation state must have the same amount of child configurations and in the same order.
+ * The restored child [Statuses][ChildNavState.Status] can be any, e.g. a previously active child may become
+ * destroyed, etc.
+ * @param navTransformer a function that transforms the current navigation state to a new one using the provided
+ * navigation event.
+ * @param onEventComplete called when a navigation event is processed and the navigation completed.
+ * @param backTransformer a function that checks the provided navigation state, and either returns another function
+ * transforming the navigation state to a new one, or `null` if back button handling should be disabled. Called
+ * during the initialisation and after each navigation event.
+ * @param stateMapper combines the provided navigation state and list of child components to a resulting state.
+ * @param childFactory a factory function that creates new child component instances.
+ * @return an observable [Value] of the resulting children state.
+ */
+fun <C : Any, T : Any, E : Any, N : NavState<C>, S : Any> ComponentContext.children(
+    source: NavigationSource<E>,
+    key: String,
+    initialNavState: () -> N,
+    saveNavState: (navState: N) -> ParcelableContainer,
+    restoreNavState: (container: ParcelableContainer) -> N,
+    navTransformer: (navState: N, event: E) -> N,
+    onEventComplete: (event: E, newNavState: N, oldNavState: N) -> Unit,
+    backTransformer: (navState: N) -> (() -> N)?,
+    stateMapper: (navState: N, children: List<Child<C, T>>) -> S,
+    childFactory: (configuration: C, componentContext: ComponentContext) -> T,
+): Value<S> {
+    val mainBackHandler = backHandler.child()
+
+    val savedState = stateKeeper.consume<SavedState>(key = key)
+
+    val navigator =
+        ChildrenNavigator(
+            lifecycle = lifecycle,
+            retainedInstanceSupplier = { factory -> instanceKeeper.getOrCreate(key = key, factory = factory) },
+            childItemFactory = DefaultChildItemFactory(
+                lifecycle = lifecycle,
+                backHandler = backHandler.child(),
+                childFactory = childFactory,
+            ),
+            navState = savedState?.navState?.let(restoreNavState) ?: initialNavState(),
+            savedChildState = savedState?.childState,
+        )
+
+    stateKeeper.register(key = key) {
+        SavedState(
+            navState = saveNavState(navigator.navState),
+            childState = navigator.saveChildState(),
+        )
+    }
+
+    val state = MutableValue(stateMapper(navigator.navState, navigator.children))
+
+    var bt: (() -> N)? = backTransformer(navigator.navState)
+
+    lateinit var backCallback: BackCallback
+
+    fun onAfterNavigate(navState: N) {
+        bt = backTransformer(navState)
+        backCallback.isEnabled = bt != null
+        state.value = stateMapper(navState, navigator.children)
+    }
+
+    backCallback =
+        BackCallback(isEnabled = bt != null) {
+            bt?.invoke()?.also { navState ->
+                navigator.navigate(navState = navState)
+                onAfterNavigate(navState)
+            }
+        }
+
+    mainBackHandler.register(backCallback)
+
+    val eventObserver: (E) -> Unit =
+        { event ->
+            val oldNavState = navigator.navState
+            navigator.navigate(navState = navTransformer(navigator.navState, event))
+            val newNavState = navigator.navState
+            onAfterNavigate(newNavState)
+            onEventComplete(event, newNavState, oldNavState)
+        }
+
+    source.subscribe(eventObserver)
+
+    lifecycle.doOnDestroy {
+        source.unsubscribe(eventObserver)
+        stateKeeper.unregister(key = key)
+        mainBackHandler.unregister(backCallback)
+    }
+
+    return state
+}
+
+@Parcelize
+private class SavedState(
+    val navState: ParcelableContainer,
+    val childState: List<ParcelableContainer?>,
+) : Parcelable
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenNavigator.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenNavigator.kt
new file mode 100644
index 00000000..eaeb0160
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenNavigator.kt
@@ -0,0 +1,259 @@
+package com.arkivanov.decompose.router.children
+
+import com.arkivanov.decompose.Child
+import com.arkivanov.decompose.router.children.ChildItem.Created
+import com.arkivanov.decompose.router.children.ChildItem.Created.*
+import com.arkivanov.decompose.router.children.ChildNavState.Status
+import com.arkivanov.essenty.instancekeeper.InstanceKeeper
+import com.arkivanov.essenty.lifecycle.Lifecycle
+import com.arkivanov.essenty.lifecycle.create
+import com.arkivanov.essenty.lifecycle.destroy
+import com.arkivanov.essenty.lifecycle.doOnDestroy
+import com.arkivanov.essenty.lifecycle.resume
+import com.arkivanov.essenty.lifecycle.stop
+import com.arkivanov.essenty.parcelable.ParcelableContainer
+
+internal class ChildrenNavigator<out C : Any, out T : Any, N : NavState<C>>(
+    lifecycle: Lifecycle,
+    retainedInstanceSupplier: (factory: () -> InstanceKeeper.Instance) -> InstanceKeeper.Instance,
+    private val childItemFactory: ChildItemFactory<C, T>,
+    navState: N,
+    savedChildState: List<ParcelableContainer?>?,
+) {
+    var navState: N = navState
+        private set
+
+    private val items = ArrayList<ChildItem<C, T>>()
+
+    val children: List<Child<C, T>>
+        get() =
+            items.map { item ->
+                val instance = item.instance
+                if (instance != null) {
+                    Child.Created(configuration = item.configuration, instance = instance)
+                } else {
+                    Child.Destroyed(configuration = item.configuration)
+                }
+            }
+
+    @Suppress("UNCHECKED_CAST")
+    private val retainedInstance = retainedInstanceSupplier { RetainedInstance<C, T>() } as RetainedInstance<C, T>
+
+    init {
+        if (savedChildState == null) {
+            retainedInstance.onDestroy()
+            switch(newStates = navState.children)
+        } else {
+            restore(navState = navState, savedStates = savedChildState)
+        }
+
+        lifecycle.doOnDestroy {
+            items.forEach { item ->
+                when (item) {
+                    is Created -> {
+                        item.backHandler.stop()
+                        item.lifecycleRegistry.destroy()
+                    }
+
+                    is ChildItem.Destroyed -> Unit
+                }
+            }
+        }
+    }
+
+    private fun restore(navState: N, savedStates: List<ParcelableContainer?>) {
+        val retainedChildren = retainedInstance.items.associateByTo(HashMap(), Created<C, *>::configuration)
+        retainedInstance.items.clear()
+
+        navState.children.zip(savedStates).forEach { (childNavState, savedState) ->
+            items +=
+                when (childNavState.status) {
+                    Status.DESTROYED -> ChildItem.Destroyed(configuration = childNavState.configuration, savedState = savedState)
+
+                    Status.INACTIVE -> {
+                        childItemFactory(
+                            configuration = childNavState.configuration,
+                            savedState = savedState,
+                            instanceKeeperDispatcher = retainedChildren.remove(childNavState.configuration)?.instanceKeeperDispatcher,
+                        ).also {
+                            retainedInstance.items += it
+                            it.lifecycleRegistry.create()
+                        }
+                    }
+
+                    Status.ACTIVE ->
+                        childItemFactory(
+                            configuration = childNavState.configuration,
+                            savedState = savedState,
+                            instanceKeeperDispatcher = retainedChildren.remove(childNavState.configuration)?.instanceKeeperDispatcher,
+                        ).also {
+                            it.backHandler.start()
+                            retainedInstance.items += it
+                            it.lifecycleRegistry.resume()
+                        }
+                }
+        }
+
+        retainedChildren.values.forEach { it.instanceKeeperDispatcher.destroy() }
+    }
+
+    fun saveChildState(): List<ParcelableContainer?> =
+        items.map { item ->
+            when (item) {
+                is Created -> item.saveStateIfNeeded()
+                is ChildItem.Destroyed -> item.savedState
+            }
+        }
+
+    fun navigate(navState: N) {
+        switch(newStates = navState.children)
+        this.navState = navState
+    }
+
+    private fun switch(newStates: List<ChildNavState<C>>) {
+        val oldItems = items.associateBy(ChildItem<C, *>::configuration)
+        val newItems = prepareNewItems(newStates = newStates, oldItems = oldItems)
+        destroyOldItems(newStates = newStates, oldItems = oldItems.values)
+        processNewItems(newItems = newItems)
+    }
+
+    private fun prepareNewItems(
+        newStates: List<ChildNavState<C>>,
+        oldItems: Map<C, ChildItem<C, T>>,
+    ): List<Pair<ChildItem<C, T>, Status>> {
+        val newItems = ArrayList<Pair<ChildItem<C, T>, Status>>(newStates.size)
+
+        newStates.forEach { state ->
+            newItems +=
+                when (val child = oldItems[state.configuration]) {
+                    is Created -> child to state.status
+
+                    is ChildItem.Destroyed ->
+                        when (state.status) {
+                            Status.DESTROYED -> child to state.status
+
+                            Status.INACTIVE ->
+                                Pair(
+                                    first = childItemFactory(
+                                        configuration = state.configuration,
+                                        savedState = child.savedState
+                                    ).apply { lifecycleRegistry.create() },
+                                    second = state.status,
+                                )
+
+                            Status.ACTIVE ->
+                                Pair(
+                                    first = childItemFactory(
+                                        configuration = state.configuration,
+                                        savedState = child.savedState,
+                                    ).apply { lifecycleRegistry.create() },
+                                    second = state.status,
+                                )
+                        }
+
+                    null ->
+                        when (state.status) {
+                            Status.DESTROYED -> ChildItem.Destroyed(configuration = state.configuration) to state.status
+
+                            Status.INACTIVE ->
+                                Pair(
+                                    first = childItemFactory(configuration = state.configuration)
+                                        .apply { lifecycleRegistry.create() },
+                                    second = state.status,
+                                )
+
+                            Status.ACTIVE ->
+                                Pair(
+                                    first = childItemFactory(configuration = state.configuration)
+                                        .apply { lifecycleRegistry.create() },
+                                    second = state.status,
+                                )
+                        }
+                }
+        }
+
+        return newItems
+    }
+
+    private fun destroyOldItems(
+        newStates: List<ChildNavState<C>>,
+        oldItems: Collection<ChildItem<C, T>>,
+    ) {
+        val newStatesSet = newStates.mapTo(HashSet(), ChildNavState<C>::configuration)
+        for (item in oldItems) {
+            val child = item as? Created ?: continue
+            if (item.configuration !in newStatesSet) {
+                child.backHandler.stop()
+                child.instanceKeeperDispatcher.destroy()
+                child.lifecycleRegistry.destroy()
+            }
+        }
+    }
+
+    private fun Created<*, *>.saveStateIfNeeded(): ParcelableContainer? =
+        if (lifecycleRegistry.state > Lifecycle.State.CREATED) {
+            stateKeeperDispatcher.save()
+        } else {
+            savedState
+        }
+
+    private fun processNewItems(newItems: List<Pair<ChildItem<C, T>, Status>>) {
+        items.clear()
+        retainedInstance.items.clear()
+
+        newItems.forEach { (item, status) ->
+            items +=
+                when (item) {
+                    is Created -> {
+                        when (status) {
+                            Status.DESTROYED -> {
+                                val savedState = item.saveStateIfNeeded()
+                                item.backHandler.stop()
+                                item.instanceKeeperDispatcher.destroy()
+                                item.lifecycleRegistry.destroy()
+
+                                ChildItem.Destroyed(configuration = item.configuration, savedState = savedState)
+                            }
+
+                            Status.INACTIVE -> {
+                                retainedInstance.items += item
+
+                                item
+                                    .takeIf { it.lifecycleRegistry.state != Lifecycle.State.CREATED }
+                                    ?.copy(savedState = item.saveStateIfNeeded())
+                                    ?.apply {
+                                        backHandler.stop()
+                                        lifecycleRegistry.stop()
+                                    }
+                                    ?: item
+                            }
+
+                            Status.ACTIVE -> {
+                                retainedInstance.items += item
+
+                                item
+                                    .takeIf { it.lifecycleRegistry.state != Lifecycle.State.RESUMED }
+                                    ?.copy(savedState = null)
+                                    ?.apply {
+                                        backHandler.start()
+                                        lifecycleRegistry.resume()
+                                    }
+                                    ?: item
+                            }
+                        }
+                    }
+
+                    is ChildItem.Destroyed -> item
+                }
+        }
+    }
+
+    private class RetainedInstance<C : Any, T : Any> : InstanceKeeper.Instance {
+        val items: MutableList<Created<C, T>> = ArrayList()
+
+        override fun onDestroy() {
+            items.forEach { it.instanceKeeperDispatcher.destroy() }
+            items.clear()
+        }
+    }
+}
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/DefaultChildItemFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/DefaultChildItemFactory.kt
new file mode 100644
index 00000000..0bdf7e85
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/DefaultChildItemFactory.kt
@@ -0,0 +1,51 @@
+package com.arkivanov.decompose.router.children
+
+import com.arkivanov.decompose.ComponentContext
+import com.arkivanov.decompose.DefaultComponentContext
+import com.arkivanov.decompose.backhandler.childBackHandler
+import com.arkivanov.decompose.lifecycle.MergedLifecycle
+import com.arkivanov.essenty.backhandler.BackHandler
+import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
+import com.arkivanov.essenty.lifecycle.Lifecycle
+import com.arkivanov.essenty.lifecycle.LifecycleRegistry
+import com.arkivanov.essenty.parcelable.ParcelableContainer
+import com.arkivanov.essenty.statekeeper.StateKeeperDispatcher
+
+internal class DefaultChildItemFactory<C : Any, out T : Any>(
+    private val lifecycle: Lifecycle,
+    private val backHandler: BackHandler,
+    private val childFactory: (configuration: C, ComponentContext) -> T,
+) : ChildItemFactory<C, T> {
+
+    override fun invoke(
+        configuration: C,
+        savedState: ParcelableContainer?,
+        instanceKeeperDispatcher: InstanceKeeperDispatcher?
+    ): ChildItem.Created<C, T> {
+        val componentLifecycleRegistry = LifecycleRegistry()
+        val mergedLifecycle = MergedLifecycle(lifecycle, componentLifecycleRegistry)
+        val stateKeeperDispatcher = StateKeeperDispatcher(savedState)
+        val instanceKeeperRegistry = instanceKeeperDispatcher ?: InstanceKeeperDispatcher()
+        val backHandler = backHandler.childBackHandler()
+
+        val component =
+            childFactory(
+                configuration,
+                DefaultComponentContext(
+                    lifecycle = mergedLifecycle,
+                    stateKeeper = stateKeeperDispatcher,
+                    instanceKeeper = instanceKeeperRegistry,
+                    backHandler = backHandler,
+                )
+            )
+
+        return ChildItem.Created(
+            configuration = configuration,
+            instance = component,
+            lifecycleRegistry = componentLifecycleRegistry,
+            stateKeeperDispatcher = stateKeeperDispatcher,
+            instanceKeeperDispatcher = instanceKeeperRegistry,
+            backHandler = backHandler,
+        )
+    }
+}
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/NavState.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/NavState.kt
new file mode 100644
index 00000000..b69198cc
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/NavState.kt
@@ -0,0 +1,12 @@
+package com.arkivanov.decompose.router.children
+
+/**
+ * Represents an entire navigation state.
+ */
+interface NavState<out C : Any> {
+
+    /**
+     * A list of child navigation states. Every [ChildNavState.configuration] must be unique by equality.
+     */
+    val children: List<ChildNavState<C>>
+}
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/NavigationSource.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/NavigationSource.kt
new file mode 100644
index 00000000..e29ac492
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/NavigationSource.kt
@@ -0,0 +1,13 @@
+package com.arkivanov.decompose.router.children
+
+/**
+ * Represents a generic source of navigation events.
+ *
+ * @see [children]
+ */
+interface NavigationSource<out T : Any> {
+
+    fun subscribe(observer: (T) -> Unit)
+
+    fun unsubscribe(observer: (T) -> Unit)
+}
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/SimpleChildNavState.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/SimpleChildNavState.kt
new file mode 100644
index 00000000..14f32778
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/SimpleChildNavState.kt
@@ -0,0 +1,9 @@
+package com.arkivanov.decompose.router.children
+
+/**
+ * A simple implementation of the [ChildNavState] interface.
+ */
+internal data class SimpleChildNavState<out C : Any>(
+    override val configuration: C,
+    override val status: ChildNavState.Status,
+) : ChildNavState<C>


Test patch:
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenBackPressedTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenBackPressedTest.kt
new file mode 100644
index 00000000..99f07c90
--- /dev/null
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenBackPressedTest.kt
@@ -0,0 +1,198 @@
+package com.arkivanov.decompose.router.children
+
+import com.arkivanov.decompose.router.children.ChildNavState.Status.ACTIVE
+import com.arkivanov.decompose.router.children.ChildNavState.Status.DESTROYED
+import com.arkivanov.decompose.router.children.ChildNavState.Status.INACTIVE
+import com.arkivanov.decompose.value.getValue
+import com.arkivanov.essenty.backhandler.BackCallback
+import kotlin.test.Test
+import kotlin.test.assertContentEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@Suppress("TestFunctionName")
+internal class ChildrenBackPressedTest : ChildrenTestBase() {
+
+    @Test
+    fun GIVEN_children_created_with_active_child_and_enabled_callback_registered_WHEN_back_THEN_callback_called() {
+        var isCalled = false
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        children.getById(id = 3).requireInstance().backHandler.register(BackCallback(isEnabled = true) { isCalled = true })
+
+        backDispatcher.back()
+
+        assertTrue(isCalled)
+    }
+
+    @Test
+    fun GIVEN_children_created_with_active_child_and_disabled_callback_registered_WHEN_back_THEN_callback_not_called() {
+        var isCalled = false
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        children.getById(id = 3).requireInstance().backHandler.register(BackCallback(isEnabled = false) { isCalled = true })
+
+        backDispatcher.back()
+
+        assertFalse(isCalled)
+    }
+
+    @Test
+    fun GIVEN_children_created_with_inactive_child_and_enabled_callback_registered_WHEN_back_THEN_callback_not_called() {
+        var isCalled = false
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        children.getById(id = 2).requireInstance().backHandler.register(BackCallback(isEnabled = true) { isCalled = true })
+
+        backDispatcher.back()
+
+        assertFalse(isCalled)
+    }
+
+    @Test
+    fun GIVEN_children_created_with_inactive_child_and_disabled_callback_registered_WHEN_back_THEN_callback_not_called() {
+        var isCalled = false
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        children.getById(id = 2).requireInstance().backHandler.register(BackCallback(isEnabled = false) { isCalled = true })
+
+        backDispatcher.back()
+
+        assertFalse(isCalled)
+    }
+
+    @Test
+    fun GIVEN_child_switched_from_destroyed_to_inactive_and_enabled_callback_registered_WHEN_back_THEN_callback_not_called() {
+        var isCalled = false
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        navigate { listOf(1 by INACTIVE, 2 by INACTIVE, 3 by ACTIVE) }
+        children.getById(id = 1).requireInstance().backHandler.register(BackCallback(isEnabled = true) { isCalled = true })
+
+        backDispatcher.back()
+
+        assertFalse(isCalled)
+    }
+
+    @Test
+    fun GIVEN_child_switched_from_destroyed_to_active_and_enabled_callback_registered_WHEN_back_THEN_callback_called() {
+        var isCalled = false
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        navigate { listOf(1 by ACTIVE, 2 by INACTIVE, 3 by ACTIVE) }
+        children.getById(id = 1).requireInstance().backHandler.register(BackCallback(isEnabled = true) { isCalled = true })
+
+        backDispatcher.back()
+
+        assertTrue(isCalled)
+    }
+
+    @Test
+    fun GIVEN_child_switched_from_inactive_to_active_and_enabled_callback_registered_WHEN_back_THEN_callback_called() {
+        var isCalled = false
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        navigate { listOf(1 by DESTROYED, 2 by ACTIVE, 3 by ACTIVE) }
+        children.getById(id = 2).requireInstance().backHandler.register(BackCallback(isEnabled = true) { isCalled = true })
+
+        backDispatcher.back()
+
+        assertTrue(isCalled)
+    }
+
+    @Test
+    fun GIVEN_child_switched_from_active_to_inactive_and_enabled_callback_registered_WHEN_back_THEN_callback_not_called() {
+        var isCalled = false
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by INACTIVE) }
+        children.getById(id = 3).requireInstance().backHandler.register(BackCallback(isEnabled = true) { isCalled = true })
+
+        backDispatcher.back()
+
+        assertFalse(isCalled)
+    }
+
+    @Test
+    fun GIVEN_enabled_callback_registered_in_inactive_child_and_switched_to_active_WHEN_back_THEN_callback_called() {
+        var isCalled = false
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        children.getById(id = 2).requireInstance().backHandler.register(BackCallback(isEnabled = true) { isCalled = true })
+        navigate { listOf(1 by DESTROYED, 2 by ACTIVE, 3 by ACTIVE) }
+
+        backDispatcher.back()
+
+        assertTrue(isCalled)
+    }
+
+    @Test
+    fun GIVEN_enabled_callback_registered_in_inactive_child_and_switched_to_destroyed_WHEN_back_THEN_callback_not_called() {
+        var isCalled = false
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        children.getById(id = 2).requireInstance().backHandler.register(BackCallback(isEnabled = true) { isCalled = true })
+        navigate { listOf(1 by DESTROYED, 2 by DESTROYED, 3 by ACTIVE) }
+
+        backDispatcher.back()
+
+        assertFalse(isCalled)
+    }
+
+    @Test
+    fun GIVEN_enabled_callback_registered_in_active_child_and_switched_to_inactive_WHEN_back_THEN_callback_not_called() {
+        var isCalled = false
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        children.getById(id = 3).requireInstance().backHandler.register(BackCallback(isEnabled = true) { isCalled = true })
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by INACTIVE) }
+
+        backDispatcher.back()
+
+        assertFalse(isCalled)
+    }
+
+    @Test
+    fun GIVEN_enabled_callback_registered_in_active_child_and_switched_to_destroyed_WHEN_back_THEN_callback_not_called() {
+        var isCalled = false
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        children.getById(id = 3).requireInstance().backHandler.register(BackCallback(isEnabled = true) { isCalled = true })
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by DESTROYED) }
+
+        backDispatcher.back()
+
+        assertFalse(isCalled)
+    }
+
+    @Test
+    fun GIVEN_backTransformer_returns_null_WHEN_back_THEN_returned_false() {
+        context.children(
+            initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE),
+            backTransformer = { null },
+        )
+
+        val result = backDispatcher.back()
+
+        assertFalse(result)
+    }
+
+    @Test
+    fun GIVEN_backTransformer_returns_not_null_WHEN_back_THEN_navigated() {
+        val children by context.children(
+            initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE),
+            backTransformer = { { emptyList() } },
+        )
+
+        backDispatcher.back()
+
+        assertContentEquals(emptyList(), children)
+    }
+
+    @Test
+    fun GIVEN_navigated_and_backTransformer_returns_null_WHEN_back_THEN_returned_false() {
+        context.children(
+            initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE),
+            backTransformer = { navState ->
+                if (navState.isNotEmpty()) {
+                    { emptyList() }
+                } else {
+                    null
+                }
+            },
+        )
+        navigate { emptyList() }
+
+        val result = backDispatcher.back()
+
+        assertFalse(result)
+    }
+}
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenBasicTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenBasicTest.kt
new file mode 100644
index 00000000..a9a7566c
--- /dev/null
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenBasicTest.kt
@@ -0,0 +1,22 @@
+package com.arkivanov.decompose.router.children
+
+import com.arkivanov.decompose.router.children.ChildNavState.Status.*
+import kotlin.test.Test
+import kotlin.test.assertContentEquals
+
+@Suppress("TestFunctionName")
+internal class ChildrenBasicTest : ChildrenTestBase() {
+
+    @Test
+    fun WHEN_navigate_THEN_onComplete_called() {
+        val results = ArrayList<Pair<List<SimpleChildNavState<Config>>, List<SimpleChildNavState<Config>>>>()
+        context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+
+        navigate(transformer = { emptyList() }, onComplete = { newNavState, oldNavState -> results += newNavState to oldNavState })
+
+        assertContentEquals(
+            listOf(emptyList<SimpleChildNavState<Config>>() to listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE)),
+            results,
+        )
+    }
+}
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenLifecycleTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenLifecycleTest.kt
new file mode 100644
index 00000000..7b763c70
--- /dev/null
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenLifecycleTest.kt
@@ -0,0 +1,190 @@
+package com.arkivanov.decompose.router.children
+
+import com.arkivanov.decompose.router.children.ChildNavState.Status.ACTIVE
+import com.arkivanov.decompose.router.children.ChildNavState.Status.DESTROYED
+import com.arkivanov.decompose.router.children.ChildNavState.Status.INACTIVE
+import com.arkivanov.decompose.value.getValue
+import com.arkivanov.essenty.lifecycle.Lifecycle
+import com.arkivanov.essenty.lifecycle.destroy
+import com.arkivanov.essenty.lifecycle.stop
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+@Suppress("TestFunctionName")
+internal class ChildrenLifecycleTest : ChildrenTestBase() {
+
+    @Test
+    fun WHEN_created_THEN_initial_state() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+
+        children.assertChildren(1 to null, 2 to 2, 3 to 3)
+    }
+
+    @Test
+    fun WHEN_created_THEN_lifecycles_correct() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+
+        assertEquals(Lifecycle.State.CREATED, children.getById(id = 2).requireInstance().lifecycle.state)
+        assertEquals(Lifecycle.State.RESUMED, children.getById(id = 3).requireInstance().lifecycle.state)
+    }
+
+    @Test
+    fun WHEN_children_added_THEN_state_updated() {
+        val children by context.children()
+
+        navigate { it + (1 by DESTROYED) }
+        navigate { it + (2 by INACTIVE) }
+        navigate { it + (3 by ACTIVE) }
+
+        children.assertChildren(1 to null, 2 to 2, 3 to 3)
+    }
+
+    @Test
+    fun WHEN_children_added_THEN_lifecycles_correct() {
+        val children by context.children()
+
+        navigate { it + (1 by DESTROYED) }
+        navigate { it + (2 by INACTIVE) }
+        navigate { it + (3 by ACTIVE) }
+
+        assertEquals(Lifecycle.State.CREATED, children.getById(id = 2).requireInstance().lifecycle.state)
+        assertEquals(Lifecycle.State.RESUMED, children.getById(id = 3).requireInstance().lifecycle.state)
+    }
+
+    @Test
+    fun WHEN_destroyed_child_removed_THEN_state_updated() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+
+        navigate { navState -> navState.filterNot { it.configuration.id == 1 } }
+
+        children.assertChildren(2 to 2, 3 to 3)
+    }
+
+    @Test
+    fun WHEN_inactive_child_removed_THEN_state_updated() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+
+        navigate { navState -> navState.filterNot { it.configuration.id == 2 } }
+
+        children.assertChildren(1 to null, 3 to 3)
+    }
+
+    @Test
+    fun WHEN_inactive_child_removed_THEN_lifecycle_destroyed() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val component = children.getById(id = 2).requireInstance()
+
+        navigate { navState -> navState.filterNot { it.configuration.id == 2 } }
+
+        assertEquals(Lifecycle.State.DESTROYED, component.lifecycle.state)
+    }
+
+    @Test
+    fun WHEN_active_child_removed_THEN_state_updated() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+
+        navigate { navState -> navState.filterNot { it.configuration.id == 3 } }
+
+        children.assertChildren(1 to null, 2 to 2)
+    }
+
+    @Test
+    fun WHEN_active_child_removed_THEN_lifecycle_destroyed() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val component = children.getById(id = 3).requireInstance()
+
+        navigate { navState -> navState.filterNot { it.configuration.id == 3 } }
+
+        assertEquals(Lifecycle.State.DESTROYED, component.lifecycle.state)
+    }
+
+    @Test
+    fun WHEN_all_children_removed_THEN_state_updated() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+
+        navigate { emptyList() }
+
+        children.assertChildren()
+    }
+
+    @Test
+    fun WHEN_all_children_removed_THEN_lifecycles_destroyed() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val component2 = children.getById(id = 2).requireInstance()
+        val component3 = children.getById(id = 3).requireInstance()
+
+        navigate { emptyList() }
+
+        assertEquals(Lifecycle.State.DESTROYED, component2.lifecycle.state)
+        assertEquals(Lifecycle.State.DESTROYED, component3.lifecycle.state)
+    }
+
+    @Test
+    fun WHEN_child_switched_from_destroyed_to_inactive_THEN_lifecycle_created() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+
+        navigate { listOf(1 by INACTIVE, 2 by INACTIVE, 3 by ACTIVE) }
+
+        assertEquals(Lifecycle.State.CREATED, children.getById(id = 1).requireInstance().lifecycle.state)
+    }
+
+    @Test
+    fun WHEN_child_switched_from_inactive_to_active_THEN_lifecycle_resumed() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+
+        navigate { listOf(1 by DESTROYED, 2 by ACTIVE, 3 by ACTIVE) }
+
+        assertEquals(Lifecycle.State.RESUMED, children.getById(id = 2).requireInstance().lifecycle.state)
+    }
+
+    @Test
+    fun WHEN_child_switched_from_inactive_to_destroyed_THEN_lifecycle_destroyed() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val component = children.getById(id = 2).requireInstance()
+
+        navigate { listOf(1 by DESTROYED, 2 by DESTROYED, 3 by ACTIVE) }
+
+        assertEquals(Lifecycle.State.DESTROYED, component.lifecycle.state)
+    }
+
+    @Test
+    fun WHEN_child_switched_from_active_to_inactive_THEN_lifecycle_stopped() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by INACTIVE) }
+
+        assertEquals(Lifecycle.State.CREATED, children.getById(id = 3).requireInstance().lifecycle.state)
+    }
+
+    @Test
+    fun WHEN_child_switched_from_active_to_destroyed_THEN_lifecycle_destroyed() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val component = children.getById(id = 3).requireInstance()
+
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by DESTROYED) }
+
+        assertEquals(Lifecycle.State.DESTROYED, component.lifecycle.state)
+    }
+
+    @Test
+    fun WHEN_context_lifecycle_stopped_THEN_all_component_lifecycles_stopped() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+
+        lifecycle.stop()
+
+        assertEquals(Lifecycle.State.CREATED, children.getById(id = 2).requireInstance().lifecycle.state)
+        assertEquals(Lifecycle.State.CREATED, children.getById(id = 3).requireInstance().lifecycle.state)
+    }
+
+    @Test
+    fun WHEN_context_lifecycle_destroyed_THEN_all_component_lifecycles_destroyed() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val component2 = children.getById(id = 2).requireInstance()
+        val component3 = children.getById(id = 3).requireInstance()
+
+        lifecycle.destroy()
+
+        assertEquals(Lifecycle.State.DESTROYED, component2.lifecycle.state)
+        assertEquals(Lifecycle.State.DESTROYED, component3.lifecycle.state)
+    }
+}
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenRetainedInstanceTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenRetainedInstanceTest.kt
new file mode 100644
index 00000000..7bf0789b
--- /dev/null
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenRetainedInstanceTest.kt
@@ -0,0 +1,158 @@
+package com.arkivanov.decompose.router.children
+
+import com.arkivanov.decompose.DefaultComponentContext
+import com.arkivanov.decompose.router.TestInstance
+import com.arkivanov.decompose.router.children.ChildNavState.Status.ACTIVE
+import com.arkivanov.decompose.router.children.ChildNavState.Status.DESTROYED
+import com.arkivanov.decompose.router.children.ChildNavState.Status.INACTIVE
+import com.arkivanov.decompose.statekeeper.TestStateKeeperDispatcher
+import com.arkivanov.decompose.value.getValue
+import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
+import com.arkivanov.essenty.instancekeeper.getOrCreate
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertNotSame
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+@Suppress("TestFunctionName")
+internal class ChildrenRetainedInstanceTest : ChildrenTestBase() {
+
+    @Test
+    fun WHEN_child_switched_from_inactive_to_destroyed_THEN_instance_destroyed() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val component = children.getById(id = 2).requireInstance()
+        val instance = component.instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+
+        navigate { listOf(1 by DESTROYED, 2 by DESTROYED, 3 by ACTIVE) }
+
+        assertTrue(instance.isDestroyed)
+    }
+
+    @Test
+    fun WHEN_child_switched_from_active_to_destroyed_THEN_instance_destroyed() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val component = children.getById(id = 3).requireInstance()
+        val instance = component.instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by DESTROYED) }
+
+        assertTrue(instance.isDestroyed)
+    }
+
+    @Test
+    fun WHEN_child_switched_from_active_to_inactive_THEN_instance_not_destroyed() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val instance = children.getById(id = 3).requireInstance().instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by INACTIVE) }
+
+        assertFalse(instance.isDestroyed)
+    }
+
+    @Test
+    fun GIVEN_child_switched_from_inactive_to_destroyed_WHEN_child_switched_to_inactive_THEN_instance_not_retained() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val oldInstance = children.getById(id = 2).requireInstance().instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+        navigate { listOf(1 by DESTROYED, 2 by DESTROYED, 3 by ACTIVE) }
+
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE) }
+        val newInstance = children.getById(id = 2).requireInstance().instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+
+        assertNotSame(oldInstance, newInstance)
+    }
+
+    @Test
+    fun GIVEN_child_switched_from_active_to_destroyed_WHEN_child_switched_to_active_THEN_instance_not_retained() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val oldInstance = children.getById(id = 3).requireInstance().instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by DESTROYED) }
+
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE) }
+        val newInstance = children.getById(id = 3).requireInstance().instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+
+        assertNotSame(oldInstance, newInstance)
+    }
+
+    @Test
+    fun WHEN_inactive_child_recreated_THEN_instance_retained() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val instanceKeeper = InstanceKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper, instanceKeeper = instanceKeeper)
+        val oldChildren by oldContext.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val oldInstance = oldChildren.getById(id = 2).requireInstance().instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper)
+        val newChildren by newContext.children()
+        val newInstance = newChildren.getById(id = 2).requireInstance().instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+
+        assertSame(oldInstance, newInstance)
+    }
+
+    @Test
+    fun WHEN_inactive_child_recreated_THEN_instance_not_destroyed() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val instanceKeeper = InstanceKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper, instanceKeeper = instanceKeeper)
+        val oldChildren by oldContext.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val instance = oldChildren.getById(id = 2).requireInstance().instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper)
+        newContext.children()
+
+        assertFalse(instance.isDestroyed)
+    }
+
+    @Test
+    fun WHEN_active_child_recreated_THEN_instance_retained() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val instanceKeeper = InstanceKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper, instanceKeeper = instanceKeeper)
+        val oldChildren by oldContext.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val oldInstance = oldChildren.getById(id = 3).requireInstance().instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper)
+        val newChildren by newContext.children()
+        val newInstance = newChildren.getById(id = 3).requireInstance().instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+
+        assertSame(oldInstance, newInstance)
+    }
+
+    @Test
+    fun WHEN_active_child_recreated_THEN_instance_not_destroyed() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val instanceKeeper = InstanceKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper, instanceKeeper = instanceKeeper)
+        val oldChildren by oldContext.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val instance = oldChildren.getById(id = 3).requireInstance().instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper)
+        newContext.children()
+
+        assertFalse(instance.isDestroyed)
+    }
+
+    @Test
+    fun WHEN_recreated_and_child_restored_as_destroyed_THEN_instance_destroyed() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val instanceKeeper = InstanceKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper, instanceKeeper = instanceKeeper)
+        val oldChildren by oldContext.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        val instance = oldChildren.getById(id = 3).requireInstance().instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper)
+        newContext.children(restoreNavState = { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by DESTROYED) })
+
+        assertTrue(instance.isDestroyed)
+    }
+}
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenSavedStateTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenSavedStateTest.kt
new file mode 100644
index 00000000..d8af0ca1
--- /dev/null
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenSavedStateTest.kt
@@ -0,0 +1,201 @@
+package com.arkivanov.decompose.router.children
+
+import com.arkivanov.decompose.DefaultComponentContext
+import com.arkivanov.decompose.router.children.ChildNavState.Status.ACTIVE
+import com.arkivanov.decompose.router.children.ChildNavState.Status.DESTROYED
+import com.arkivanov.decompose.router.children.ChildNavState.Status.INACTIVE
+import com.arkivanov.decompose.statekeeper.ParcelableStub
+import com.arkivanov.decompose.statekeeper.TestStateKeeperDispatcher
+import com.arkivanov.decompose.value.getValue
+import com.arkivanov.essenty.parcelable.Parcelable
+import com.arkivanov.essenty.statekeeper.consume
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@Suppress("TestFunctionName")
+internal class ChildrenSavedStateTest : ChildrenTestBase() {
+
+    @Test
+    fun WHEN_child_switched_from_active_to_inactive_THEN_state_saved() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        var isCalled = false
+        children.getById(id = 3).requireInstance().stateKeeper.register(key = "key") {
+            isCalled = true
+            ParcelableStub()
+        }
+
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by INACTIVE) }
+
+        assertTrue(isCalled)
+    }
+
+    @Test
+    fun WHEN_child_switched_from_active_to_destroyed_THEN_state_saved() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        var isCalled = false
+        children.getById(id = 3).requireInstance().stateKeeper.register(key = "key") {
+            isCalled = true
+            ParcelableStub()
+        }
+
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by DESTROYED) }
+
+        assertTrue(isCalled)
+    }
+
+    @Test
+    fun WHEN_child_switched_from_inactive_to_destroyed_THEN_state_not_saved() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        var isCalled = false
+        children.getById(id = 2).requireInstance().stateKeeper.register(key = "key") {
+            isCalled = true
+            ParcelableStub()
+        }
+
+        navigate { listOf(1 by DESTROYED, 2 by DESTROYED, 3 by ACTIVE) }
+
+        assertFalse(isCalled)
+    }
+
+    @Test
+    fun WHEN_child_switched_from_inactive_to_active_THEN_state_not_saved() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        var isCalled = false
+        children.getById(id = 2).requireInstance().stateKeeper.register(key = "key") {
+            isCalled = true
+            ParcelableStub()
+        }
+
+        navigate { listOf(1 by DESTROYED, 2 by ACTIVE, 3 by ACTIVE) }
+
+        assertFalse(isCalled)
+    }
+
+    @Test
+    fun WHEN_child_switched_from_destroyed_to_inactive_THEN_state_not_saved() {
+        var isCalled = false
+        context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE)) { config, componentContext ->
+            val component = Component(config, componentContext)
+            if (config.id == 1) {
+                component.stateKeeper.register(key = "key") {
+                    isCalled = true
+                    ParcelableStub()
+                }
+            }
+            component
+        }
+
+        navigate { listOf(1 by INACTIVE, 2 by INACTIVE, 3 by ACTIVE) }
+
+        assertFalse(isCalled)
+    }
+
+    @Test
+    fun WHEN_child_switched_from_destroyed_to_active_THEN_state_not_saved() {
+        var isCalled = false
+        context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE)) { config, componentContext ->
+            val component = Component(config, componentContext)
+            if (config.id == 1) {
+                component.stateKeeper.register(key = "key") {
+                    isCalled = true
+                    ParcelableStub()
+                }
+            }
+            component
+        }
+
+        navigate { listOf(1 by ACTIVE, 2 by INACTIVE, 3 by ACTIVE) }
+
+        assertFalse(isCalled)
+    }
+
+    @Test
+    fun GIVEN_child_switched_from_active_to_destroyed_WHEN_child_switched_to_active_THEN_state_restored() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        children.getById(id = 3).requireInstance().stateKeeper.register(key = "key") { Config(id = 30) }
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by DESTROYED) }
+
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE) }
+        val restoredState = children.getById(id = 3).requireInstance().stateKeeper.consume<Parcelable>(key = "key")
+
+        assertEquals(Config(id = 30), restoredState)
+    }
+
+    @Test
+    fun GIVEN_child_switched_from_active_to_destroyed_WHEN_child_switched_to_inactive_THEN_state_restored() {
+        val children by context.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        children.getById(id = 3).requireInstance().stateKeeper.register(key = "key") { Config(id = 30) }
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by DESTROYED) }
+
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by INACTIVE) }
+        val restoredState = children.getById(id = 3).requireInstance().stateKeeper.consume<Parcelable>(key = "key")
+
+        assertEquals(Config(id = 30), restoredState)
+    }
+
+    @Test
+    fun WHEN_recreated_THEN_navigation_state_restored() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
+        oldContext.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
+        val newChildren by newContext.children()
+
+        newChildren.assertChildren(1 to null, 2 to 2, 3 to 3)
+    }
+
+    @Test
+    fun WHEN_recreated_THEN_child_states_restored() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
+        val oldChildren by oldContext.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        oldChildren.getById(id = 3).requireInstance().stateKeeper.register(key = "key") { Config(id = 30) }
+
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
+        val newChildren by newContext.children()
+        val restoredState3 = newChildren.getById(id = 3).requireInstance().stateKeeper.consume<Config>(key = "key")
+
+        assertEquals(Config(id = 30), restoredState3)
+    }
+
+    @Test
+    fun GIVEN_recreated_and_child_restored_as_destroyed_WHEN_child_switched_to_inactive_THEN_state_restored() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
+        val oldChildren by oldContext.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        oldChildren.getById(id = 3).requireInstance().stateKeeper.register(key = "key") { Config(id = 30) }
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
+        val newChildren by newContext.children(restoreNavState = { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by DESTROYED) })
+
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by INACTIVE) }
+        val restoredState3 = newChildren.getById(id = 3).requireInstance().stateKeeper.consume<Config>(key = "key")
+
+        assertEquals(Config(id = 30), restoredState3)
+    }
+
+    @Test
+    fun GIVEN_recreated_and_child_restored_as_destroyed_WHEN_child_switched_to_active_THEN_state_restored() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
+        val oldChildren by oldContext.children(initialNavState = listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
+        oldChildren.getById(id = 3).requireInstance().stateKeeper.register(key = "key") { Config(id = 30) }
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
+        val newChildren by newContext.children(restoreNavState = { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by DESTROYED) })
+
+        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE) }
+        val restoredState3 = newChildren.getById(id = 3).requireInstance().stateKeeper.consume<Config>(key = "key")
+
+        assertEquals(Config(id = 30), restoredState3)
+    }
+}
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenTestBase.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenTestBase.kt
new file mode 100644
index 00000000..800c1366
--- /dev/null
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenTestBase.kt
@@ -0,0 +1,156 @@
+package com.arkivanov.decompose.router.children
+
+import com.arkivanov.decompose.Child
+import com.arkivanov.decompose.ComponentContext
+import com.arkivanov.decompose.DefaultComponentContext
+import com.arkivanov.decompose.Relay
+import com.arkivanov.decompose.statekeeper.TestStateKeeperDispatcher
+import com.arkivanov.decompose.value.Value
+import com.arkivanov.essenty.backhandler.BackDispatcher
+import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
+import com.arkivanov.essenty.lifecycle.LifecycleRegistry
+import com.arkivanov.essenty.lifecycle.resume
+import com.arkivanov.essenty.parcelable.Parcelable
+import com.arkivanov.essenty.parcelable.ParcelableContainer
+import com.arkivanov.essenty.parcelable.Parcelize
+import com.arkivanov.essenty.parcelable.consumeRequired
+import kotlin.test.BeforeTest
+import kotlin.test.assertContentEquals
+
+internal open class ChildrenTestBase {
+
+    private val navigation = TestNavigation()
+    protected val lifecycle = LifecycleRegistry()
+    protected val backDispatcher = BackDispatcher()
+
+    protected val context =
+        DefaultComponentContext(
+            lifecycle = lifecycle,
+            stateKeeper = TestStateKeeperDispatcher(),
+            instanceKeeper = InstanceKeeperDispatcher(),
+            backHandler = backDispatcher,
+        )
+
+    @BeforeTest
+    fun before() {
+        lifecycle.resume()
+    }
+
+    protected fun ComponentContext.children(
+        initialNavState: List<SimpleChildNavState<Config>> = emptyList(),
+        restoreNavState: (container: ParcelableContainer) -> List<SimpleChildNavState<Config>> = { container ->
+            val savedState = container.consumeRequired<SavedNavState>()
+            savedState.configurations.zip(savedState.statuses).map { (configuration, status) ->
+                SimpleChildNavState(
+                    configuration = configuration,
+                    status = status,
+                )
+            }
+        },
+        backTransformer: (navState: List<SimpleChildNavState<Config>>) -> (() -> List<SimpleChildNavState<Config>>)? = { null },
+        childFactory: (Config, ComponentContext) -> Component = ::Component,
+    ): Value<List<Child<Config, Component>>> =
+        children(
+            source = navigation,
+            key = "Key",
+            initialNavState = { TestNavState(children = initialNavState) },
+            saveNavState = { navState ->
+                ParcelableContainer(
+                    SavedNavState(
+                        configurations = navState.children.map { it.configuration },
+                        statuses = navState.children.map { it.status },
+                    )
+                )
+            },
+            restoreNavState = { container -> TestNavState(children = restoreNavState(container)) },
+            navTransformer = { navState, event -> event.transformer(navState) },
+            onEventComplete = { event, newNavState, oldNavState -> event.onComplete(newNavState, oldNavState) },
+            backTransformer = {
+                backTransformer(it.children)?.let { transformer ->
+                    { TestNavState(children = transformer()) }
+                }
+            },
+            stateMapper = { _, children -> children },
+            childFactory = childFactory,
+        )
+
+    protected fun navigate(transformer: (List<SimpleChildNavState<Config>>) -> List<SimpleChildNavState<Config>>) {
+        navigate(transformer = transformer, onComplete = null)
+    }
+
+    protected fun navigate(
+        transformer: (List<SimpleChildNavState<Config>>) -> List<SimpleChildNavState<Config>>,
+        onComplete: ((newNavState: List<SimpleChildNavState<Config>>, oldNavState: List<SimpleChildNavState<Config>>) -> Unit)?,
+    ) {
+        navigation.navigate(
+            Event(
+                transformer = { TestNavState(children = transformer(it.children)) },
+                onComplete = { newNavState, oldNavState -> onComplete?.invoke(newNavState.children, oldNavState.children) },
+            )
+        )
+    }
+
+    protected infix fun Int.by(status: ChildNavState.Status): SimpleChildNavState<Config> =
+        SimpleChildNavState(configuration = Config(id = this), status = status)
+
+    protected fun List<Child<Config, Component>>.assertChildren(vararg children: Pair<Int, Int?>) {
+        assertContentEquals(
+            children.toList(),
+            map { child ->
+                when (child) {
+                    is Child.Created -> child.configuration.id to child.instance.id
+                    is Child.Destroyed -> child.configuration.id to null
+                }
+            }
+        )
+    }
+
+    protected fun List<Child<Config, Component>>.getById(id: Int): Child<Config, Component> =
+        first { it.configuration.id == id }
+
+    protected fun Child<*, Component>.requireInstance(): Component =
+        requireNotNull(instance)
+
+    protected data class TestNavState(
+        override val children: List<SimpleChildNavState<Config>> = emptyList(),
+    ) : NavState<Config>
+
+    @Parcelize
+    protected data class Config(
+        val id: Int,
+    ) : Parcelable
+
+    protected class Component(
+        config: Config,
+        componentContext: ComponentContext,
+    ) : ComponentContext by componentContext {
+        val id: Int = config.id
+    }
+
+    @Parcelize
+    protected class SavedNavState(
+        val configurations: List<Config>,
+        val statuses: List<ChildNavState.Status>,
+    ) : Parcelable
+
+    protected class Event(
+        val transformer: (TestNavState) -> TestNavState,
+        val onComplete: (newNavState: TestNavState, oldNavState: TestNavState) -> Unit,
+    )
+
+    protected class TestNavigation : NavigationSource<Event> {
+        private val relay = Relay<Event>()
+
+        override fun subscribe(observer: (Event) -> Unit) {
+            relay.subscribe(observer)
+        }
+
+        override fun unsubscribe(observer: (Event) -> Unit) {
+            relay.unsubscribe(observer)
+        }
+
+        fun navigate(event: Event) {
+            relay.accept(event)
+        }
+    }
+}