Repository: arkivanov/Decompose
Pull request #140 (2022-07-12 23:11:36)

Late initialization of child stack


Patch:
diff --git a/decompose/api/android/decompose.api b/decompose/api/android/decompose.api
index 03b8ec2b..86b3bca6 100644
--- a/decompose/api/android/decompose.api
+++ b/decompose/api/android/decompose.api
@@ -131,9 +131,28 @@ public final class com/arkivanov/decompose/router/stack/ChildStack {
 	public fun toString ()Ljava/lang/String;
 }
 
-public final class com/arkivanov/decompose/router/stack/RouterFactoryKt {
-	public static final fun stackRouter (Lcom/arkivanov/decompose/ComponentContext;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/router/stack/StackRouter;
-	public static synthetic fun stackRouter$default (Lcom/arkivanov/decompose/ComponentContext;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/router/stack/StackRouter;
+public final class com/arkivanov/decompose/router/stack/ChildStackFactoryKt {
+	public static final fun childStack (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value;
+	public static synthetic fun childStack$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value;
+}
+
+public abstract interface class com/arkivanov/decompose/router/stack/StackNavigation : com/arkivanov/decompose/router/stack/StackNavigationSource, com/arkivanov/decompose/router/stack/StackNavigator {
+}
+
+public final class com/arkivanov/decompose/router/stack/StackNavigationFactoryKt {
+	public static final fun StackNavigation ()Lcom/arkivanov/decompose/router/stack/StackNavigation;
+}
+
+public abstract interface class com/arkivanov/decompose/router/stack/StackNavigationSource {
+	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/stack/StackNavigationSource$Event {
+	public fun <init> (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
+	public synthetic fun <init> (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public final fun getOnComplete ()Lkotlin/jvm/functions/Function2;
+	public final fun getTransformer ()Lkotlin/jvm/functions/Function1;
 }
 
 public abstract interface class com/arkivanov/decompose/router/stack/StackNavigator {
@@ -154,16 +173,8 @@ public final class com/arkivanov/decompose/router/stack/StackNavigatorExtKt {
 	public static synthetic fun replaceCurrent$default (Lcom/arkivanov/decompose/router/stack/StackNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
 }
 
-public abstract interface class com/arkivanov/decompose/router/stack/StackRouter : com/arkivanov/decompose/router/stack/StackNavigator {
-	public abstract fun getStack ()Lcom/arkivanov/decompose/value/Value;
-}
-
-public final class com/arkivanov/decompose/router/stack/StackRouterExtKt {
-	public static final fun getActiveChild (Lcom/arkivanov/decompose/router/stack/StackRouter;)Lcom/arkivanov/decompose/Child$Created;
-}
-
 public abstract interface class com/arkivanov/decompose/router/stack/webhistory/WebHistoryController {
-	public abstract fun attach (Lcom/arkivanov/decompose/router/stack/StackRouter;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
+	public abstract fun attach (Lcom/arkivanov/decompose/router/stack/StackNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
 }
 
 public abstract interface class com/arkivanov/decompose/router/webhistory/WebHistoryController {
diff --git a/decompose/api/jvm/decompose.api b/decompose/api/jvm/decompose.api
index f2d2d7bd..1d38c29e 100644
--- a/decompose/api/jvm/decompose.api
+++ b/decompose/api/jvm/decompose.api
@@ -118,9 +118,28 @@ public final class com/arkivanov/decompose/router/stack/ChildStack {
 	public fun toString ()Ljava/lang/String;
 }
 
-public final class com/arkivanov/decompose/router/stack/RouterFactoryKt {
-	public static final fun stackRouter (Lcom/arkivanov/decompose/ComponentContext;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/router/stack/StackRouter;
-	public static synthetic fun stackRouter$default (Lcom/arkivanov/decompose/ComponentContext;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/router/stack/StackRouter;
+public final class com/arkivanov/decompose/router/stack/ChildStackFactoryKt {
+	public static final fun childStack (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value;
+	public static synthetic fun childStack$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value;
+}
+
+public abstract interface class com/arkivanov/decompose/router/stack/StackNavigation : com/arkivanov/decompose/router/stack/StackNavigationSource, com/arkivanov/decompose/router/stack/StackNavigator {
+}
+
+public final class com/arkivanov/decompose/router/stack/StackNavigationFactoryKt {
+	public static final fun StackNavigation ()Lcom/arkivanov/decompose/router/stack/StackNavigation;
+}
+
+public abstract interface class com/arkivanov/decompose/router/stack/StackNavigationSource {
+	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/stack/StackNavigationSource$Event {
+	public fun <init> (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
+	public synthetic fun <init> (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public final fun getOnComplete ()Lkotlin/jvm/functions/Function2;
+	public final fun getTransformer ()Lkotlin/jvm/functions/Function1;
 }
 
 public abstract interface class com/arkivanov/decompose/router/stack/StackNavigator {
@@ -141,16 +160,8 @@ public final class com/arkivanov/decompose/router/stack/StackNavigatorExtKt {
 	public static synthetic fun replaceCurrent$default (Lcom/arkivanov/decompose/router/stack/StackNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
 }
 
-public abstract interface class com/arkivanov/decompose/router/stack/StackRouter : com/arkivanov/decompose/router/stack/StackNavigator {
-	public abstract fun getStack ()Lcom/arkivanov/decompose/value/Value;
-}
-
-public final class com/arkivanov/decompose/router/stack/StackRouterExtKt {
-	public static final fun getActiveChild (Lcom/arkivanov/decompose/router/stack/StackRouter;)Lcom/arkivanov/decompose/Child$Created;
-}
-
 public abstract interface class com/arkivanov/decompose/router/stack/webhistory/WebHistoryController {
-	public abstract fun attach (Lcom/arkivanov/decompose/router/stack/StackRouter;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
+	public abstract fun attach (Lcom/arkivanov/decompose/router/stack/StackNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
 }
 
 public abstract interface class com/arkivanov/decompose/router/webhistory/WebHistoryController {
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Relay.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Relay.kt
new file mode 100644
index 00000000..398945c9
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Relay.kt
@@ -0,0 +1,27 @@
+package com.arkivanov.decompose
+
+internal class Relay<T> {
+
+    init {
+        ensureNeverFrozen()
+    }
+
+    private var observers = emptySet<(T) -> Unit>()
+
+    private val queue =
+        SerializedQueue<T> { value ->
+            observers.forEach { it(value) }
+        }
+
+    fun subscribe(observer: (T) -> Unit) {
+        this.observers += observer
+    }
+
+    fun unsubscribe(observer: (T) -> Unit) {
+        this.observers -= observer
+    }
+
+    fun accept(value: T) {
+        queue.offer(value)
+    }
+}
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackRouterImpl.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackController.kt
similarity index 63%
rename from decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackRouterImpl.kt
rename to decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackController.kt
index 61928841..a9a5b815 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackRouterImpl.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackController.kt
@@ -3,49 +3,49 @@ package com.arkivanov.decompose.router.stack
 import com.arkivanov.decompose.Child
 import com.arkivanov.decompose.SerializedQueue
 import com.arkivanov.decompose.ensureNeverFrozen
+import com.arkivanov.decompose.router.stack.StackNavigationSource.Event
 import com.arkivanov.decompose.value.MutableValue
+import com.arkivanov.decompose.value.Value
 import com.arkivanov.essenty.backpressed.BackPressedHandler
 import com.arkivanov.essenty.lifecycle.Lifecycle
 import com.arkivanov.essenty.lifecycle.doOnDestroy
 
-internal class StackRouterImpl<C : Any, T : Any>(
+internal class ChildStackController<out C : Any, out T : Any>(
+    private val source: StackNavigationSource<C>,
     lifecycle: Lifecycle,
     private val backPressedHandler: BackPressedHandler,
     private val popOnBackPressed: Boolean,
     private val stackHolder: StackHolder<C, T>,
     private val controller: StackController<C, T>,
-) : StackRouter<C, T> {
+) {
 
     init {
         ensureNeverFrozen()
     }
 
-    private val onBackPressedHandler = ::onBackPressed
-    override val stack: MutableValue<ChildStack<C, T>> = MutableValue(stackHolder.stack.toState())
     private val queue = SerializedQueue(::navigateActual)
+    private val _stack = MutableValue(stackHolder.stack.toState())
+    val stack: Value<ChildStack<C, T>> get() = _stack
 
     init {
-        backPressedHandler.register(onBackPressedHandler)
-        lifecycle.doOnDestroy(::destroy)
-    }
+        val eventObserver = queue::offer
+        source.subscribe(eventObserver)
 
-    private fun destroy() {
-        backPressedHandler.unregister(onBackPressedHandler)
-    }
+        val onBackPressedHandler = ::onBackPressed
+        backPressedHandler.register(onBackPressedHandler)
 
-    override fun navigate(
-        transformer: (stack: List<C>) -> List<C>,
-        onComplete: (newStack: List<C>, oldStack: List<C>) -> Unit
-    ) {
-        queue.offer(NavigationItem(transformer = transformer, onComplete = onComplete))
+        lifecycle.doOnDestroy {
+            backPressedHandler.unregister(onBackPressedHandler)
+            source.unsubscribe(eventObserver)
+        }
     }
 
-    private fun navigateActual(item: NavigationItem<C>) {
+    private fun navigateActual(event: Event<C>) {
         val oldStack = stackHolder.stack
-        val newStack = controller.navigate(oldStack = oldStack, transformer = item.transformer)
+        val newStack = controller.navigate(oldStack = oldStack, transformer = event.transformer)
         stackHolder.stack = newStack
-        stack.value = newStack.toState()
-        item.onComplete(newStack.configurationStack, oldStack.configurationStack)
+        _stack.value = newStack.toState()
+        event.onComplete(newStack.configurationStack, oldStack.configurationStack)
     }
 
     private fun onBackPressed(): Boolean =
@@ -53,7 +53,7 @@ internal class StackRouterImpl<C : Any, T : Any>(
             stackHolder.stack.active.backPressedDispatcher.onBackPressed() -> true
 
             popOnBackPressed && stackHolder.stack.backStack.isNotEmpty() -> {
-                pop()
+                queue.offer(Event(transformer = { it.dropLast(1) }))
                 true
             }
 
@@ -71,9 +71,4 @@ internal class StackRouterImpl<C : Any, T : Any>(
             is RouterEntry.Created -> Child.Created(configuration = configuration, instance = instance)
             is RouterEntry.Destroyed -> Child.Destroyed(configuration = configuration)
         }
-
-    private class NavigationItem<C : Any>(
-        val transformer: (stack: List<C>) -> List<C>,
-        val onComplete: (newStack: List<C>, oldStack: List<C>) -> Unit,
-    )
 }
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt
new file mode 100644
index 00000000..80b80fce
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt
@@ -0,0 +1,91 @@
+package com.arkivanov.decompose.router.stack
+
+import com.arkivanov.decompose.ComponentContext
+import com.arkivanov.decompose.value.Value
+import com.arkivanov.essenty.parcelable.Parcelable
+import com.arkivanov.essenty.parcelable.ParcelableContainer
+import kotlin.reflect.KClass
+
+/**
+ * Initializes and manages a stack of components.
+ *
+ * @param source a source of navigation events
+ * @param initialStack a stack of component configurations (from tail to head) that should be set if there is
+ * no saved state, must be not empty and unique
+ * @param configurationClass a [KClass] of the component configurations
+ * @param key a key of the stack, should be unique if there are multiple stacks in the same component
+ * @param handleBackButton determines whether the stack should be automatically popped on back button press or not
+ * @param childFactory a factory function that creates new child instances
+ * @return an observable [Value] of [ChildStack]
+ */
+fun <C : Parcelable, T : Any> ComponentContext.childStack(
+    source: StackNavigationSource<C>,
+    initialStack: () -> List<C>,
+    configurationClass: KClass<out C>,
+    key: String = "DefaultChildStack",
+    handleBackButton: Boolean = false,
+    childFactory: (configuration: C, ComponentContext) -> T,
+): Value<ChildStack<C, T>> {
+    val routerEntryFactory = RouterEntryFactoryImpl(lifecycle = lifecycle, childFactory = childFactory)
+
+    val controller =
+        ChildStackController(
+            lifecycle = lifecycle,
+            backPressedHandler = backPressedHandler,
+            popOnBackPressed = handleBackButton,
+            stackHolder = StackHolderImpl(
+                initialStack = initialStack,
+                lifecycle = lifecycle,
+                key = key,
+                stackSaver = StackSaverImpl(
+                    configurationClass = configurationClass,
+                    stateKeeper = stateKeeper,
+                    parcelableContainerFactory = ::ParcelableContainer
+                ),
+                instanceKeeper = instanceKeeper,
+                routerEntryFactory = routerEntryFactory
+            ),
+            controller = StackControllerImpl(routerEntryFactory = routerEntryFactory),
+            source = source,
+        )
+
+    return controller.stack
+}
+
+/**
+ * A convenience extension function for [ComponentContext.childStack].
+ */
+inline fun <reified C : Parcelable, T : Any> ComponentContext.childStack(
+    source: StackNavigationSource<C>,
+    noinline initialStack: () -> List<C>,
+    key: String = "DefaultRouter",
+    handleBackButton: Boolean = false,
+    noinline childFactory: (configuration: C, ComponentContext) -> T
+): Value<ChildStack<C, T>> =
+    childStack(
+        source = source,
+        initialStack = initialStack,
+        configurationClass = C::class,
+        key = key,
+        handleBackButton = handleBackButton,
+        childFactory = childFactory,
+    )
+
+/**
+ * A convenience extension function for [ComponentContext.childStack].
+ */
+inline fun <reified C : Parcelable, T : Any> ComponentContext.childStack(
+    source: StackNavigationSource<C>,
+    initialConfiguration: C,
+    key: String = "DefaultRouter",
+    handleBackButton: Boolean = false,
+    noinline childFactory: (configuration: C, ComponentContext) -> T
+): Value<ChildStack<C, T>> =
+    childStack(
+        source = source,
+        initialStack = { listOf(initialConfiguration) },
+        configurationClass = C::class,
+        key = key,
+        handleBackButton = handleBackButton,
+        childFactory = childFactory,
+    )
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/RouterFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/RouterFactory.kt
deleted file mode 100644
index d2452d54..00000000
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/RouterFactory.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-package com.arkivanov.decompose.router.stack
-
-import com.arkivanov.decompose.ComponentContext
-import com.arkivanov.essenty.parcelable.Parcelable
-import com.arkivanov.essenty.parcelable.ParcelableContainer
-import kotlin.reflect.KClass
-
-/**
- * Creates a new [StackRouter].
- *
- * @param initialStack a stack of component configurations (from tail to head) that should be set if there is
- * no saved state, must be not empty and unique
- * @param configurationClass a [KClass] of the component configurations
- * @param key a key of the [StackRouter], should be unique if there are multiple [StackRouter]s in the same component
- * @param handleBackButton determines whether the [StackRouter] should handle back button clicks or not
- * @param childFactory a factory function that creates new child instances
- * @return a new instance of [StackRouter]
- */
-fun <C : Parcelable, T : Any> ComponentContext.stackRouter(
-    initialStack: () -> List<C>,
-    configurationClass: KClass<out C>,
-    key: String = "DefaultRouter",
-    handleBackButton: Boolean = false,
-    childFactory: (configuration: C, ComponentContext) -> T,
-): StackRouter<C, T> {
-    val routerEntryFactory = RouterEntryFactoryImpl(lifecycle = lifecycle, childFactory = childFactory)
-
-    return StackRouterImpl(
-        lifecycle = lifecycle,
-        backPressedHandler = backPressedHandler,
-        popOnBackPressed = handleBackButton,
-        stackHolder = StackHolderImpl(
-            initialStack = initialStack,
-            lifecycle = lifecycle,
-            key = key,
-            stackSaver = StackSaverImpl(
-                configurationClass = configurationClass,
-                stateKeeper = stateKeeper,
-                parcelableContainerFactory = ::ParcelableContainer
-            ),
-            instanceKeeper = instanceKeeper,
-            routerEntryFactory = routerEntryFactory
-        ),
-        controller = StackControllerImpl(routerEntryFactory = routerEntryFactory),
-    )
-}
-
-/**
- * A convenience extension function for [ComponentContext.stackRouter].
- */
-inline fun <reified C : Parcelable, T : Any> ComponentContext.stackRouter(
-    noinline initialStack: () -> List<C>,
-    key: String = "DefaultRouter",
-    handleBackButton: Boolean = false,
-    noinline childFactory: (configuration: C, ComponentContext) -> T
-): StackRouter<C, T> =
-    stackRouter(
-        initialStack = initialStack,
-        configurationClass = C::class,
-        key = key,
-        handleBackButton = handleBackButton,
-        childFactory = childFactory,
-    )
-
-/**
- * A convenience extension function for [ComponentContext.stackRouter].
- */
-inline fun <reified C : Parcelable, T : Any> ComponentContext.stackRouter(
-    initialConfiguration: C,
-    key: String = "DefaultRouter",
-    handleBackButton: Boolean = false,
-    noinline childFactory: (configuration: C, ComponentContext) -> T
-): StackRouter<C, T> =
-    stackRouter(
-        initialStack = { listOf(initialConfiguration) },
-        configurationClass = C::class,
-        key = key,
-        handleBackButton = handleBackButton,
-        childFactory = childFactory,
-    )
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigation.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigation.kt
new file mode 100644
index 00000000..8f3fb86a
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigation.kt
@@ -0,0 +1,6 @@
+package com.arkivanov.decompose.router.stack
+
+/**
+ * Represents [StackNavigator] and [StackNavigationSource] at the same time.
+ */
+interface StackNavigation<C : Any> : StackNavigator<C>, StackNavigationSource<C>
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigationFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigationFactory.kt
new file mode 100644
index 00000000..dd0964b5
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigationFactory.kt
@@ -0,0 +1,28 @@
+package com.arkivanov.decompose.router.stack
+
+import com.arkivanov.decompose.Relay
+import com.arkivanov.decompose.router.stack.StackNavigationSource.Event
+
+/**
+ * Returns a default implementation of [StackNavigation].
+ * Broadcasts navigation events to all subscribed observers.
+ */
+@Suppress("FunctionName") // Factory function
+fun <C : Any> StackNavigation(): StackNavigation<C> = StackNavigationImpl()
+
+private class StackNavigationImpl<C : Any> : StackNavigation<C> {
+
+    private val relay = Relay<Event<C>>()
+
+    override fun navigate(transformer: (stack: List<C>) -> List<C>, onComplete: (newStack: List<C>, oldStack: List<C>) -> Unit) {
+        relay.accept(Event(transformer, onComplete))
+    }
+
+    override fun subscribe(observer: (Event<C>) -> Unit) {
+        relay.subscribe(observer)
+    }
+
+    override fun unsubscribe(observer: (Event<C>) -> Unit) {
+        relay.unsubscribe(observer)
+    }
+}
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigationSource.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigationSource.kt
new file mode 100644
index 00000000..6d4cc265
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigationSource.kt
@@ -0,0 +1,18 @@
+package com.arkivanov.decompose.router.stack
+
+/**
+ * Represents a source of navigation events.
+ *
+ * @see StackNavigator
+ */
+interface StackNavigationSource<C : Any> {
+
+    fun subscribe(observer: (Event<C>) -> Unit)
+
+    fun unsubscribe(observer: (Event<C>) -> Unit)
+
+    class Event<C : Any>(
+        val transformer: (stack: List<C>) -> List<C>,
+        val onComplete: (newStack: List<C>, oldStack: List<C>) -> Unit = { _, _ -> },
+    )
+}
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigatorExt.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigatorExt.kt
index b0aa7f47..4602ca42 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigatorExt.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigatorExt.kt
@@ -1,7 +1,7 @@
 package com.arkivanov.decompose.router.stack
 
 /**
- * A convenience method for [StackRouter.navigate].
+ * A convenience method for [StackNavigator.navigate].
  */
 fun <C : Any> StackNavigator<C>.navigate(transformer: (stack: List<C>) -> List<C>) {
     navigate(transformer = transformer, onComplete = { _, _ -> })
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackRouter.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackRouter.kt
deleted file mode 100644
index 08c5f32d..00000000
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackRouter.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.arkivanov.decompose.router.stack
-
-import com.arkivanov.decompose.value.Value
-
-interface StackRouter<C : Any, out T : Any> : StackNavigator<C> {
-
-    /**
-     * Returns the current stack of components, which can also be observed for changes.
-     */
-    val stack: Value<ChildStack<C, T>>
-}
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackRouterExt.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackRouterExt.kt
deleted file mode 100644
index 31ffc386..00000000
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackRouterExt.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.arkivanov.decompose.router.stack
-
-import com.arkivanov.decompose.Child
-
-val <C : Any, T : Any> StackRouter<C, T>.activeChild: Child.Created<C, T> get() = stack.value.active
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/webhistory/WebHistoryController.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/webhistory/WebHistoryController.kt
index 16489819..2e231eae 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/webhistory/WebHistoryController.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/webhistory/WebHistoryController.kt
@@ -1,25 +1,28 @@
 package com.arkivanov.decompose.router.stack.webhistory
 
 import com.arkivanov.decompose.ExperimentalDecomposeApi
-import com.arkivanov.decompose.router.stack.StackRouter
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.router.stack.StackNavigator
+import com.arkivanov.decompose.value.Value
 
 /**
- * Connects the [StackRouter] and the Web [History](https://developer.mozilla.org/en-US/docs/Web/API/History) API together.
+ * Connects the child stack and the Web [History](https://developer.mozilla.org/en-US/docs/Web/API/History) API together.
  */
 @ExperimentalDecomposeApi
 interface WebHistoryController {
 
-
     /**
-     * Listens for the [StackRouter] state changes and updates the Web [History](https://developer.mozilla.org/en-US/docs/Web/API/History)
-     * accordingly. Also listens for the `History` changes and navigates the [StackRouter].
+     * Listens for the [ChildStack] changes and updates the Web [History](https://developer.mozilla.org/en-US/docs/Web/API/History)
+     * accordingly. Also listens for the `History` changes and manipulates the [StackNavigator].
      *
-     * @param router a [StackRouter] that should be observed and manipulated
-     * @param getPath a mapper from the [StackRouter] configuration to a corresponding Web page path (starting from '/')
-     * @param getConfiguration a mapper from the Web page path (starting from '/') to a corresponding [StackRouter] configuration
+     * @param navigator a [StackNavigator] that should be manipulated
+     * @param stack an observable [ChildStack]
+     * @param getPath a mapper from the configuration to a corresponding Web page path (starting from '/')
+     * @param getConfiguration a mapper from the Web page path (starting from '/') to a corresponding configuration
      */
     fun <C : Any> attach(
-        router: StackRouter<C, *>,
+        navigator: StackNavigator<C>,
+        stack: Value<ChildStack<C, *>>,
         getPath: (configuration: C) -> String,
         getConfiguration: (path: String) -> C
     )
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/value/MutableValueBuilder.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/value/MutableValueBuilder.kt
index 4a1023af..9dc52ab5 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/value/MutableValueBuilder.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/value/MutableValueBuilder.kt
@@ -1,5 +1,6 @@
 package com.arkivanov.decompose.value
 
+import com.arkivanov.decompose.Relay
 import com.arkivanov.decompose.ensureNeverFrozen
 import kotlin.properties.Delegates
 
@@ -15,18 +16,15 @@ private class MutableValueImpl<T : Any>(initialValue: T) : MutableValue<T>() {
         ensureNeverFrozen()
     }
 
-    private var observers = emptySet<(T) -> Unit>()
-
-    override var value: T by Delegates.observable(initialValue) { _, _, value ->
-        observers.forEach { it(value) }
-    }
+    private val relay = Relay<T>()
+    override var value: T by Delegates.observable(initialValue) { _, _, value -> relay.accept(value) }
 
     override fun subscribe(observer: (T) -> Unit) {
-        observers = observers + observer
+        relay.subscribe(observer)
         observer(value)
     }
 
     override fun unsubscribe(observer: (T) -> Unit) {
-        observers = observers - observer
+        relay.unsubscribe(observer)
     }
 }
diff --git a/decompose/src/jsMain/kotlin/com/arkivanov/decompose/router/stack/webhistory/DefaultWebHistoryController.kt b/decompose/src/jsMain/kotlin/com/arkivanov/decompose/router/stack/webhistory/DefaultWebHistoryController.kt
index 9beee98d..2ea9e487 100644
--- a/decompose/src/jsMain/kotlin/com/arkivanov/decompose/router/stack/webhistory/DefaultWebHistoryController.kt
+++ b/decompose/src/jsMain/kotlin/com/arkivanov/decompose/router/stack/webhistory/DefaultWebHistoryController.kt
@@ -3,10 +3,11 @@ package com.arkivanov.decompose.router.stack.webhistory
 import com.arkivanov.decompose.ExperimentalDecomposeApi
 import com.arkivanov.decompose.router.findFirstDifferentIndex
 import com.arkivanov.decompose.router.stack.ChildStack
-import com.arkivanov.decompose.router.stack.StackRouter
+import com.arkivanov.decompose.router.stack.StackNavigator
 import com.arkivanov.decompose.router.stack.navigate
 import com.arkivanov.decompose.router.startsWith
 import com.arkivanov.decompose.router.subscribe
+import com.arkivanov.decompose.value.Value
 import org.w3c.dom.PopStateEvent
 
 @ExperimentalDecomposeApi
@@ -18,17 +19,18 @@ class DefaultWebHistoryController internal constructor(
     constructor() : this(WindowImpl())
 
     override fun <C : Any> attach(
-        router: StackRouter<C, *>,
+        navigator: StackNavigator<C>,
+        stack: Value<ChildStack<C, *>>,
         getPath: (configuration: C) -> String,
-        getConfiguration: (path: String) -> C,
+        getConfiguration: (path: String) -> C
     ) {
-        val impl = Impl(router, window, getPath, getConfiguration)
-        router.stack.subscribe(impl::onStackChanged)
+        val impl = Impl(navigator, window, getPath, getConfiguration)
+        stack.subscribe(impl::onStackChanged)
         window.onPopState = impl::onPopState
     }
 
     private class Impl<in C : Any>(
-        private val router: StackRouter<C, *>,
+        private val navigator: StackNavigator<C>,
         private val window: Window,
         private val getPath: (C) -> String,
         private val getConfiguration: (String) -> C,
@@ -122,7 +124,7 @@ class DefaultWebHistoryController internal constructor(
 
             isStateObserverEnabled = false
 
-            router.navigate { stack ->
+            navigator.navigate { stack ->
                 val indexInStack = stack.indexOfLast { it.hashCode() == newConfigurationKey }
                 if (indexInStack >= 0) {
                     // History popped, pop from the Router
diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/CountersComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/CountersComponent.kt
index 3eaddf9b..b3a25201 100644
--- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/CountersComponent.kt
+++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/CountersComponent.kt
@@ -2,10 +2,10 @@ package com.arkivanov.sample.shared.counters
 
 import com.arkivanov.decompose.ComponentContext
 import com.arkivanov.decompose.router.stack.ChildStack
-import com.arkivanov.decompose.router.stack.StackRouter
+import com.arkivanov.decompose.router.stack.StackNavigation
+import com.arkivanov.decompose.router.stack.childStack
 import com.arkivanov.decompose.router.stack.pop
 import com.arkivanov.decompose.router.stack.push
-import com.arkivanov.decompose.router.stack.stackRouter
 import com.arkivanov.decompose.value.Value
 import com.arkivanov.essenty.parcelable.Parcelable
 import com.arkivanov.essenty.parcelable.Parcelize
@@ -16,23 +16,29 @@ internal class CountersComponent(
     componentContext: ComponentContext,
 ) : Counters, ComponentContext by componentContext {
 
-    private val firstRouter: StackRouter<Config, Counter> =
-        stackRouter(
+    private val firstNavigation = StackNavigation<Config>()
+
+    private val firstStack =
+        childStack(
+            source = firstNavigation,
             initialConfiguration = Config(index = 0, isBackEnabled = false),
-            key = "LeftRouter",
+            key = "FirstStack",
             childFactory = ::firstChild,
         )
 
-    override val firstChildStack: Value<ChildStack<*, Counter>> get() = firstRouter.stack
+    override val firstChildStack: Value<ChildStack<*, Counter>> get() = firstStack
+
+    private val secondNavigation = StackNavigation<Config>()
 
-    private val secondRouter: StackRouter<Config, Counter> =
-        stackRouter(
+    private val secondStack =
+        childStack(
+            source = secondNavigation,
             initialConfiguration = Config(index = 0, isBackEnabled = false),
-            key = "RightRouter",
+            key = "SecondStack",
             childFactory = ::secondChild,
         )
 
-    override val secondChildStack: Value<ChildStack<*, Counter>> get() = secondRouter.stack
+    override val secondChildStack: Value<ChildStack<*, Counter>> get() = secondStack
 
     private fun firstChild(
         config: Config,
@@ -42,8 +48,8 @@ internal class CountersComponent(
             componentContext = componentContext,
             title = "Counter ${config.index}",
             isBackEnabled = config.isBackEnabled,
-            onNext = { firstRouter.push(Config(index = config.index + 1, isBackEnabled = true)) },
-            onPrev = { firstRouter.pop() },
+            onNext = { firstNavigation.push(Config(index = config.index + 1, isBackEnabled = true)) },
+            onPrev = firstNavigation::pop,
         )
 
     private fun secondChild(
@@ -54,8 +60,8 @@ internal class CountersComponent(
             componentContext = componentContext,
             title = "Counter ${config.index}",
             isBackEnabled = config.isBackEnabled,
-            onNext = { secondRouter.push(Config(index = config.index + 1, isBackEnabled = true)) },
-            onPrev = { secondRouter.pop() },
+            onNext = { secondNavigation.push(Config(index = config.index + 1, isBackEnabled = true)) },
+            onPrev = secondNavigation::pop,
         )
 
     @Parcelize
diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/DynamicFeaturesComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/DynamicFeaturesComponent.kt
index 834f5386..29db0cd5 100644
--- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/DynamicFeaturesComponent.kt
+++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/DynamicFeaturesComponent.kt
@@ -2,9 +2,10 @@ package com.arkivanov.sample.shared.dynamicfeatures
 
 import com.arkivanov.decompose.ComponentContext
 import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.router.stack.StackNavigation
+import com.arkivanov.decompose.router.stack.childStack
 import com.arkivanov.decompose.router.stack.pop
 import com.arkivanov.decompose.router.stack.push
-import com.arkivanov.decompose.router.stack.stackRouter
 import com.arkivanov.decompose.value.Value
 import com.arkivanov.essenty.parcelable.Parcelable
 import com.arkivanov.essenty.parcelable.Parcelize
@@ -21,14 +22,17 @@ internal class DynamicFeaturesComponent(
     private val featureInstaller: FeatureInstaller,
 ) : DynamicFeatures, ComponentContext by componentContext {
 
-    private val router =
-        stackRouter<Config, Child>(
+    private val navigation = StackNavigation<Config>()
+
+    private val stack =
+        childStack(
+            source = navigation,
             initialConfiguration = Config.Feature1,
             handleBackButton = true,
             childFactory = ::child,
         )
 
-    override val childStack: Value<ChildStack<*, Child>> = router.stack
+    override val childStack: Value<ChildStack<*, Child>> get() = stack
 
     private fun child(config: Config, componentContext: ComponentContext): Child =
         when (config) {
@@ -44,7 +48,7 @@ internal class DynamicFeaturesComponent(
             factory = { featureComponentContext ->
                 Feature1(
                     componentContext = featureComponentContext,
-                    onFeature2 = { router.push(Config.Feature2(magicNumber = Random.nextInt())) },
+                    onFeature2 = { navigation.push(Config.Feature2(magicNumber = Random.nextInt())) },
                 )
             }
         )
@@ -58,7 +62,7 @@ internal class DynamicFeaturesComponent(
                 Feature2(
                     componentContext = featureComponentContext,
                     magicNumber = config.magicNumber,
-                    onFinished = { router.pop() },
+                    onFinished = navigation::pop,
                 )
             }
         )
diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/dynamicfeature/DynamicFeatureComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/dynamicfeature/DynamicFeatureComponent.kt
index ee27830a..49b65e7a 100644
--- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/dynamicfeature/DynamicFeatureComponent.kt
+++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/dynamicfeature/DynamicFeatureComponent.kt
@@ -2,9 +2,9 @@ package com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature
 
 import com.arkivanov.decompose.ComponentContext
 import com.arkivanov.decompose.router.stack.ChildStack
-import com.arkivanov.decompose.router.stack.StackRouter
+import com.arkivanov.decompose.router.stack.StackNavigation
+import com.arkivanov.decompose.router.stack.childStack
 import com.arkivanov.decompose.router.stack.replaceCurrent
-import com.arkivanov.decompose.router.stack.stackRouter
 import com.arkivanov.decompose.value.Value
 import com.arkivanov.essenty.lifecycle.subscribe
 import com.arkivanov.essenty.parcelable.Parcelable
@@ -20,13 +20,16 @@ internal class DynamicFeatureComponent<out T : Any>(
     private val factory: (ComponentContext) -> T
 ) : DynamicFeature<T>, ComponentContext by componentContext {
 
-    private val router: StackRouter<Config, Child<T>> =
-        stackRouter(
+    private val navigation = StackNavigation<Config>()
+
+    private val stack =
+        childStack(
+            source = navigation,
             initialConfiguration = Config.Loading,
             childFactory = ::child,
         )
 
-    override val childStack: Value<ChildStack<*, Child<T>>> = router.stack
+    override val childStack: Value<ChildStack<*, Child<T>>> get() = stack
 
     private fun child(config: Config, componentContext: ComponentContext): Child<T> =
         when (config) {
@@ -49,9 +52,9 @@ internal class DynamicFeatureComponent<out T : Any>(
     private fun DisposableScope.loadFeature() {
         featureInstaller.install(name = name).subscribeScoped {
             when (it) {
-                is FeatureInstaller.Result.Installed -> router.replaceCurrent(Config.Feature)
+                is FeatureInstaller.Result.Installed -> navigation.replaceCurrent(Config.Feature)
                 is FeatureInstaller.Result.Cancelled,
-                is FeatureInstaller.Result.Error -> router.replaceCurrent(Config.Error)
+                is FeatureInstaller.Result.Error -> navigation.replaceCurrent(Config.Error)
             }.let {}
         }
     }
diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/DetailsRouter.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/DetailsRouter.kt
index def05d80..675f61c3 100644
--- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/DetailsRouter.kt
+++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/DetailsRouter.kt
@@ -2,10 +2,10 @@ package com.arkivanov.sample.shared.multipane
 
 import com.arkivanov.decompose.ComponentContext
 import com.arkivanov.decompose.router.stack.ChildStack
-import com.arkivanov.decompose.router.stack.activeChild
+import com.arkivanov.decompose.router.stack.StackNavigation
+import com.arkivanov.decompose.router.stack.childStack
 import com.arkivanov.decompose.router.stack.navigate
 import com.arkivanov.decompose.router.stack.popWhile
-import com.arkivanov.decompose.router.stack.stackRouter
 import com.arkivanov.decompose.value.Value
 import com.arkivanov.essenty.parcelable.Parcelable
 import com.arkivanov.essenty.parcelable.Parcelize
@@ -22,15 +22,16 @@ internal class DetailsRouter(
     private val onFinished: () -> Unit
 ) {
 
-    private val router =
-        componentContext.stackRouter<Config, DetailsChild>(
+    private val navigation = StackNavigation<Config>()
+
+    val stack: Value<ChildStack<Config, DetailsChild>> =
+        componentContext.childStack(
+            source = navigation,
             initialConfiguration = Config.None,
             key = "DetailsRouter",
             childFactory = ::createChild,
         )
 
-    val stack: Value<ChildStack<Config, DetailsChild>> = router.stack
-
     private fun createChild(config: Config, componentContext: ComponentContext): DetailsChild =
         when (config) {
             is Config.None -> DetailsChild.None
@@ -47,7 +48,7 @@ internal class DetailsRouter(
         )
 
     fun showArticle(id: Long) {
-        router.navigate { stack ->
+        navigation.navigate { stack ->
             stack
                 .dropLastWhile { it is Config.Details }
                 .plus(Config.Details(articleId = id))
@@ -55,11 +56,11 @@ internal class DetailsRouter(
     }
 
     fun closeArticle() {
-        router.popWhile { it !is Config.None }
+        navigation.popWhile { it !is Config.None }
     }
 
     fun isShown(): Boolean =
-        when (router.activeChild.configuration) {
+        when (stack.value.active.configuration) {
             is Config.None -> false
             is Config.Details -> true
         }
diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/ListRouter.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/ListRouter.kt
index 4ad34c80..c8f006e9 100644
--- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/ListRouter.kt
+++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/ListRouter.kt
@@ -2,10 +2,10 @@ package com.arkivanov.sample.shared.multipane
 
 import com.arkivanov.decompose.ComponentContext
 import com.arkivanov.decompose.router.stack.ChildStack
-import com.arkivanov.decompose.router.stack.activeChild
+import com.arkivanov.decompose.router.stack.StackNavigation
+import com.arkivanov.decompose.router.stack.childStack
 import com.arkivanov.decompose.router.stack.pop
 import com.arkivanov.decompose.router.stack.push
-import com.arkivanov.decompose.router.stack.stackRouter
 import com.arkivanov.decompose.value.Value
 import com.arkivanov.essenty.parcelable.Parcelable
 import com.arkivanov.essenty.parcelable.Parcelize
@@ -22,15 +22,16 @@ internal class ListRouter(
     private val onArticleSelected: (id: Long) -> Unit
 ) {
 
-    private val router =
-        componentContext.stackRouter<Config, ListChild>(
+    private val navigation = StackNavigation<Config>()
+
+    val stack: Value<ChildStack<Config, ListChild>> =
+        componentContext.childStack(
+            source = navigation,
             initialConfiguration = Config.List,
             key = "MainRouter",
             childFactory = ::createChild,
         )
 
-    val stack: Value<ChildStack<Config, ListChild>> = router.stack
-
     private fun createChild(config: Config, componentContext: ComponentContext): ListChild =
         when (config) {
             is Config.List -> ListChild.List(articleList(componentContext))
@@ -46,14 +47,14 @@ internal class ListRouter(
         )
 
     fun moveToBackStack() {
-        if (router.activeChild.configuration !is Config.None) {
-            router.push(Config.None)
+        if (stack.value.active.configuration !is Config.None) {
+            navigation.push(Config.None)
         }
     }
 
     fun show() {
-        if (router.activeChild.configuration !is Config.List) {
-            router.pop()
+        if (stack.value.active.configuration !is Config.List) {
+            navigation.pop()
         }
     }
 
diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootComponent.kt
index b528033c..23a7214a 100644
--- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootComponent.kt
+++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootComponent.kt
@@ -3,9 +3,9 @@ package com.arkivanov.sample.shared.root
 import com.arkivanov.decompose.ComponentContext
 import com.arkivanov.decompose.ExperimentalDecomposeApi
 import com.arkivanov.decompose.router.stack.ChildStack
-import com.arkivanov.decompose.router.stack.StackRouter
+import com.arkivanov.decompose.router.stack.StackNavigation
 import com.arkivanov.decompose.router.stack.bringToFront
-import com.arkivanov.decompose.router.stack.stackRouter
+import com.arkivanov.decompose.router.stack.childStack
 import com.arkivanov.decompose.router.stack.webhistory.WebHistoryController
 import com.arkivanov.decompose.value.Value
 import com.arkivanov.essenty.parcelable.Parcelable
@@ -27,17 +27,21 @@ class RootComponent constructor(
     webHistoryController: WebHistoryController? = null,
 ) : Root, ComponentContext by componentContext {
 
-    private val router: StackRouter<Config, Child> =
-        stackRouter(
+    private val navigation = StackNavigation<Config>()
+
+    private val stack =
+        childStack(
+            source = navigation,
             initialStack = { getInitialStack(deepLink) },
             childFactory = ::child,
         )
 
-    override val childStack: Value<ChildStack<*, Child>> = router.stack
+    override val childStack: Value<ChildStack<*, Child>> = stack
 
     init {
         webHistoryController?.attach(
-            router = router,
+            navigator = navigation,
+            stack = stack,
             getPath = ::getPathForConfig,
             getConfiguration = ::getConfigForPath,
         )
@@ -51,15 +55,15 @@ class RootComponent constructor(
         }
 
     override fun onCountersTabClicked() {
-        router.bringToFront(Config.Counters)
+        navigation.bringToFront(Config.Counters)
     }
 
     override fun onMultiPaneTabClicked() {
-        router.bringToFront(Config.MultiPane)
+        navigation.bringToFront(Config.MultiPane)
     }
 
     override fun onDynamicFeaturesTabClicked() {
-        router.bringToFront(Config.DynamicFeatures)
+        navigation.bringToFront(Config.DynamicFeatures)
     }
 
     private companion object {


Test patch:
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/RelayTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/RelayTest.kt
new file mode 100644
index 00000000..41a9c761
--- /dev/null
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/RelayTest.kt
@@ -0,0 +1,72 @@
+package com.arkivanov.decompose
+
+import kotlin.test.Test
+import kotlin.test.assertContentEquals
+import kotlin.test.assertFalse
+
+@Suppress("TestFunctionName")
+class RelayTest {
+
+    @Test
+    fun GIVEN_subscribed_WHEN_accept_values_THEN_values_emitted_in_order() {
+        val relay = Relay<Int>()
+        val list = relay.subscribe()
+
+        relay.accept(1)
+        relay.accept(2)
+        relay.accept(3)
+
+        assertContentEquals(listOf(1, 2, 3), list)
+    }
+
+    @Test
+    fun GIVEN_multiple_observers_subscribed_WHEN_accept_values_THEN_values_emitted_in_order_to_all_observers() {
+        val relay = Relay<Int>()
+        val list1 = relay.subscribe()
+        val list2 = relay.subscribe()
+
+        relay.accept(1)
+        relay.accept(2)
+        relay.accept(3)
+
+        assertContentEquals(listOf(1, 2, 3), list1)
+        assertContentEquals(listOf(1, 2, 3), list2)
+    }
+
+    @Test
+    fun GIVEN_subscribed_and_unsubscribed_WHEN_accept_value_THEN_value_not_emitted() {
+        val relay = Relay<Int>()
+        var isEmitted = false
+        val observer: (Int) -> Unit = { isEmitted = true }
+        relay.subscribe(observer)
+        relay.unsubscribe(observer)
+
+        relay.accept(1)
+
+        assertFalse(isEmitted)
+    }
+
+    @Test
+    fun WHEN_accept_recursively_THEN_values_emitted_in_order() {
+        val relay = Relay<Int>()
+        val list = ArrayList<Int>()
+
+        relay.subscribe { value ->
+            if (value == 1) {
+                relay.accept(2)
+            }
+            list += value
+        }
+
+        relay.accept(1)
+
+        assertContentEquals(listOf(1, 2), list)
+    }
+
+    private fun <T> Relay<T>.subscribe(): List<T> {
+        val list = ArrayList<T>()
+        subscribe { list += it }
+
+        return list
+    }
+}
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/RouterTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/ChildStackControllerTest.kt
similarity index 63%
rename from decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/RouterTest.kt
rename to decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/ChildStackControllerTest.kt
index 6fcb6339..8bac11be 100644
--- a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/RouterTest.kt
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/ChildStackControllerTest.kt
@@ -1,5 +1,15 @@
-package com.arkivanov.decompose.router.stack
-
+package com.arkivanov.decompose.controller.stack
+
+import com.arkivanov.decompose.router.stack.ChildStack
+import com.arkivanov.decompose.router.stack.ChildStackController
+import com.arkivanov.decompose.router.stack.RouterEntry
+import com.arkivanov.decompose.router.stack.RouterStack
+import com.arkivanov.decompose.router.stack.StackController
+import com.arkivanov.decompose.router.stack.StackHolder
+import com.arkivanov.decompose.router.stack.StackNavigation
+import com.arkivanov.decompose.router.stack.StackNavigationSource
+import com.arkivanov.decompose.router.stack.configurationStack
+import com.arkivanov.decompose.router.stack.navigate
 import com.arkivanov.decompose.statekeeper.TestStateKeeperDispatcher
 import com.arkivanov.essenty.backpressed.BackPressedDispatcher
 import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
@@ -11,7 +21,7 @@ import kotlin.test.assertEquals
 import kotlin.test.assertFalse
 
 @Suppress("TestFunctionName")
-class RouterTest {
+class ChildStackControllerTest {
 
     @Test
     fun WHEN_navigate_THEN_new_stack_applied() {
@@ -19,11 +29,12 @@ class RouterTest {
         val config2 = Config()
         val config3 = Config()
         val config4 = Config()
-        val router = router(initialStack = listOf(config1, config2, config3))
+        val navigation = StackNavigation<Config>()
+        val controller = controller(source = navigation, initialStack = listOf(config1, config2, config3))
 
-        router.navigate { listOf(config1, config3, config4) }
+        navigation.navigate { listOf(config1, config3, config4) }
 
-        assertEquals(listOf(config1, config3, config4), router.configurations)
+        assertEquals(listOf(config1, config3, config4), controller.configurations)
     }
 
     @Test
@@ -32,11 +43,12 @@ class RouterTest {
         val config2 = Config()
         val config3 = Config()
         val config4 = Config()
-        val router = router(initialStack = listOf(config1, config2, config3))
+        val navigation = StackNavigation<Config>()
+        controller(source = navigation, initialStack = listOf(config1, config2, config3))
 
         var resultNewStack: List<Config>? = null
         var resultOldStack: List<Config>? = null
-        router.navigate(transformer = { listOf(config1, config3, config4) }) { newStack, oldStack ->
+        navigation.navigate(transformer = { listOf(config1, config3, config4) }) { newStack, oldStack ->
             resultNewStack = newStack
             resultOldStack = oldStack
         }
@@ -50,19 +62,20 @@ class RouterTest {
         val config1 = Config()
         val config2 = Config()
         val config3 = Config()
-        val controller = TestStackController()
-        val router = router(initialStack = listOf(config1), controller = controller)
+        val navigation = StackNavigation<Config>()
+        val stackController = TestStackController()
+        val controller = controller(source = navigation, initialStack = listOf(config1), controller = stackController)
 
-        controller.onNavigate =
+        stackController.onNavigate =
             { newConfigs ->
                 if (newConfigs == listOf(config2)) {
-                    router.navigate { listOf(config3) }
+                    navigation.navigate { listOf(config3) }
                 }
             }
 
-        router.navigate { listOf(config2) }
+        navigation.navigate { listOf(config2) }
 
-        assertEquals(listOf(config3), router.configurations)
+        assertEquals(listOf(config3), controller.configurations)
     }
 
 
@@ -71,22 +84,23 @@ class RouterTest {
         val config1 = Config()
         val config2 = Config()
         val config3 = Config()
-        val controller = TestStackController()
-        val router = router(initialStack = listOf(config1), controller = controller)
+        val navigation = StackNavigation<Config>()
+        val stackController = TestStackController()
+        controller(source = navigation, initialStack = listOf(config1), controller = stackController)
 
         var isCalledSynchronously = false
-        controller.onNavigate =
+        stackController.onNavigate =
             { newConfigs ->
                 if (newConfigs == listOf(config2)) {
                     var isCalled = false
-                    router.navigate(transformer = { listOf(config3) }) { _, _ ->
+                    navigation.navigate(transformer = { listOf(config3) }) { _, _ ->
                         isCalled = true
                     }
                     isCalledSynchronously = isCalled
                 }
             }
 
-        router.navigate { listOf(config2) }
+        navigation.navigate { listOf(config2) }
 
         assertFalse(isCalledSynchronously)
     }
@@ -97,32 +111,35 @@ class RouterTest {
         val config1 = Config()
         val config2 = Config()
         val config3 = Config()
-        val controller = TestStackController()
-        val router = router(initialStack = listOf(config1), controller = controller)
+        val navigation = StackNavigation<Config>()
+        val stackController = TestStackController()
+        controller(source = navigation, initialStack = listOf(config1), controller = stackController)
 
         var resultNewStack: List<Config>? = null
         var resultOldStack: List<Config>? = null
-        controller.onNavigate =
+        stackController.onNavigate =
             { newConfigs ->
                 if (newConfigs == listOf(config2)) {
-                    router.navigate(transformer = { listOf(config3) }) { newStack, oldStack ->
+                    navigation.navigate(transformer = { listOf(config3) }) { newStack, oldStack ->
                         resultNewStack = newStack
                         resultOldStack = oldStack
                     }
                 }
             }
 
-        router.navigate { listOf(config2) }
+        navigation.navigate { listOf(config2) }
 
         assertEquals(listOf(config3), resultNewStack)
         assertEquals(listOf(config2), resultOldStack)
     }
 
-    private fun router(
+    private fun controller(
+        source: StackNavigationSource<Config>,
         initialStack: List<Config>,
         controller: TestStackController = TestStackController(),
-    ): StackRouter<Config, Config> =
-        StackRouterImpl(
+    ): ChildStackController<Config, Config> =
+        ChildStackController(
+            source = source,
             lifecycle = LifecycleRegistry(),
             backPressedHandler = BackPressedDispatcher(),
             popOnBackPressed = false,
@@ -146,7 +163,7 @@ class RouterTest {
                 }
             )
 
-        private val StackRouter<Config, *>.configurations: List<Config>
+        private val ChildStackController<Config, *>.configurations: List<Config>
             get() = stack.value.configurations
 
         private val ChildStack<Config, *>.configurations: List<Config>
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/TestStackRouter.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/TestStackRouter.kt
index f8461832..a886257d 100644
--- a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/TestStackRouter.kt
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/TestStackRouter.kt
@@ -4,10 +4,10 @@ import com.arkivanov.decompose.Child
 import com.arkivanov.decompose.value.MutableValue
 import com.arkivanov.decompose.value.Value
 
-class TestStackRouter<C : Any>(stack: List<C>) : TestStackNavigator<C>(stack), StackRouter<C, Any> {
+class TestStackRouter<C : Any>(stack: List<C>) : TestStackNavigator<C>(stack) {
 
     private val _stack = MutableValue(stack.toRouterState())
-    override val stack: Value<ChildStack<C, Any>> = _stack
+    val stack: Value<ChildStack<C, Any>> = _stack
 
     override fun onConfigurationsChanged(configurations: List<C>) {
         super.onConfigurationsChanged(configurations)
diff --git a/decompose/src/jsTest/kotlin/com/arkivanov/decompose/router/stack/webhistory/DefaultWebHistoryControllerTest.kt b/decompose/src/jsTest/kotlin/com/arkivanov/decompose/router/stack/webhistory/DefaultWebHistoryControllerTest.kt
index 12975faa..f619b378 100644
--- a/decompose/src/jsTest/kotlin/com/arkivanov/decompose/router/stack/webhistory/DefaultWebHistoryControllerTest.kt
+++ b/decompose/src/jsTest/kotlin/com/arkivanov/decompose/router/stack/webhistory/DefaultWebHistoryControllerTest.kt
@@ -417,7 +417,8 @@ class DefaultWebHistoryControllerTest {
 
     private fun attach(router: TestStackRouter<Config>) {
         controller.attach(
-            router = router,
+            navigator = router,
+            stack = router.stack,
             getPath = { "/${it.value}" },
             getConfiguration = { Config(it.removePrefix("/").toInt()) },
         )