Repository: arkivanov/Decompose
Pull request #14 (2022-01-11 23:48:54)

Added router builders with initial stack, deprecated old builders

arkivanov: @andkulikov Fixed comments, could you please check?

Patch:
diff --git a/README.md b/README.md
index cfef3b27..f5d9e49a 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ There should be no breaking changes related to this transfer. Most of the extern
 Here is what is mostly affected by the transfer:
 
 - All the stars were transferred
-- Search in the code will not work **as long as this repository has less stars than the parent** (your help is needed!)
+- Search in the code will not work **unless this repository has less stars than the parent** (your help is needed!)
 - All the Issues and Discussions were transferred as well. I will do all my best to fill the gap here.
 - All pull requests with all the comment history are also gone.
 
diff --git a/decompose/api/android/decompose.api b/decompose/api/android/decompose.api
index 2afa0a61..55b8ff49 100644
--- a/decompose/api/android/decompose.api
+++ b/decompose/api/android/decompose.api
@@ -88,7 +88,9 @@ public final class com/arkivanov/decompose/router/RouterExtKt {
 
 public final class com/arkivanov/decompose/router/RouterFactoryExtKt {
 	public static final fun router (Lcom/arkivanov/decompose/ComponentContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/router/Router;
+	public static final fun router (Lcom/arkivanov/decompose/ComponentContext;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/router/Router;
 	public static synthetic fun router$default (Lcom/arkivanov/decompose/ComponentContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/router/Router;
+	public static synthetic fun router$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/Router;
 }
 
 public final class com/arkivanov/decompose/router/RouterState {
diff --git a/decompose/api/jvm/decompose.api b/decompose/api/jvm/decompose.api
index 79bc8b6f..0dda24cb 100644
--- a/decompose/api/jvm/decompose.api
+++ b/decompose/api/jvm/decompose.api
@@ -75,7 +75,9 @@ public final class com/arkivanov/decompose/router/RouterExtKt {
 
 public final class com/arkivanov/decompose/router/RouterFactoryExtKt {
 	public static final fun router (Lcom/arkivanov/decompose/ComponentContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/router/Router;
+	public static final fun router (Lcom/arkivanov/decompose/ComponentContext;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/router/Router;
 	public static synthetic fun router$default (Lcom/arkivanov/decompose/ComponentContext;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/reflect/KClass;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/router/Router;
+	public static synthetic fun router$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/Router;
 }
 
 public final class com/arkivanov/decompose/router/RouterState {
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/RouterFactoryExt.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/RouterFactoryExt.kt
index 748349b6..a844fbf3 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/RouterFactoryExt.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/RouterFactoryExt.kt
@@ -8,8 +8,7 @@ import kotlin.reflect.KClass
 /**
  * Creates a new [Router].
  *
- * @param initialConfiguration a configuration of a component that should be displayed if there is no saved state
- * @param initialBackStack a stack of component configurations that should be set as back stack if there is no saved state
+ * @param initialStack a stack of component configurations 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 [Router], should be unique if there are multiple [Router]s in the same component
  * @param handleBackButton determines whether the [Router] should handle back button clicks or not
@@ -17,8 +16,7 @@ import kotlin.reflect.KClass
  * @return a new instance of [Router]
  */
 fun <C : Parcelable, T : Any> ComponentContext.router(
-    initialConfiguration: () -> C,
-    initialBackStack: () -> List<C> = ::emptyList,
+    initialStack: () -> List<C>,
     configurationClass: KClass<out C>,
     key: String = "DefaultRouter",
     handleBackButton: Boolean = false,
@@ -31,8 +29,7 @@ fun <C : Parcelable, T : Any> ComponentContext.router(
         backPressedHandler = backPressedHandler,
         popOnBackPressed = handleBackButton,
         stackHolder = StackHolderImpl(
-            initialConfiguration = initialConfiguration,
-            initialBackStack = initialBackStack,
+            initialStack = initialStack,
             lifecycle = lifecycle,
             key = key,
             stackSaver = StackSaverImpl(
@@ -50,6 +47,63 @@ fun <C : Parcelable, T : Any> ComponentContext.router(
 /**
  * A convenience extension function for [ComponentContext.router].
  */
+inline fun <reified C : Parcelable, T : Any> ComponentContext.router(
+    noinline initialStack: () -> List<C>,
+    key: String = "DefaultRouter",
+    handleBackButton: Boolean = false,
+    noinline childFactory: (configuration: C, ComponentContext) -> T
+): Router<C, T> =
+    router(
+        initialStack = initialStack,
+        configurationClass = C::class,
+        key = key,
+        handleBackButton = handleBackButton,
+        childFactory = childFactory
+    )
+
+/**
+ * A convenience extension function for [ComponentContext.router].
+ */
+inline fun <reified C : Parcelable, T : Any> ComponentContext.router(
+    initialConfiguration: C,
+    key: String = "DefaultRouter",
+    handleBackButton: Boolean = false,
+    noinline childFactory: (configuration: C, ComponentContext) -> T
+): Router<C, T> =
+    router(
+        initialStack = { listOf(initialConfiguration) },
+        configurationClass = C::class,
+        key = key,
+        handleBackButton = handleBackButton,
+        childFactory = childFactory
+    )
+
+/**
+ * A convenience extension function for [ComponentContext.router].
+ */
+@Suppress("DeprecatedCallableAddReplaceWith")
+@Deprecated(message = "Please use ComponentContext.router extension function with initialStack argument")
+fun <C : Parcelable, T : Any> ComponentContext.router(
+    initialConfiguration: () -> C,
+    initialBackStack: () -> List<C> = ::emptyList,
+    configurationClass: KClass<out C>,
+    key: String = "DefaultRouter",
+    handleBackButton: Boolean = false,
+    childFactory: (configuration: C, ComponentContext) -> T
+): Router<C, T> =
+    router(
+        initialStack = { initialBackStack() + initialConfiguration() },
+        configurationClass = configurationClass,
+        key = key,
+        handleBackButton = handleBackButton,
+        childFactory = childFactory,
+    )
+
+/**
+ * A convenience extension function for [ComponentContext.router].
+ */
+@Suppress("DeprecatedCallableAddReplaceWith")
+@Deprecated(message = "Please use ComponentContext.router extension function with initialStack argument")
 inline fun <reified C : Parcelable, T : Any> ComponentContext.router(
     initialConfiguration: C,
     initialBackStack: List<C> = emptyList(),
@@ -69,6 +123,8 @@ inline fun <reified C : Parcelable, T : Any> ComponentContext.router(
 /**
  * A convenience extension function for [ComponentContext.router].
  */
+@Suppress("DeprecatedCallableAddReplaceWith")
+@Deprecated(message = "Please use ComponentContext.router extension function with initialStack argument")
 inline fun <reified C : Parcelable, T : Any> ComponentContext.router(
     noinline initialConfiguration: () -> C,
     noinline initialBackStack: () -> List<C> = ::emptyList,
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/StackHolderImpl.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/StackHolderImpl.kt
index 5c38634c..d63dd06a 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/StackHolderImpl.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/StackHolderImpl.kt
@@ -1,5 +1,6 @@
 package com.arkivanov.decompose.router
 
+import com.arkivanov.decompose.isUnique
 import com.arkivanov.essenty.instancekeeper.InstanceKeeper
 import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
 import com.arkivanov.essenty.instancekeeper.getOrCreate
@@ -10,8 +11,7 @@ import com.arkivanov.essenty.lifecycle.resume
 import kotlin.properties.Delegates.observable
 
 internal class StackHolderImpl<C : Any, T : Any>(
-    private val initialConfiguration: () -> C,
-    private val initialBackStack: () -> List<C>,
+    private val initialStack: () -> List<C>,
     lifecycle: Lifecycle,
     private val key: String,
     private val stackSaver: StackSaver<C>,
@@ -22,7 +22,7 @@ internal class StackHolderImpl<C : Any, T : Any>(
     private val retainedInstance: RetainedInstance<C, T> = instanceKeeper.getOrCreate(key, ::RetainedInstance)
 
     override var stack: RouterStack<C, T>
-        by observable(restoreStack() ?: initialStack()) { _, _, newValue ->
+        by observable(restoreStack() ?: createInitialStack()) { _, _, newValue ->
             retainedInstance.activeEntry = newValue.active
         }
 
@@ -42,11 +42,17 @@ internal class StackHolderImpl<C : Any, T : Any>(
         stack.backStack.destroy()
     }
 
-    private fun initialStack(): RouterStack<C, T> =
-        RouterStack(
-            active = routerEntryFactory(initialConfiguration()),
-            backStack = initialBackStack().map { RouterEntry.Destroyed(configuration = it) }
+    private fun createInitialStack(): RouterStack<C, T> {
+        val initialStack = initialStack()
+
+        check(initialStack.isNotEmpty()) { "Initial stack can not be empty" }
+        check(initialStack.isUnique()) { "Configurations in the initial stack must be unique" }
+
+        return RouterStack(
+            active = routerEntryFactory(initialStack.last()),
+            backStack = initialStack.dropLast(1).map { RouterEntry.Destroyed(configuration = it) }
         )
+    }
 
     private fun restoreStack(): RouterStack<C, T>? {
         val savedStack: StackSaver.RestoredStack<C>? = stackSaver.restore(key)
diff --git a/docs/component/custom-component-context.md b/docs/component/custom-component-context.md
index ed2a369f..c8766e9e 100644
--- a/docs/component/custom-component-context.md
+++ b/docs/component/custom-component-context.md
@@ -30,16 +30,14 @@ create the `Router` and provide child `AppComponentContext`.
 
 ```kotlin
 fun <C : Parcelable, T : Any> AppComponentContext.appRouter(
-    initialConfiguration: () -> C,
-    initialBackStack: () -> List<C> = ::emptyList,
+    initialStack: () -> List<C>,
     configurationClass: KClass<out C>,
     key: String = "DefaultRouter",
     handleBackButton: Boolean = false,
     childFactory: (configuration: C, AppComponentContext) -> T
 ): Router<C, T> =
     router(
-        initialConfiguration = initialConfiguration,
-        initialBackStack = initialBackStack,
+        initialStack = initialStack,
         configurationClass = configurationClass,
         key = key,
         handleBackButton = handleBackButton
@@ -59,7 +57,7 @@ Finally, in your components you can create a new router that will utilize the ne
 class MyComponent(componentContext: AppComponentContext): AppComponentContext by componentContext {
 
     private val router = appRouter(
-        initialConfiguration = { Configuration.Home },
+        initialStack = { listOf(Configuration.Home) },
         childFactory = { configuration, appComponentContext ->
             // return child components using the custom component context
         }
diff --git a/docs/router/deeplinking.md b/docs/router/deeplinking.md
index 4955d64a..14012e01 100644
--- a/docs/router/deeplinking.md
+++ b/docs/router/deeplinking.md
@@ -22,8 +22,12 @@ class RootComponent(
 
     private val router =
         router<Config, Root.Child>(
-            initialConfiguration = if (initialItemId == null) Config.List else Config.Details(itemId = initialItemId),
-            initialBackStack = if (initialItemId == null) emptyList() else listOf(Config.List),
+            initialStack = {
+                listOfNotNull(
+                    Config.List,
+                    if (initialItemId != null) Config.Details(itemId = initialItemId) else null,
+                )
+            },
             handleBackButton = true,
             childFactory = ::createChild
         )
@@ -32,5 +36,5 @@ class RootComponent(
 }
 ```
 
-Now, if the `initialItemId` is supplied, the first screen will be the `ItemDetails` component. The `ItemList` component will be in the back
-stack, so the user will be able to go back.
+Now, if the `initialItemId` is supplied, the initial screen will be the `ItemDetails` component. The `ItemList` component will be in the
+back stack, so the user will be able to go back.


Test patch:
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/StackHolderImplTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/StackHolderImplTest.kt
index c1d0a17a..771daa2c 100644
--- a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/StackHolderImplTest.kt
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/StackHolderImplTest.kt
@@ -123,8 +123,7 @@ class StackHolderImplTest {
         routerEntryFactory: RouterEntryFactory<Config, Component> = TestRouterEntryFactory
     ): StackHolderImpl<Config, Component> =
         StackHolderImpl(
-            initialConfiguration = { initialConfiguration },
-            initialBackStack = ::emptyList,
+            initialStack = { listOf(initialConfiguration) },
             lifecycle = LifecycleRegistry(),
             key = "key",
             stackSaver = stackSaver,