Repository: arkivanov/Decompose (kotlin)

Issue:
ChildSlot doesn't preserve the navigation state when there is no active child
When there is no active child in ChildSlot, the navigation state is not preserved and `initialCongfiguration` function is called again after configuration change.

Pull request:
Fixes: #913

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* Bug Fixes
  - Improved reliability of persisted slot restoration, correctly handling absence of configuration without failures.
  - Ensures a slot that starts inactive remains inactive after recreation, preventing unintended activations and improving stability across restarts.
* Tests
  - Added scenario for inactive persisted slot recreation.
  - Updated tests to serialize/deserialize saved state for round-trip verification.
* Chores
  - Internal cleanup of unused imports.
* No public API changes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Patch:
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/slot/ChildSlotFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/slot/ChildSlotFactory.kt
index ec75d4321..2144e9af1 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/slot/ChildSlotFactory.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/slot/ChildSlotFactory.kt
@@ -9,7 +9,6 @@ import com.arkivanov.decompose.router.children.SimpleChildNavState
 import com.arkivanov.decompose.router.children.children
 import com.arkivanov.decompose.value.Value
 import com.arkivanov.essenty.statekeeper.SerializableContainer
-import com.arkivanov.essenty.statekeeper.consumeRequired
 import kotlinx.serialization.KSerializer
 
 /**
@@ -40,7 +39,7 @@ fun <Ctx : GenericComponentContext<Ctx>, C : Any, T : Any> Ctx.childSlot(
     childSlot(
         source = source,
         saveConfiguration = { configuration ->
-            if ((serializer != null) && (configuration != null)) {
+            if (serializer != null) {
                 SerializableContainer(value = configuration, strategy = serializer)
             } else {
                 null
@@ -48,7 +47,7 @@ fun <Ctx : GenericComponentContext<Ctx>, C : Any, T : Any> Ctx.childSlot(
         },
         restoreConfiguration = { container ->
             if (serializer != null) {
-                container.consumeRequired(strategy = serializer)
+                container.consume(strategy = serializer)
             } else {
                 null
             }


Test patch:
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/slot/ChildSlotIntegrationTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/slot/ChildSlotIntegrationTest.kt
index 699d4ba4a..b244ca283 100644
--- a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/slot/ChildSlotIntegrationTest.kt
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/slot/ChildSlotIntegrationTest.kt
@@ -8,6 +8,7 @@ import com.arkivanov.decompose.statekeeper.TestStateKeeperDispatcher
 import com.arkivanov.decompose.testutils.consume
 import com.arkivanov.decompose.testutils.getValue
 import com.arkivanov.decompose.testutils.register
+import com.arkivanov.decompose.testutils.serializeAndDeserialize
 import com.arkivanov.decompose.value.Value
 import com.arkivanov.essenty.backhandler.BackDispatcher
 import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
@@ -158,12 +159,12 @@ class ChildSlotIntegrationTest {
     }
 
     @Test
-    fun GIVEN_persistent_WHEN_recreated_THEN_slot_active() {
+    fun GIVEN_persistent_and_slot_active_WHEN_recreated_THEN_slot_active() {
         val oldStateKeeper = TestStateKeeperDispatcher()
         val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
         oldContext.childSlot(initialConfiguration = 1, persistent = true)
 
-        val savedState = oldStateKeeper.save()
+        val savedState = oldStateKeeper.save().serializeAndDeserialize()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
         val newSlot by newContext.childSlot(initialConfiguration = null, persistent = true)
@@ -171,6 +172,20 @@ class ChildSlotIntegrationTest {
         newSlot.assertChild(1)
     }
 
+    @Test
+    fun GIVEN_persistent_and_slot_not_active_WHEN_recreated_THEN_slot_not_active() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
+        oldContext.childSlot(initialConfiguration = null, persistent = true)
+
+        val savedState = oldStateKeeper.save().serializeAndDeserialize()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
+        val newSlot by newContext.childSlot(initialConfiguration = 1, persistent = true)
+
+        newSlot.assertChild(null)
+    }
+
     @Test
     fun GIVEN_persistent_WHEN_recreated_THEN_slot_state_restored() {
         val oldStateKeeper = TestStateKeeperDispatcher()
@@ -178,7 +193,7 @@ class ChildSlotIntegrationTest {
         val slot by oldContext.childSlot(initialConfiguration = 1, persistent = true)
         slot.requireChild().instance.stateKeeper.register("key") { 10 }
 
-        val savedState = oldStateKeeper.save()
+        val savedState = oldStateKeeper.save().serializeAndDeserialize()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
         val newSlot by newContext.childSlot(initialConfiguration = null, persistent = true)
@@ -193,7 +208,7 @@ class ChildSlotIntegrationTest {
         val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
         oldContext.childSlot(initialConfiguration = 1, persistent = false)
 
-        val savedState = oldStateKeeper.save()
+        val savedState = oldStateKeeper.save().serializeAndDeserialize()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
         val newSlot by newContext.childSlot(initialConfiguration = null, persistent = false)
@@ -216,7 +231,7 @@ class ChildSlotIntegrationTest {
         val oldSlot by oldContext.childSlot(initialConfiguration = 1, persistent = true)
         val oldInstance = oldSlot.requireChild().instance.instanceKeeper.getOrCreate(::TestInstance)
 
-        val savedState = oldStateKeeper.save()
+        val savedState = oldStateKeeper.save().serializeAndDeserialize()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper)
         val newSlot by newContext.childSlot(initialConfiguration = null, persistent = true)
@@ -233,7 +248,7 @@ class ChildSlotIntegrationTest {
         val oldSlot by oldContext.childSlot(initialConfiguration = 1, persistent = true)
         val instance = oldSlot.requireChild().instance.instanceKeeper.getOrCreate(::TestInstance)
 
-        val savedState = oldStateKeeper.save()
+        val savedState = oldStateKeeper.save().serializeAndDeserialize()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper)
         newContext.childSlot(initialConfiguration = null, persistent = true)
@@ -256,7 +271,7 @@ class ChildSlotIntegrationTest {
         val oldSlot by oldContext.childSlot(initialConfiguration = 1, persistent = false)
         val oldInstance = oldSlot.requireChild().instance.instanceKeeper.getOrCreate(::TestInstance)
 
-        val savedState = oldStateKeeper.save()
+        val savedState = oldStateKeeper.save().serializeAndDeserialize()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper)
         val newSlot by newContext.childSlot(initialConfiguration = 1, persistent = false)
@@ -273,7 +288,7 @@ class ChildSlotIntegrationTest {
         val oldSlot by oldContext.childSlot(initialConfiguration = 1, persistent = false)
         val instance = oldSlot.requireChild().instance.instanceKeeper.getOrCreate(::TestInstance)
 
-        val savedState = oldStateKeeper.save()
+        val savedState = oldStateKeeper.save().serializeAndDeserialize()
         val newStateKeeper = TestStateKeeperDispatcher(savedState)
         val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper)
         newContext.childSlot(initialConfiguration = null, persistent = false)