Repository: arkivanov/Decompose
Pull request #293 (2023-01-02 21:36:55)

Added backStackRecreationDepth argument to Child Stack


Patch:
diff --git a/decompose/api/android/decompose.api b/decompose/api/android/decompose.api
index 8f43fca3..bcfafced 100644
--- a/decompose/api/android/decompose.api
+++ b/decompose/api/android/decompose.api
@@ -195,8 +195,8 @@ public final class com/arkivanov/decompose/router/stack/ChildStack {
 }
 
 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 static final fun childStack (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZILkotlin/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;ZILkotlin/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 {
diff --git a/decompose/api/jvm/decompose.api b/decompose/api/jvm/decompose.api
index 5ab08496..99b7ee91 100644
--- a/decompose/api/jvm/decompose.api
+++ b/decompose/api/jvm/decompose.api
@@ -174,8 +174,8 @@ public final class com/arkivanov/decompose/router/stack/ChildStack {
 }
 
 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 static final fun childStack (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/stack/StackNavigationSource;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZILkotlin/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;ZILkotlin/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 {
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
index 1e442d9c..ef0a8c9c 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt
@@ -13,6 +13,7 @@ import com.arkivanov.essenty.parcelable.Parcelable
 import com.arkivanov.essenty.parcelable.ParcelableContainer
 import com.arkivanov.essenty.parcelable.Parcelize
 import com.arkivanov.essenty.parcelable.consumeRequired
+import kotlin.math.max
 import kotlin.reflect.KClass
 
 /**
@@ -25,6 +26,11 @@ import kotlin.reflect.KClass
  * @param key a key of the stack, must be unique if there are multiple stacks in the same component.
  * @param handleBackButton determines whether the overlay should be automatically dismissed
  * on back button press or not, default is `false`.
+ * @param backStackRecreationDepth automatically recreates the specified amount of components
+ * in the back stack after configuration change or process death (e.g. on Android).
+ * Default value is `0`, which means that a component is only recreated when it becomes active.
+ * Use `1` to always recreate the previous component in the stack. Use [Int.MAX_VALUE] to
+ * recreate all components in the stack.
  * @param childFactory a factory function that creates new child instances.
  * @return an observable [Value] of [ChildStack].
  */
@@ -35,6 +41,7 @@ fun <C : Parcelable, T : Any> ComponentContext.childStack(
     configurationClass: KClass<out C>,
     key: String = "DefaultChildStack",
     handleBackButton: Boolean = false,
+    backStackRecreationDepth: Int = 0,
     childFactory: (configuration: C, ComponentContext) -> T,
 ): Value<ChildStack<C, T>> =
     children(
@@ -64,12 +71,14 @@ fun <C : Parcelable, T : Any> ComponentContext.childStack(
 
             StackNavState(
                 active = state.activeConfiguration.consumeRequired(configurationClass),
-                backStack = state.backStackConfigurations.map {
-                    SimpleChildNavState(
-                        configuration = it.consumeRequired(configurationClass),
-                        status = Status.DESTROYED,
-                    )
-                }
+                backStack = state.backStackConfigurations
+                    .map {
+                        SimpleChildNavState(
+                            configuration = it.consumeRequired(configurationClass),
+                            status = Status.DESTROYED,
+                        )
+                    }
+                    .withRecreationDepth(recreationDepth = backStackRecreationDepth)
             )
         },
         navTransformer = { navState, event ->
@@ -92,7 +101,8 @@ fun <C : Parcelable, T : Any> ComponentContext.childStack(
                                 null -> Status.DESTROYED
                             },
                         )
-                    },
+                    }
+                    .withRecreationDepth(recreationDepth = backStackRecreationDepth),
             )
         },
         onEventComplete = { event, newNavState, oldNavState ->
@@ -103,7 +113,7 @@ fun <C : Parcelable, T : Any> ComponentContext.childStack(
                 {
                     StackNavState(
                         active = navState.backStack.last().configuration,
-                        backStack = navState.backStack.dropLast(1),
+                        backStack = navState.backStack.dropLast(1).withRecreationDepth(recreationDepth = backStackRecreationDepth),
                     )
                 }
             } else {
@@ -119,6 +129,31 @@ fun <C : Parcelable, T : Any> ComponentContext.childStack(
         childFactory = childFactory,
     )
 
+@ExperimentalDecomposeApi
+private fun <C : Any> List<SimpleChildNavState<C>>.withRecreationDepth(recreationDepth: Int): List<SimpleChildNavState<C>> =
+    if (isRecreationRequired(recreationDepth = recreationDepth)) {
+        mapIndexed { index, item ->
+            when (item.status) {
+                Status.DESTROYED -> if (lastIndex - index < recreationDepth) item.copy(status = Status.INACTIVE) else item
+                Status.INACTIVE,
+                Status.ACTIVE -> item
+            }
+        }
+    } else {
+        this
+    }
+
+@ExperimentalDecomposeApi
+private fun List<SimpleChildNavState<*>>.isRecreationRequired(recreationDepth: Int): Boolean {
+    for (i in lastIndex downTo max(lastIndex - recreationDepth + 1, 0)) {
+        if (get(i).status == Status.DESTROYED) {
+            return true
+        }
+    }
+
+    return false
+}
+
 @OptIn(ExperimentalDecomposeApi::class)
 private data class StackNavState<out C : Any>(
     val active: C,
@@ -152,6 +187,7 @@ inline fun <reified C : Parcelable, T : Any> ComponentContext.childStack(
     noinline initialStack: () -> List<C>,
     key: String = "DefaultChildStack",
     handleBackButton: Boolean = false,
+    backStackRecreationDepth: Int = 0,
     noinline childFactory: (configuration: C, ComponentContext) -> T
 ): Value<ChildStack<C, T>> =
     childStack(
@@ -160,6 +196,7 @@ inline fun <reified C : Parcelable, T : Any> ComponentContext.childStack(
         configurationClass = C::class,
         key = key,
         handleBackButton = handleBackButton,
+        backStackRecreationDepth = backStackRecreationDepth,
         childFactory = childFactory,
     )
 
@@ -171,6 +208,7 @@ inline fun <reified C : Parcelable, T : Any> ComponentContext.childStack(
     initialConfiguration: C,
     key: String = "DefaultChildStack",
     handleBackButton: Boolean = false,
+    backStackRecreationDepth: Int = 0,
     noinline childFactory: (configuration: C, ComponentContext) -> T
 ): Value<ChildStack<C, T>> =
     childStack(
@@ -179,5 +217,6 @@ inline fun <reified C : Parcelable, T : Any> ComponentContext.childStack(
         configurationClass = C::class,
         key = key,
         handleBackButton = handleBackButton,
+        backStackRecreationDepth = backStackRecreationDepth,
         childFactory = childFactory,
     )
diff --git a/docs/navigation/stack/overview.md b/docs/navigation/stack/overview.md
index ef7bb353..20cac8a5 100644
--- a/docs/navigation/stack/overview.md
+++ b/docs/navigation/stack/overview.md
@@ -132,6 +132,12 @@ class DefaultRootComponent(
 }
 ```
 
+## Configuration changes and process death on (not only) Android
+
+`Child Stack` automatically preserves the stack when a configuration change or process death occurs. By default, only the active component is recreated, components in the back stack remain destroyed and are recreated on demand.
+
+Use the `backStackRecreationDepth` argument to control how many components in the back stack are automatically recreated. E.g. use `1` to always recreate the last component in the back stack (in addition to the active component which is always recreated), use `Int.MAX_VALUE` to recreate all components in the back stack. Default value is `0`, which means that a component is only recreated when it becomes active. The automatic recreation of components in the back stack may be useful with swipe-back navigation, e.g. in SwiftUI.
+
 ## Delivering a result when navigating back
 
 To deliver a result from one component to another:
diff --git a/sample/app-ios/app-ios.xcodeproj/project.pbxproj b/sample/app-ios/app-ios.xcodeproj/project.pbxproj
index 4518d3da..eb37b178 100644
--- a/sample/app-ios/app-ios.xcodeproj/project.pbxproj
+++ b/sample/app-ios/app-ios.xcodeproj/project.pbxproj
@@ -20,8 +20,7 @@
 		798DB856282F0E6F0072CCBD /* MultiPaneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798DB855282F0E6F0072CCBD /* MultiPaneView.swift */; };
 		798DB858282F0EDF0072CCBD /* ArticleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798DB857282F0EDF0072CCBD /* ArticleListView.swift */; };
 		798DB85A282F11800072CCBD /* ArticleDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798DB859282F11800072CCBD /* ArticleDetailsView.swift */; };
-		B308A54CDAB6A3939DA5F4E3 /* StackComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B308A27F7C6962C7F515D7F0 /* StackComponent.swift */; };
-		B308A81878CCE92C321B927F /* CountersStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B308A300355A5948E5DC6CB8 /* CountersStackView.swift */; };
+		B308A81878CCE92C321B927F /* StackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B308A300355A5948E5DC6CB8 /* StackView.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXFileReference section */
@@ -39,8 +38,7 @@
 		798DB855282F0E6F0072CCBD /* MultiPaneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiPaneView.swift; sourceTree = "<group>"; };
 		798DB857282F0EDF0072CCBD /* ArticleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleListView.swift; sourceTree = "<group>"; };
 		798DB859282F11800072CCBD /* ArticleDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleDetailsView.swift; sourceTree = "<group>"; };
-		B308A27F7C6962C7F515D7F0 /* StackComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackComponent.swift; sourceTree = "<group>"; };
-		B308A300355A5948E5DC6CB8 /* CountersStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountersStackView.swift; sourceTree = "<group>"; };
+		B308A300355A5948E5DC6CB8 /* StackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackView.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -86,8 +84,7 @@
 				798DB855282F0E6F0072CCBD /* MultiPaneView.swift */,
 				798DB857282F0EDF0072CCBD /* ArticleListView.swift */,
 				798DB859282F11800072CCBD /* ArticleDetailsView.swift */,
-				B308A300355A5948E5DC6CB8 /* CountersStackView.swift */,
-				B308A27F7C6962C7F515D7F0 /* StackComponent.swift */,
+				B308A300355A5948E5DC6CB8 /* StackView.swift */,
 			);
 			path = "app-ios";
 			sourceTree = "<group>";
@@ -202,8 +199,7 @@
 				798DB852282F096C0072CCBD /* CountersView.swift in Sources */,
 				798DB85A282F11800072CCBD /* ArticleDetailsView.swift in Sources */,
 				798DB84A282F08670072CCBD /* CounterView.swift in Sources */,
-				B308A81878CCE92C321B927F /* CountersStackView.swift in Sources */,
-				B308A54CDAB6A3939DA5F4E3 /* StackComponent.swift in Sources */,
+				B308A81878CCE92C321B927F /* StackView.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
diff --git a/sample/app-ios/app-ios/CountersView.swift b/sample/app-ios/app-ios/CountersView.swift
index 68138579..3acb18dc 100644
--- a/sample/app-ios/app-ios/CountersView.swift
+++ b/sample/app-ios/app-ios/CountersView.swift
@@ -9,35 +9,22 @@ import SwiftUI
 import Shared
 
 struct CountersView: View {
-    private let counters: CountersComponent
-
     @ObservedObject
     private var childStack: ObservableValue<ChildStack<AnyObject, CounterComponent>>
-
-    private var components: [CounterComponent] { childStack.value.items.filter { $0.instance != nil }.map { $0.instance! } }
-    private var componentWrappers: [StackComponent<CounterComponent>] { components.map { StackComponent($0) } }
-
-    private var activeChild: CounterComponent { childStack.value.active.instance }
-
+    
+    private var stack: ChildStack<AnyObject, CounterComponent> { childStack.value }
+    
     init(_ counters: CountersComponent) {
-        self.counters = counters
         childStack = ObservableValue(counters.childStack)
     }
-
+    
     var body: some View {
-        if #available(iOS 16, *) {
-            let pathBinding = Binding(get: { componentWrappers.dropFirst() }, set: { _ in activeChild.onPrevClicked() })
-            NavigationStack(path: pathBinding) {
-                CounterView(components[0])
-                        .navigationDestination(for: StackComponent<CounterComponent>.self) {
-                            CounterView($0.component)
-                        }
-            }
-        } else {
-            CountersStackView(components: components) {
-                CounterView($0)
-            }
-        }
+        StackView(
+            stack: stack.items,
+            getTitle: { $0.model.value.title },
+            onBack: stack.active.instance.onPrevClicked,
+            childContent: CounterView.init
+        )
     }
 }
 
@@ -49,5 +36,5 @@ struct CountersView_Previews: PreviewProvider {
 
 class PreviewCountersComponent: CountersComponent {
     let childStack: Value<ChildStack<AnyObject, CounterComponent>> =
-            simpleChildStack(PreviewCounterComponent())
+    simpleChildStack(PreviewCounterComponent())
 }
diff --git a/sample/app-ios/app-ios/StackComponent.swift b/sample/app-ios/app-ios/StackComponent.swift
deleted file mode 100644
index 9edd8ac1..00000000
--- a/sample/app-ios/app-ios/StackComponent.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-import Foundation
-
-struct StackComponent<C>: Hashable {
-    let component: C
-
-    init(_ component: C) {
-        self.component = component
-    }
-
-    static func == (lhs: StackComponent<C>, rhs: StackComponent<C>) -> Bool {
-        (lhs.component as! NSObject).isEqual(rhs.component)
-    }
-
-    func hash(into hasher: inout Hasher) {
-        hasher.combine((component as! NSObject).hash)
-    }
-}
diff --git a/sample/app-ios/app-ios/CountersStackView.swift b/sample/app-ios/app-ios/StackView.swift
similarity index 51%
rename from sample/app-ios/app-ios/CountersStackView.swift
rename to sample/app-ios/app-ios/StackView.swift
index 47822fbe..37bd2cb8 100644
--- a/sample/app-ios/app-ios/CountersStackView.swift
+++ b/sample/app-ios/app-ios/StackView.swift
@@ -2,48 +2,78 @@ import SwiftUI
 import UIKit
 import Shared
 
-struct CountersStackView<Content: View>: UIViewControllerRepresentable {
-    var components: [CounterComponent]
-    var renderBody: (CounterComponent) -> Content
+struct StackView<T: AnyObject, Content: View>: View {
+    var stack: [Child<AnyObject, T>]
+    var getTitle: (T) -> String
+    var onBack: () -> Void
+    
+    @ViewBuilder
+    var childContent: (T) -> Content
+    
+    var body: some View {
+        if #available(iOS 16, *) {
+            NavigationStack(path: Binding(get: { stack.dropFirst() }, set: { _ in onBack() })) {
+                childContent(stack.first!.instance!)
+                    .navigationDestination(for: Child<AnyObject, T>.self) {
+                        childContent($0.instance!)
+                    }
+            }
+        } else {
+            StackInteropView(
+                components: stack.map { $0.instance! },
+                getTitle: getTitle,
+                onBack: onBack,
+                childContent: childContent
+            )
+        }
+    }
+}
 
+private struct StackInteropView<T: AnyObject, Content: View>: UIViewControllerRepresentable {
+    var components: [T]
+    var getTitle: (T) -> String
+    var onBack: () -> Void
+    var childContent: (T) -> Content
+    
     func makeCoordinator() -> Coordinator {
         Coordinator(self)
     }
-
+    
     func makeUIViewController(context: Context) -> UINavigationController {
         context.coordinator.syncChanges(self)
         let navigationController = UINavigationController(
-                rootViewController: context.coordinator.viewControllers.first!)
-
+            rootViewController: context.coordinator.viewControllers.first!)
+        
         return navigationController
     }
-
+    
     func updateUIViewController(_ navigationController: UINavigationController, context: Context) {
         context.coordinator.syncChanges(self)
         navigationController.setViewControllers(context.coordinator.viewControllers, animated: true)
     }
-
-    private func createViewController(_ component: CounterComponent, _ coordinator: Coordinator) -> NavigationItemHostingController<Content> {
-        let controller = NavigationItemHostingController(rootView: renderBody(component))
+    
+    private func createViewController(_ component: T, _ coordinator: Coordinator) -> NavigationItemHostingController {
+        let controller = NavigationItemHostingController(rootView: childContent(component))
         controller.coordinator = coordinator
         controller.component = component
-        controller.navigationItem.title = component.model.value.title
+        controller.onBack = onBack
+        controller.navigationItem.title = getTitle(component)
         return controller
     }
-
+    
     class Coordinator: NSObject {
-        var parent: CountersStackView<Content>
-        var viewControllers = [NavigationItemHostingController<Content>]()
-        var preservedComponents = [CounterComponent]()
-
-        init(_ parent: CountersStackView<Content>) {
+        var parent: StackInteropView<T, Content>
+        var viewControllers = [NavigationItemHostingController]()
+        var preservedComponents = [T]()
+        
+        init(_ parent: StackInteropView<T, Content>) {
             self.parent = parent
         }
-
-        func syncChanges(_ parent: CountersStackView<Content>) {
+        
+        func syncChanges(_ parent: StackInteropView<T, Content>) {
             self.parent = parent
             let count = max(preservedComponents.count, parent.components.count)
-
+            
             for i in 0..<count {
                 if (i >= parent.components.count) {
                     viewControllers.removeLast()
@@ -53,20 +83,21 @@ struct CountersStackView<Content: View>: UIViewControllerRepresentable {
                     viewControllers[i] = parent.createViewController(parent.components[i], self)
                 }
             }
-
+            
             preservedComponents = parent.components
         }
     }
-
-    class NavigationItemHostingController<Content: View>: UIHostingController<Content> {
+    
+    class NavigationItemHostingController: UIHostingController<Content> {
         fileprivate(set) weak var coordinator: Coordinator?
-        fileprivate(set) weak var component: CounterComponent?
-
+        fileprivate(set) var component: T?
+        fileprivate(set) var onBack: (() -> Void)?
+        
         override func viewDidDisappear(_ animated: Bool) {
             super.viewDidDisappear(animated)
-
+            
             if isMovingFromParent && coordinator?.preservedComponents.last === component {
-                component?.onPrevClicked()
+                onBack?()
             }
         }
     }
@@ -74,7 +105,7 @@ struct CountersStackView<Content: View>: UIViewControllerRepresentable {
 
 // stubs for XCode < 14:
 #if compiler(<5.7)
-struct NavigationStack<Path, Root>: View {
+private struct NavigationStack<Path, Root>: View {
     var path: Path
     @ViewBuilder var root: () -> Root
     var body: some View {
@@ -82,7 +113,7 @@ struct NavigationStack<Path, Root>: View {
     }
 }
 
-extension View {
+private extension View {
     public func navigationDestination<D, C>(for data: D.Type, @ViewBuilder destination: @escaping (D) -> C) -> some View where D: Hashable, C: View {
         EmptyView()
     }
diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/DefaultCountersComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/DefaultCountersComponent.kt
index b26a161b..bbaf8223 100644
--- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/DefaultCountersComponent.kt
+++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/DefaultCountersComponent.kt
@@ -18,6 +18,7 @@ internal class DefaultCountersComponent(
         childStack(
             source = navigation,
             initialConfiguration = Config(index = 0, isBackEnabled = false),
+            backStackRecreationDepth = Int.MAX_VALUE, // Useful for swipe navigation in SwiftUI after process death
             childFactory = ::child,
         )
 


Test patch:
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 0b30a53c..ff64f3b8 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
@@ -331,11 +331,127 @@ class ChildStackIntegrationTest {
         assertTrue(instance.isDestroyed)
     }
 
-    private fun ComponentContext.childStack(initialStack: List<Config>): Value<ChildStack<Config, Component>> =
+    @Test
+    fun GIVEN_back_stack_not_empty_WHEN_recreated_THEN_only_active_child_recreated() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
+        oldContext.childStack(initialStack = listOf(Config(1), Config(2)))
+
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
+        val newStack by newContext.childStack(initialStack = emptyList())
+
+        newStack.assertStack(1 to null, 2 to 2)
+    }
+
+    @Test
+    fun GIVEN_back_stack_not_empty_and_backStackRecreationDepth_is_1_WHEN_recreated_THEN_last_to_children_recreated() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
+        oldContext.childStack(initialStack = listOf(Config(1), Config(2), Config(3)))
+
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
+        val newStack by newContext.childStack(initialStack = emptyList(), backStackRecreationDepth = 1)
+
+        newStack.assertStack(1 to null, 2 to 2, 3 to 3)
+    }
+
+    @Test
+    fun GIVEN_back_stack_not_empty_and_backStackRecreationDepth_is_max_WHEN_recreated_THEN_all_children_recreated() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
+        oldContext.childStack(initialStack = listOf(Config(1), Config(2), Config(3)))
+
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
+        val newStack by newContext.childStack(initialStack = emptyList(), backStackRecreationDepth = Int.MAX_VALUE)
+
+        newStack.assertStack(1 to 1, 2 to 2, 3 to 3)
+    }
+
+    @Test
+    fun GIVEN_back_stack_not_empty_and_backStackRecreationDepth_is_1_and_recreated_WHEN_pop_THEN_last_two_children_recreated() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
+        oldContext.childStack(initialStack = listOf(Config(1), Config(2), Config(3), Config(4)))
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper)
+        val newStack by newContext.childStack(initialStack = emptyList(), backStackRecreationDepth = 1)
+
+        navigation.pop()
+
+        newStack.assertStack(1 to null, 2 to 2, 3 to 3)
+    }
+
+    @Test
+    fun GIVEN_back_stack_not_empty_and_backStackRecreationDepth_is_1_and_recreated_WHEN_back_pressed_THEN_last_two_children_recreated() {
+        val oldStateKeeper = TestStateKeeperDispatcher()
+        val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper)
+        oldContext.childStack(initialStack = listOf(Config(1), Config(2), Config(3), Config(4)))
+        val savedState = oldStateKeeper.save()
+        val newStateKeeper = TestStateKeeperDispatcher(savedState)
+        val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, backHandler = backDispatcher)
+        val newStack by newContext.childStack(initialStack = emptyList(), backStackRecreationDepth = 1)
+
+        backDispatcher.back()
+
+        newStack.assertStack(1 to null, 2 to 2, 3 to 3)
+    }
+
+    @Test
+    fun GIVEN_one_child_in_back_stack_and_backStackRecreationDepth_is_1_WHEN_pop_THEN_child_popped() {
+        val context = DefaultComponentContext(lifecycle = lifecycle)
+        val stack by context.childStack(initialStack = listOf(Config(1), Config(2)), backStackRecreationDepth = 1)
+
+        navigation.pop()
+
+        stack.assertStack(1 to 1)
+    }
+
+    @Test
+    fun GIVEN_one_child_in_back_stack_and_backStackRecreationDepth_is_2_WHEN_pop_THEN_child_popped() {
+        val context = DefaultComponentContext(lifecycle = lifecycle)
+        val stack by context.childStack(initialStack = listOf(Config(1), Config(2)), backStackRecreationDepth = 2)
+
+        navigation.pop()
+
+        stack.assertStack(1 to 1)
+    }
+
+    @Test
+    fun GIVEN_two_children_in_back_stack_and_backStackRecreationDepth_is_1_WHEN_pop_THEN_child_popped() {
+        val context = DefaultComponentContext(lifecycle = lifecycle)
+        val stack by context.childStack(initialStack = listOf(Config(1), Config(2), Config(3)), backStackRecreationDepth = 1)
+
+        navigation.pop()
+
+        stack.assertStack(1 to 1, 2 to 2)
+    }
+
+    @Test
+    fun GIVEN_two_children_in_back_stack_and_backStackRecreationDepth_is_2_WHEN_pop_THEN_child_popped() {
+        val context = DefaultComponentContext(lifecycle = lifecycle)
+        val stack by context.childStack(initialStack = listOf(Config(1), Config(2), Config(3)), backStackRecreationDepth = 2)
+
+        navigation.pop()
+
+        stack.assertStack(1 to 1, 2 to 2)
+    }
+
+    private fun ComponentContext.childStack(
+        initialStack: List<Config>,
+        backStackRecreationDepth: Int = 0,
+    ): Value<ChildStack<Config, Component>> =
         childStack(
             source = navigation,
             initialStack = { initialStack },
             handleBackButton = true,
+            backStackRecreationDepth = backStackRecreationDepth,
             childFactory = ::Component,
         )