Repository: arkivanov/Decompose
Pull request #354 (2023-03-29 21:24:05)

Error handling and main thread checks


Patch:
diff --git a/build.gradle.kts b/build.gradle.kts
index 2586da58..c76dce3d 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,4 +1,5 @@
 import com.arkivanov.gradle.AndroidConfig
+import com.arkivanov.gradle.BinaryCompatibilityValidatorConfig
 import com.arkivanov.gradle.PublicationConfig
 import com.arkivanov.gradle.ensureUnreachableTasksDisabled
 import com.arkivanov.gradle.iosCompat
@@ -42,6 +43,9 @@ setupDefaults(
         compileSdkVersion = 33,
         targetSdkVersion = 33,
     ),
+    binaryCompatibilityValidatorConfig = BinaryCompatibilityValidatorConfig(
+        nonPublicMarkers = listOf("com.arkivanov.decompose.InternalDecomposeApi"),
+    ),
     publicationConfig = PublicationConfig(
         group = "com.arkivanov.decompose",
         version = deps.versions.decompose.get(),
diff --git a/decompose/api/android/decompose.api b/decompose/api/android/decompose.api
index b4a01fc0..e2200e2d 100644
--- a/decompose/api/android/decompose.api
+++ b/decompose/api/android/decompose.api
@@ -67,11 +67,9 @@ public abstract interface annotation class com/arkivanov/decompose/ExperimentalD
 public abstract interface annotation class com/arkivanov/decompose/InternalDecomposeApi : java/lang/annotation/Annotation {
 }
 
-public final class com/arkivanov/decompose/lifecycle/MergedLifecycle : com/arkivanov/essenty/lifecycle/Lifecycle {
-	public fun <init> (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)V
-	public fun getState ()Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;
-	public fun subscribe (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V
-	public fun unsubscribe (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V
+public final class com/arkivanov/decompose/errorhandler/ErrorHandlersKt {
+	public static final fun getOnDecomposeError ()Lkotlin/jvm/functions/Function1;
+	public static final fun setOnDecomposeError (Lkotlin/jvm/functions/Function1;)V
 }
 
 public abstract interface class com/arkivanov/decompose/router/children/ChildNavState {
diff --git a/decompose/api/jvm/decompose.api b/decompose/api/jvm/decompose.api
index 421d91c1..20c79416 100644
--- a/decompose/api/jvm/decompose.api
+++ b/decompose/api/jvm/decompose.api
@@ -53,11 +53,9 @@ public abstract interface annotation class com/arkivanov/decompose/ExperimentalD
 public abstract interface annotation class com/arkivanov/decompose/InternalDecomposeApi : java/lang/annotation/Annotation {
 }
 
-public final class com/arkivanov/decompose/lifecycle/MergedLifecycle : com/arkivanov/essenty/lifecycle/Lifecycle {
-	public fun <init> (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)V
-	public fun getState ()Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;
-	public fun subscribe (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V
-	public fun unsubscribe (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V
+public final class com/arkivanov/decompose/errorhandler/ErrorHandlersKt {
+	public static final fun getOnDecomposeError ()Lkotlin/jvm/functions/Function1;
+	public static final fun setOnDecomposeError (Lkotlin/jvm/functions/Function1;)V
 }
 
 public abstract interface class com/arkivanov/decompose/router/children/ChildNavState {
diff --git a/decompose/build.gradle.kts b/decompose/build.gradle.kts
index 2b0ec72e..f63ca068 100644
--- a/decompose/build.gradle.kts
+++ b/decompose/build.gradle.kts
@@ -42,6 +42,7 @@ kotlin {
         all {
             languageSettings {
                 optIn("com.arkivanov.decompose.InternalDecomposeApi")
+                optIn("com.arkivanov.decompose.ExperimentalDecomposeApi")
             }
         }
 
diff --git a/decompose/src/androidMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt b/decompose/src/androidMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt
new file mode 100644
index 00000000..cd45e88a
--- /dev/null
+++ b/decompose/src/androidMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt
@@ -0,0 +1,16 @@
+package com.arkivanov.decompose.errorhandler
+
+import android.util.Log
+
+private const val LOG_TAG = "Decompose"
+private var isLogCatEnabled = true
+
+internal actual fun printError(exception: Exception) {
+    if (isLogCatEnabled) {
+        try {
+            Log.e(LOG_TAG, exception.message ?: "An occurred in Decompose", exception)
+        } catch (e: Exception) {
+            isLogCatEnabled = false
+        }
+    }
+}
diff --git a/decompose/src/androidMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt b/decompose/src/androidMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt
new file mode 100644
index 00000000..145aa7c9
--- /dev/null
+++ b/decompose/src/androidMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt
@@ -0,0 +1,18 @@
+package com.arkivanov.decompose.mainthread
+
+import android.os.Looper
+import com.arkivanov.decompose.errorhandler.onDecomposeError
+
+private val mainThreadId: Long? by lazy {
+    try {
+        Looper.getMainLooper().thread.id
+    } catch (e: Exception) {
+        null
+    }
+}
+
+internal actual fun checkMainThread() {
+    if ((mainThreadId != null) && (Thread.currentThread().id != mainThreadId)) {
+        onDecomposeError(NotOnMainThreadException(currentThreadName = Thread.currentThread().name))
+    }
+}
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Relay.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Relay.kt
index e71551d1..ec0f41fe 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Relay.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Relay.kt
@@ -1,6 +1,10 @@
 package com.arkivanov.decompose
 
-internal class Relay<T> {
+import com.arkivanov.decompose.mainthread.checkMainThread
+
+internal class Relay<T>(
+    private val isMainThreadCheckEnabled: Boolean = false,
+) {
 
     init {
         ensureNeverFrozen()
@@ -12,6 +16,10 @@ internal class Relay<T> {
     private var observers = emptySet<(T) -> Unit>()
 
     fun accept(value: T) {
+        if (isMainThreadCheckEnabled) {
+            checkMainThread()
+        }
+
         lock.synchronized {
             queue.addLast(value)
 
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/errorhandler/ErrorHandlers.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/errorhandler/ErrorHandlers.kt
new file mode 100644
index 00000000..4e25eb2f
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/errorhandler/ErrorHandlers.kt
@@ -0,0 +1,9 @@
+package com.arkivanov.decompose.errorhandler
+
+import com.arkivanov.decompose.ExperimentalDecomposeApi
+
+/**
+ * Called when a non-fatal error has occurred in Decompose.
+ */
+@ExperimentalDecomposeApi
+var onDecomposeError: (Exception) -> Unit = ::printError
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt
new file mode 100644
index 00000000..35a20486
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt
@@ -0,0 +1,3 @@
+package com.arkivanov.decompose.errorhandler
+
+internal expect fun printError(exception: Exception)
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt
new file mode 100644
index 00000000..a689f967
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt
@@ -0,0 +1,3 @@
+package com.arkivanov.decompose.mainthread
+
+internal expect fun checkMainThread()
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/mainthread/NotOnMainThreadException.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/mainthread/NotOnMainThreadException.kt
new file mode 100644
index 00000000..c4604095
--- /dev/null
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/mainthread/NotOnMainThreadException.kt
@@ -0,0 +1,5 @@
+package com.arkivanov.decompose.mainthread
+
+internal class NotOnMainThreadException(currentThreadName: String?) : Exception(
+    "Expected to be called on the main thread, but was ${currentThreadName ?: "unknown"}"
+)
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
index d9f67f9d..392cb0c7 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildItem.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildItem.kt
@@ -10,9 +10,11 @@ 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,
@@ -22,7 +24,7 @@ internal sealed interface ChildItem<out C : Any, out T : Any> {
 
     data class Destroyed<out C : Any>(
         override val configuration: C,
-        val savedState: ParcelableContainer? = null
+        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/ChildrenFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenFactory.kt
index 9193344e..28e5f4be 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenFactory.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenFactory.kt
@@ -75,8 +75,6 @@ fun <C : Any, T : Any, E : Any, N : NavState<C>, S : Any> ComponentContext.child
 
     val navigator =
         stateKeeper.consume<SavedState>(key = key).let { savedState ->
-            val restoredNavState: N? = savedState?.navState?.let(restoreState)
-
             ChildrenNavigator(
                 lifecycle = lifecycle,
                 retainedInstanceSupplier = { factory -> instanceKeeper.getOrCreate(key = key, factory = factory) },
@@ -85,8 +83,8 @@ fun <C : Any, T : Any, E : Any, N : NavState<C>, S : Any> ComponentContext.child
                     backHandler = backHandler.child(),
                     childFactory = childFactory,
                 ),
-                navState = restoredNavState ?: initialState(),
-                savedChildState = savedState?.childState?.takeUnless { restoredNavState == null },
+                navState = savedState?.navState?.let(restoreState) ?: initialState(),
+                savedChildState = savedState?.childState,
             )
         }
 
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
index 231d47c0..b65e43b4 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenNavigator.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenNavigator.kt
@@ -100,7 +100,7 @@ internal class ChildrenNavigator<out C : Any, out T : Any, N : NavState<C>>(
     fun saveChildState(): List<ParcelableContainer?> =
         items.map { item ->
             when (item) {
-                is Created -> item.stateKeeperDispatcher.save()
+                is Created -> item.saveStateIfNeeded()
                 is ChildItem.Destroyed -> item.savedState
             }
         }
@@ -192,6 +192,13 @@ internal class ChildrenNavigator<out C : Any, out T : Any, N : NavState<C>>(
         }
     }
 
+    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()
@@ -202,7 +209,7 @@ internal class ChildrenNavigator<out C : Any, out T : Any, N : NavState<C>>(
                     is Created -> {
                         when (status) {
                             Status.DESTROYED -> {
-                                val savedState = item.stateKeeperDispatcher.save()
+                                val savedState = item.saveStateIfNeeded()
                                 item.backHandler.stop()
                                 item.instanceKeeperDispatcher.destroy()
                                 item.lifecycleRegistry.destroy()
@@ -215,6 +222,7 @@ internal class ChildrenNavigator<out C : Any, out T : Any, N : NavState<C>>(
 
                                 item
                                     .takeIf { it.lifecycleRegistry.state != Lifecycle.State.CREATED }
+                                    ?.copy(savedState = item.saveStateIfNeeded())
                                     ?.apply {
                                         backHandler.stop()
                                         lifecycleRegistry.stop()
@@ -227,6 +235,7 @@ internal class ChildrenNavigator<out C : Any, out T : Any, N : NavState<C>>(
 
                                 item
                                     .takeIf { it.lifecycleRegistry.state != Lifecycle.State.RESUMED }
+                                    ?.copy(savedState = null)
                                     ?.apply {
                                         backHandler.start()
                                         lifecycleRegistry.resume()
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/SimpleNavigation.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/SimpleNavigation.kt
index 39248398..de9d5677 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/SimpleNavigation.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/SimpleNavigation.kt
@@ -8,7 +8,7 @@ import com.arkivanov.decompose.Relay
  */
 class SimpleNavigation<T : Any> : NavigationSource<T> {
 
-    private val relay = Relay<T>()
+    private val relay = Relay<T>(isMainThreadCheckEnabled = true)
 
     override fun subscribe(observer: (T) -> Unit) {
         relay.subscribe(observer)
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/DefaultOverlayNavigation.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/DefaultOverlayNavigation.kt
index 76c13f58..abd70359 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/DefaultOverlayNavigation.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/DefaultOverlayNavigation.kt
@@ -5,7 +5,7 @@ import com.arkivanov.decompose.router.overlay.OverlayNavigationSource.Event
 
 internal class DefaultOverlayNavigation<C : Any> : OverlayNavigation<C> {
 
-    private val relay = Relay<Event<C>>()
+    private val relay = Relay<Event<C>>(isMainThreadCheckEnabled = true)
 
     override fun navigate(
         transformer: (configuration: C?) -> C?,
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/DefaultStackNavigation.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/DefaultStackNavigation.kt
index 288d3b26..8105ff37 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/DefaultStackNavigation.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/DefaultStackNavigation.kt
@@ -5,7 +5,7 @@ import com.arkivanov.decompose.router.stack.StackNavigationSource.Event
 
 internal class DefaultStackNavigation<C : Any> : StackNavigation<C> {
 
-    private val relay = Relay<Event<C>>()
+    private val relay = Relay<Event<C>>(isMainThreadCheckEnabled = true)
 
     override fun navigate(transformer: (stack: List<C>) -> List<C>, onComplete: (newStack: List<C>, oldStack: List<C>) -> Unit) {
         relay.accept(Event(transformer, onComplete))
diff --git a/decompose/src/darwinMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt b/decompose/src/darwinMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt
new file mode 100644
index 00000000..b9f71ca7
--- /dev/null
+++ b/decompose/src/darwinMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt
@@ -0,0 +1,5 @@
+package com.arkivanov.decompose.errorhandler
+
+internal actual fun printError(exception: Exception) {
+    exception.printStackTrace()
+}
diff --git a/decompose/src/darwinMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt b/decompose/src/darwinMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt
new file mode 100644
index 00000000..0e44c8be
--- /dev/null
+++ b/decompose/src/darwinMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt
@@ -0,0 +1,10 @@
+package com.arkivanov.decompose.mainthread
+
+import com.arkivanov.decompose.errorhandler.onDecomposeError
+import platform.Foundation.NSThread
+
+internal actual fun checkMainThread() {
+    if (!NSThread.isMainThread()) {
+        onDecomposeError(NotOnMainThreadException(currentThreadName = NSThread.currentThread.description()))
+    }
+}
diff --git a/decompose/src/jsMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt b/decompose/src/jsMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt
new file mode 100644
index 00000000..b9f71ca7
--- /dev/null
+++ b/decompose/src/jsMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt
@@ -0,0 +1,5 @@
+package com.arkivanov.decompose.errorhandler
+
+internal actual fun printError(exception: Exception) {
+    exception.printStackTrace()
+}
diff --git a/decompose/src/jsMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt b/decompose/src/jsMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt
new file mode 100644
index 00000000..e2f6c2f6
--- /dev/null
+++ b/decompose/src/jsMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt
@@ -0,0 +1,5 @@
+package com.arkivanov.decompose.mainthread
+
+internal actual fun checkMainThread() {
+    // No-op
+}
diff --git a/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt b/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt
new file mode 100644
index 00000000..b9f71ca7
--- /dev/null
+++ b/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt
@@ -0,0 +1,5 @@
+package com.arkivanov.decompose.errorhandler
+
+internal actual fun printError(exception: Exception) {
+    exception.printStackTrace()
+}
diff --git a/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt b/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt
new file mode 100644
index 00000000..838d2675
--- /dev/null
+++ b/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt
@@ -0,0 +1,9 @@
+package com.arkivanov.decompose.mainthread
+
+import com.arkivanov.decompose.errorhandler.onDecomposeError
+
+internal actual fun checkMainThread() {
+    if (mainThreadChecker?.isMainThread() == false) {
+        onDecomposeError(NotOnMainThreadException(currentThreadName = Thread.currentThread().name))
+    }
+}
diff --git a/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/mainthread/MainThreadChecker.kt b/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/mainthread/MainThreadChecker.kt
new file mode 100644
index 00000000..eadb74f7
--- /dev/null
+++ b/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/mainthread/MainThreadChecker.kt
@@ -0,0 +1,9 @@
+package com.arkivanov.decompose.mainthread
+
+import com.arkivanov.decompose.InternalDecomposeApi
+
+@InternalDecomposeApi
+interface MainThreadChecker {
+
+    fun isMainThread(): Boolean
+}
diff --git a/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/mainthread/MainThreadCheckerProvider.kt b/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/mainthread/MainThreadCheckerProvider.kt
new file mode 100644
index 00000000..f80db8d5
--- /dev/null
+++ b/decompose/src/jvmMain/kotlin/com/arkivanov/decompose/mainthread/MainThreadCheckerProvider.kt
@@ -0,0 +1,7 @@
+package com.arkivanov.decompose.mainthread
+
+import java.util.ServiceLoader
+
+internal val mainThreadChecker: MainThreadChecker? by lazy {
+    ServiceLoader.load(MainThreadChecker::class.java).firstOrNull()
+}
diff --git a/docs/navigation/overview.md b/docs/navigation/overview.md
index 0db5db0b..52f30e6b 100644
--- a/docs/navigation/overview.md
+++ b/docs/navigation/overview.md
@@ -38,3 +38,12 @@ If you support the `android` target, make sure you have applied [kotlin-parceliz
 
 !!!warning
     On Android the amount of data that can be preserved is [limited](https://developer.android.com/guide/components/activities/parcelables-and-bundles). Please mind the size of configurations.
+
+## Navigation and the main thread
+
+The navigation API is thread-safe, technically the navigation can be performed on any thread. However, it's strongly recommended to perform the navigation on the Main thread. Since the navigation is performed synchronously, Decompose instantiates components and calls lifecycle callbacks on the current thread. Navigating on a background thread may cause unexpected behaviour.
+
+!!!warning
+    Always perform the navigation on the Main thread.
+
+Decompose tries its best to detect when the navigation is performed on a non-main thread. When it happens, Decompose calls the special error handler - `onDecomposeError`. By default, it prints the exception to logs,  however you can override the default behaviour by providing your own handler.
diff --git a/extensions-compose-jetbrains/build.gradle.kts b/extensions-compose-jetbrains/build.gradle.kts
index 7dc124b4..b1246891 100644
--- a/extensions-compose-jetbrains/build.gradle.kts
+++ b/extensions-compose-jetbrains/build.gradle.kts
@@ -28,6 +28,12 @@ kotlin {
         val android by bundle()
         val jvm by bundle()
 
+        all {
+            languageSettings {
+                optIn("com.arkivanov.decompose.InternalDecomposeApi")
+            }
+        }
+
         common.main.dependencies {
             implementation(project(":decompose"))
             implementation(compose.foundation)
diff --git a/extensions-compose-jetbrains/src/jvmMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/mainthread/SwingMainThreadChecker.kt b/extensions-compose-jetbrains/src/jvmMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/mainthread/SwingMainThreadChecker.kt
new file mode 100644
index 00000000..4945ff9b
--- /dev/null
+++ b/extensions-compose-jetbrains/src/jvmMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/mainthread/SwingMainThreadChecker.kt
@@ -0,0 +1,10 @@
+package com.arkivanov.decompose.extensions.compose.jetbrains.mainthread
+
+import com.arkivanov.decompose.mainthread.MainThreadChecker
+import javax.swing.SwingUtilities
+
+internal class SwingMainThreadChecker : MainThreadChecker {
+
+    override fun isMainThread(): Boolean =
+        SwingUtilities.isEventDispatchThread()
+}
diff --git a/extensions-compose-jetbrains/src/jvmMain/resources/META-INF/services/com.arkivanov.decompose.mainthread.MainThreadChecker b/extensions-compose-jetbrains/src/jvmMain/resources/META-INF/services/com.arkivanov.decompose.mainthread.MainThreadChecker
new file mode 100644
index 00000000..d78029ad
--- /dev/null
+++ b/extensions-compose-jetbrains/src/jvmMain/resources/META-INF/services/com.arkivanov.decompose.mainthread.MainThreadChecker
@@ -0,0 +1,1 @@
+com.arkivanov.decompose.extensions.compose.jetbrains.mainthread.SwingMainThreadChecker
diff --git a/settings.gradle.kts b/settings.gradle.kts
index d148e1b5..176583f6 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -18,7 +18,7 @@ pluginManagement {
     resolutionStrategy {
         eachPlugin {
             if (requested.id.toString() == "com.arkivanov.gradle.setup") {
-                useModule("com.github.arkivanov:gradle-setup-plugin:9666736fb8")
+                useModule("com.github.arkivanov:gradle-setup-plugin:5a89a70d63")
             }
         }
     }


Test patch:
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
index 1e26018f..b281db7b 100644
--- a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenSavedStateTest.kt
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/children/ChildrenSavedStateTest.kt
@@ -19,7 +19,7 @@ import kotlin.test.assertTrue
 internal class ChildrenSavedStateTest : ChildrenTestBase() {
 
     @Test
-    fun WHEN_child_switched_from_active_to_inactive_THEN_state_not_saved() {
+    fun WHEN_child_switched_from_active_to_inactive_THEN_state_saved() {
         val children by context.children(initialState = stateOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
         var isCalled = false
         children.getById(id = 3).requireInstance().stateKeeper.register(key = "key") {
@@ -29,7 +29,7 @@ internal class ChildrenSavedStateTest : ChildrenTestBase() {
 
         navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by INACTIVE) }
 
-        assertFalse(isCalled)
+        assertTrue(isCalled)
     }
 
     @Test
@@ -47,7 +47,7 @@ internal class ChildrenSavedStateTest : ChildrenTestBase() {
     }
 
     @Test
-    fun WHEN_child_switched_from_inactive_to_destroyed_THEN_state_saved() {
+    fun WHEN_child_switched_from_inactive_to_destroyed_THEN_state_not_saved() {
         val children by context.children(initialState = stateOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
         var isCalled = false
         children.getById(id = 2).requireInstance().stateKeeper.register(key = "key") {
@@ -57,7 +57,7 @@ internal class ChildrenSavedStateTest : ChildrenTestBase() {
 
         navigate { listOf(1 by DESTROYED, 2 by DESTROYED, 3 by ACTIVE) }
 
-        assertTrue(isCalled)
+        assertFalse(isCalled)
     }
 
     @Test
@@ -183,17 +183,14 @@ internal class ChildrenSavedStateTest : ChildrenTestBase() {
         val oldStateKeeper = TestStateKeeperDispatcher()
         val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
         val oldChildren by oldContext.children(initialState = stateOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
-        oldChildren.getById(id = 2).requireInstance().stateKeeper.register(key = "key") { Config(id = 20) }
         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 restoredState2 = newChildren.getById(id = 2).requireInstance().stateKeeper.consume<Config>(key = "key")
         val restoredState3 = newChildren.getById(id = 3).requireInstance().stateKeeper.consume<Config>(key = "key")
 
-        assertEquals(Config(id = 20), restoredState2)
         assertEquals(Config(id = 30), restoredState3)
     }
 
@@ -201,18 +198,15 @@ internal class ChildrenSavedStateTest : ChildrenTestBase() {
     fun GIVEN_not_saving_state_WHEN_recreated_THEN_child_states_not_restored() {
         val oldStateKeeper = TestStateKeeperDispatcher()
         val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
-        val oldChildren by oldContext.children(initialState = stateOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE), saveState = { null })
-        oldChildren.getById(id = 2).requireInstance().stateKeeper.register(key = "key") { Config(id = 20) }
-        oldChildren.getById(id = 3).requireInstance().stateKeeper.register(key = "key") { Config(id = 30) }
+        val oldChildren by oldContext.children(initialState = stateOf(1 by ACTIVE), saveState = { null })
+        oldChildren.getById(id = 1).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(initialState = stateOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
-        val restoredState2 = newChildren.getById(id = 2).requireInstance().stateKeeper.consume<Config>(key = "key")
-        val restoredState3 = newChildren.getById(id = 3).requireInstance().stateKeeper.consume<Config>(key = "key")
+        val newChildren by newContext.children(initialState = stateOf(1 by ACTIVE))
+        val restoredState3 = newChildren.getById(id = 1).requireInstance().stateKeeper.consume<Config>(key = "key")
 
-        assertNull(restoredState2)
         assertNull(restoredState3)
     }
 
@@ -220,18 +214,15 @@ internal class ChildrenSavedStateTest : ChildrenTestBase() {
     fun GIVEN_not_restoring_state_WHEN_recreated_THEN_child_states_not_restored() {
         val oldStateKeeper = TestStateKeeperDispatcher()
         val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
-        val oldChildren by oldContext.children(initialState = stateOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE))
-        oldChildren.getById(id = 2).requireInstance().stateKeeper.register(key = "key") { Config(id = 20) }
-        oldChildren.getById(id = 3).requireInstance().stateKeeper.register(key = "key") { Config(id = 30) }
+        val oldChildren by oldContext.children(initialState = stateOf(1 by ACTIVE), saveState = { null })
+        oldChildren.getById(id = 1).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(initialState = stateOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE), restoreState = { null })
-        val restoredState2 = newChildren.getById(id = 2).requireInstance().stateKeeper.consume<Config>(key = "key")
-        val restoredState3 = newChildren.getById(id = 3).requireInstance().stateKeeper.consume<Config>(key = "key")
+        val newChildren by newContext.children(initialState = stateOf(1 by ACTIVE), restoreState = { null })
+        val restoredState3 = newChildren.getById(id = 1).requireInstance().stateKeeper.consume<Config>(key = "key")
 
-        assertNull(restoredState2)
         assertNull(restoredState3)
     }
 
@@ -268,28 +259,4 @@ internal class ChildrenSavedStateTest : ChildrenTestBase() {
 
         assertEquals(Config(id = 30), restoredState3)
     }
-
-    @Test
-    fun GIVEN_child_switched_to_inactive_and_recreated_twice_WHEN_child_switched_to_active_THEN_state_restored() {
-        val oldStateKeeper = TestStateKeeperDispatcher()
-        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
-        val oldChildren by oldContext.children(initialState = stateOf(1 by DESTROYED, 2 by ACTIVE, 3 by ACTIVE))
-        oldChildren.getById(id = 2).requireInstance().stateKeeper.register(key = "key") { Config(id = 30) }
-        navigate { listOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE) }
-
-        val savedState1 = oldStateKeeper.save()
-        val newStateKeeper1 = TestStateKeeperDispatcher(savedState1)
-        val newContext1 = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper1)
-        val newChildren1 by newContext1.children(restoreState = { stateOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE) })
-        newChildren1.getById(id = 2).requireInstance().stateKeeper.register(key = "key") { Config(id = 31) }
-
-        val savedState2 = newStateKeeper1.save()
-        val newStateKeeper2 = TestStateKeeperDispatcher(savedState2)
-        val newContext2 = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper2)
-        val newChildren2 by newContext2.children(restoreState = { stateOf(1 by DESTROYED, 2 by INACTIVE, 3 by ACTIVE) })
-
-        val restoredState2 = newChildren2.getById(id = 2).requireInstance().stateKeeper.consume<Config>(key = "key")
-
-        assertEquals(Config(id = 31), restoredState2)
-    }
 }
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/ChildStackIntegrationTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/ChildStackIntegrationTest.kt
index 5d9367d2..46c8cec8 100644
--- a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/ChildStackIntegrationTest.kt
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/stack/ChildStackIntegrationTest.kt
@@ -19,10 +19,11 @@ import com.arkivanov.essenty.parcelable.Parcelize
 import com.arkivanov.essenty.statekeeper.consume
 import kotlin.test.BeforeTest
 import kotlin.test.Test
-import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
-import kotlin.test.assertNotEquals
+import kotlin.test.assertNotSame
+import kotlin.test.assertNull
+import kotlin.test.assertSame
 import kotlin.test.assertTrue
 
 @Suppress("TestFunctionName")
@@ -220,7 +221,7 @@ class ChildStackIntegrationTest {
         val savedState = oldStateKeeper.save()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
-        val newStack by newContext.childStack()
+        val newStack by newContext.childStack(initialStack = emptyList())
 
         newStack.assertStack(1 to 1, 2 to 2)
     }
@@ -240,7 +241,7 @@ class ChildStackIntegrationTest {
     }
 
     @Test
-    fun WHEN_component_pushed_THEN_state_not_saved_for_previous_component() {
+    fun WHEN_child_pushed_THEN_state_saved_for_previous_child() {
         val stack by context.childStack(initialStack = listOf(Config(1)))
         var isCalled = false
         stack.active.instance.stateKeeper.register(key = "key") {
@@ -250,139 +251,123 @@ class ChildStackIntegrationTest {
 
         navigation.push(Config(2))
 
-        assertFalse(isCalled)
+        assertTrue(isCalled)
     }
 
     @Test
-    fun GIVEN_persistent_WHEN_recreated_THEN_state_restored_for_all_components() {
+    fun GIVEN_persistent_WHEN_recreated_THEN_state_restored_for_active_child() {
         val oldStateKeeper = TestStateKeeperDispatcher()
         val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
-        val oldStack by oldContext.childStack(initialStack = listOf(Config(1), Config(2), Config(3)), persistent = true)
-        oldStack.items.forEach { (configuration, instance) ->
-            instance.stateKeeper.register(key = "key") { Config(configuration.id) }
-        }
+        val oldStack by oldContext.childStack(initialStack = listOf(Config(1)), persistent = true)
+        oldStack.active.instance.stateKeeper.register(key = "key") { Config(10) }
 
         val savedState = oldStateKeeper.save()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
-        val newStack by newContext.childStack()
-        val restoredStates = newStack.items.map { it.instance.stateKeeper.consume<Config>(key = "key") }
+        val newStack by newContext.childStack(initialStack = emptyList())
+        val restoredState = newStack.active.instance.stateKeeper.consume<Config>(key = "key")
 
-        assertContentEquals(listOf(Config(1), Config(2), Config(3)), restoredStates)
+        assertEquals(Config(10), restoredState)
     }
 
     @Test
-    fun GIVEN_persistent_WHEN_recreated_twice_THEN_state_restored_for_all_components() {
+    fun GIVEN_not_persistent_WHEN_recreated_THEN_state_not_restored_for_active_child() {
         val oldStateKeeper = TestStateKeeperDispatcher()
         val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
-        val oldStack by oldContext.childStack(initialStack = listOf(Config(1), Config(2), Config(3)), persistent = true)
-        oldStack.items.forEach { (configuration, instance) ->
-            instance.stateKeeper.register(key = "key") { Config(configuration.id) }
-        }
-
-        val savedState1 = oldStateKeeper.save()
-        val newStateKeeper1 = TestStateKeeperDispatcher(savedState1)
-        val newContext1 = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper1)
-        val newStack1 by newContext1.childStack()
-        newStack1.items.forEach { it.instance.stateKeeper.consume<Config>(key = "key") }
+        val oldStack by oldContext.childStack(initialStack = listOf(Config(1)), persistent = false)
+        oldStack.active.instance.stateKeeper.register(key = "key") { Config(10) }
 
-        val savedState2 = oldStateKeeper.save()
-        val newStateKeeper2 = TestStateKeeperDispatcher(savedState2)
-        val newContext2 = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper2)
-        val newStack2 by newContext2.childStack()
-        val restoredStates = newStack2.items.map { it.instance.stateKeeper.consume<Config>(key = "key") }
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
+        val newStack by newContext.childStack(initialStack = listOf(Config(1)))
+        val restoredState = newStack.active.instance.stateKeeper.consume<Config>(key = "key")
 
-        assertContentEquals(listOf(Config(1), Config(2), Config(3)), restoredStates)
+        assertNull(restoredState)
     }
 
     @Test
-    fun GIVEN_not_persistent_WHEN_recreated_THEN_state_not_restored_for_any_component() {
+    fun GIVEN_back_stack_not_empty_and_recreated_WHEN_pop_THEN_state_restored_for_previous_child() {
         val oldStateKeeper = TestStateKeeperDispatcher()
         val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
-        val oldStack by oldContext.childStack(initialStack = listOf(Config(1), Config(2), Config(3)), persistent = false)
-        oldStack.items.forEach { (configuration, instance) ->
-            instance.stateKeeper.register(key = "key") { Config(configuration.id) }
-        }
-
+        val oldStack by oldContext.childStack(initialStack = listOf(Config(1)))
+        oldStack.active.instance.stateKeeper.register(key = "key") { Config(10) }
+        navigation.push(Config(2))
         val savedState = oldStateKeeper.save()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
-        val newStack by newContext.childStack(initialStack = listOf(Config(1), Config(2), Config(3)))
-        val restoredStates = newStack.items.map { it.instance.stateKeeper.consume<Config>(key = "key") }
+        val newStack by newContext.childStack(initialStack = emptyList())
+
+        navigation.pop()
+        val restoredState = newStack.active.instance.stateKeeper.consume<Config>(key = "key")
 
-        assertContentEquals(listOf(null, null, null), restoredStates)
+        assertEquals(Config(10), restoredState)
     }
 
     @Test
-    fun GIVEN_persistent_WHEN_recreated_THEN_instance_retained_for_all_components() {
+    fun GIVEN_persistent_WHEN_recreated_THEN_instance_retained_for_active_component() {
         val oldStateKeeper = TestStateKeeperDispatcher()
         val instanceKeeper = InstanceKeeperDispatcher()
         val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper, instanceKeeper = instanceKeeper)
-        val oldStack by oldContext.childStack(initialStack = listOf(Config(1), Config(2), Config(3)), persistent = true)
-        val oldInstances = oldStack.items.map { it.instance.instanceKeeper.getOrCreate(::TestInstance) }
+        val oldStack by oldContext.childStack(initialStack = listOf(Config(1)), persistent = true)
+        val oldInstance = oldStack.active.instance.instanceKeeper.getOrCreate(::TestInstance)
 
         val savedState = oldStateKeeper.save()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper)
-        val newStack by newContext.childStack()
-        val retainedInstances = newStack.items.map { it.instance.instanceKeeper.getOrCreate(::TestInstance) }
+        val newStack by newContext.childStack(initialStack = emptyList())
+        val retainedInstance = newStack.active.instance.instanceKeeper.getOrCreate(::TestInstance)
 
-        assertContentEquals(oldInstances, retainedInstances)
+        assertSame(oldInstance, retainedInstance)
     }
 
     @Test
-    fun GIVEN_not_persistent_WHEN_recreated_THEN_instance_not_retained_for_any_component() {
+    fun GIVEN_not_persistent_WHEN_recreated_THEN_instance_not_retained_for_active_component() {
         val oldStateKeeper = TestStateKeeperDispatcher()
         val instanceKeeper = InstanceKeeperDispatcher()
         val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper, instanceKeeper = instanceKeeper)
-        val oldStack by oldContext.childStack(initialStack = listOf(Config(1), Config(2), Config(3)), persistent = false)
-        val oldInstances = oldStack.items.map { it.instance.instanceKeeper.getOrCreate(::TestInstance) }
+        val oldStack by oldContext.childStack(initialStack = listOf(Config(1)), persistent = false)
+        val oldInstance = oldStack.active.instance.instanceKeeper.getOrCreate(::TestInstance)
 
         val savedState = oldStateKeeper.save()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper)
-        val newStack by newContext.childStack(initialStack = listOf(Config(1), Config(2), Config(3)))
-        val retainedInstances = newStack.items.map { it.instance.instanceKeeper.getOrCreate(::TestInstance) }
+        val newStack by newContext.childStack(initialStack = listOf(Config(1)))
+        val newInstance = newStack.active.instance.instanceKeeper.getOrCreate(::TestInstance)
 
-        oldInstances.forEachIndexed { index, instance ->
-            assertNotEquals(instance, retainedInstances[index])
-        }
+        assertNotSame(oldInstance, newInstance)
     }
 
     @Test
-    fun GIVEN_persistent_WHEN_recreated_THEN_instance_not_destroyed_for_any_component() {
+    fun GIVEN_persistent_WHEN_recreated_THEN_instance_not_destroyed_for_active_component() {
         val oldStateKeeper = TestStateKeeperDispatcher()
         val instanceKeeper = InstanceKeeperDispatcher()
         val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper, instanceKeeper = instanceKeeper)
-        val stack by oldContext.childStack(initialStack = listOf(Config(1), Config(2), Config(3)), persistent = true)
-        val instances = stack.items.map { it.instance.instanceKeeper.getOrCreate(::TestInstance) }
+        val stack by oldContext.childStack(initialStack = listOf(Config(1)), persistent = true)
+        val instance = stack.active.instance.instanceKeeper.getOrCreate(::TestInstance)
 
         val savedState = oldStateKeeper.save()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper)
-        newContext.childStack()
+        newContext.childStack(initialStack = emptyList())
 
-        instances.forEach {
-            assertFalse(it.isDestroyed)
-        }
+        assertFalse(instance.isDestroyed)
     }
 
     @Test
-    fun GIVEN_not_persistent_WHEN_recreated_THEN_instances_destroyed_for_all_component() {
+    fun GIVEN_not_persistent_WHEN_recreated_THEN_instance_destroyed_for_active_component() {
         val oldStateKeeper = TestStateKeeperDispatcher()
         val instanceKeeper = InstanceKeeperDispatcher()
         val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper, instanceKeeper = instanceKeeper)
-        val stack by oldContext.childStack(initialStack = listOf(Config(1), Config(2), Config(3)), persistent = false)
-        val instances = stack.items.map { it.instance.instanceKeeper.getOrCreate(::TestInstance) }
+        val stack by oldContext.childStack(initialStack = listOf(Config(1)), persistent = false)
+        val instance = stack.active.instance.instanceKeeper.getOrCreate(::TestInstance)
 
         val savedState = oldStateKeeper.save()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper)
-        newContext.childStack(initialStack = listOf(Config(1), Config(2), Config(3)))
+        newContext.childStack(initialStack = listOf(Config(1)))
 
-        instances.forEach {
-            assertTrue(it.isDestroyed)
-        }
+        assertTrue(instance.isDestroyed)
     }
 
     @Test
@@ -407,13 +392,13 @@ class ChildStackIntegrationTest {
         val savedState = oldStateKeeper.save()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper)
-        newContext.childStack()
+        newContext.childStack(initialStack = emptyList())
 
         assertFalse(instance.isDestroyed)
     }
 
     private fun ComponentContext.childStack(
-        initialStack: List<Config> = emptyList(),
+        initialStack: List<Config>,
         persistent: Boolean = true,
     ): Value<ChildStack<Config, Component>> =
         childStack(
@@ -427,9 +412,6 @@ class ChildStackIntegrationTest {
     private val ChildStack<Config, Component>.children: List<Pair<Int, Int>>
         get() = items.map { child -> child.configuration.id to child.instance.id }
 
-    private fun ChildStack<Config, Component>.getById(id: Int): Child.Created<Config, Component> =
-        items.first { it.configuration.id == id }
-
     private fun ChildStack<Config, Component>.assertStack(vararg children: Pair<Int, Int>) {
         assertEquals(children.toList(), this.children)
         assertEquals(Lifecycle.State.RESUMED, active.instance.lifecycle.state)
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 f619b378..8c71dc11 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
@@ -1,6 +1,5 @@
 package com.arkivanov.decompose.router.stack.webhistory
 
-import com.arkivanov.decompose.ExperimentalDecomposeApi
 import com.arkivanov.decompose.router.stack.TestStackRouter
 import com.arkivanov.decompose.router.stack.navigate
 import com.arkivanov.decompose.router.stack.pop
@@ -10,7 +9,6 @@ import com.arkivanov.essenty.parcelable.Parcelable
 import kotlin.test.Test
 import kotlin.test.assertEquals
 
-@OptIn(ExperimentalDecomposeApi::class)
 @Suppress("TestFunctionName")
 class DefaultWebHistoryControllerTest {
 
diff --git a/decompose/src/jsTest/kotlin/com/arkivanov/decompose/router/stack/webhistory/TestHistory.kt b/decompose/src/jsTest/kotlin/com/arkivanov/decompose/router/stack/webhistory/TestHistory.kt
index c96d30fd..03ad2965 100644
--- a/decompose/src/jsTest/kotlin/com/arkivanov/decompose/router/stack/webhistory/TestHistory.kt
+++ b/decompose/src/jsTest/kotlin/com/arkivanov/decompose/router/stack/webhistory/TestHistory.kt
@@ -1,11 +1,9 @@
 package com.arkivanov.decompose.router.stack.webhistory
 
-import com.arkivanov.decompose.ExperimentalDecomposeApi
 import org.w3c.dom.PopStateEvent
 import org.w3c.dom.PopStateEventInit
 import kotlin.test.assertEquals
 
-@OptIn(ExperimentalDecomposeApi::class)
 class TestHistory(
     private val scheduleOperation: (() -> Unit) -> Unit,
     private val onPopState: (PopStateEvent) -> Unit,
diff --git a/decompose/src/jsTest/kotlin/com/arkivanov/decompose/router/stack/webhistory/TestWindow.kt b/decompose/src/jsTest/kotlin/com/arkivanov/decompose/router/stack/webhistory/TestWindow.kt
index bb891796..7d8a18ff 100644
--- a/decompose/src/jsTest/kotlin/com/arkivanov/decompose/router/stack/webhistory/TestWindow.kt
+++ b/decompose/src/jsTest/kotlin/com/arkivanov/decompose/router/stack/webhistory/TestWindow.kt
@@ -1,9 +1,7 @@
 package com.arkivanov.decompose.router.stack.webhistory
 
-import com.arkivanov.decompose.ExperimentalDecomposeApi
 import org.w3c.dom.PopStateEvent
 
-@OptIn(ExperimentalDecomposeApi::class)
 class TestWindow : DefaultWebHistoryController.Window {
 
     private val pendingOperations = ArrayList<() -> Unit>()