Repository: arkivanov/Decompose
Pull request #128 (2022-06-25 20:36:25)

Added onComplete callback for all navigation extensions


Patch:
diff --git a/decompose/api/android/decompose.api b/decompose/api/android/decompose.api
index d197c47f..dd5eae74 100644
--- a/decompose/api/android/decompose.api
+++ b/decompose/api/android/decompose.api
@@ -76,15 +76,21 @@ public abstract interface class com/arkivanov/decompose/router/Router {
 }
 
 public final class com/arkivanov/decompose/router/RouterExtKt {
-	public static final fun bringToFront (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
+	public static final synthetic fun bringToFront (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
+	public static final fun bringToFront (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
+	public static synthetic fun bringToFront$default (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
 	public static final fun getActiveChild (Lcom/arkivanov/decompose/router/Router;)Lcom/arkivanov/decompose/Child$Created;
 	public static final fun navigate (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
 	public static final fun pop (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
 	public static synthetic fun pop$default (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
 	public static final fun popWhile (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
 	public static final fun popWhile (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
-	public static final fun push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
-	public static final fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
+	public static final synthetic fun push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
+	public static final fun push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
+	public static synthetic fun push$default (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
+	public static final synthetic fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
+	public static final fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
+	public static synthetic fun replaceCurrent$default (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
 }
 
 public final class com/arkivanov/decompose/router/RouterFactoryExtKt {
diff --git a/decompose/api/jvm/decompose.api b/decompose/api/jvm/decompose.api
index 2e2f40a0..53ad1858 100644
--- a/decompose/api/jvm/decompose.api
+++ b/decompose/api/jvm/decompose.api
@@ -63,15 +63,21 @@ public abstract interface class com/arkivanov/decompose/router/Router {
 }
 
 public final class com/arkivanov/decompose/router/RouterExtKt {
-	public static final fun bringToFront (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
+	public static final synthetic fun bringToFront (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
+	public static final fun bringToFront (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
+	public static synthetic fun bringToFront$default (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
 	public static final fun getActiveChild (Lcom/arkivanov/decompose/router/Router;)Lcom/arkivanov/decompose/Child$Created;
 	public static final fun navigate (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
 	public static final fun pop (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
 	public static synthetic fun pop$default (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
 	public static final fun popWhile (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;)V
 	public static final fun popWhile (Lcom/arkivanov/decompose/router/Router;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
-	public static final fun push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
-	public static final fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
+	public static final synthetic fun push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
+	public static final fun push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
+	public static synthetic fun push$default (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
+	public static final synthetic fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
+	public static final fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
+	public static synthetic fun replaceCurrent$default (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
 }
 
 public final class com/arkivanov/decompose/router/RouterFactoryExtKt {
diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/RouterExt.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/RouterExt.kt
index dbab31a7..b15da097 100644
--- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/RouterExt.kt
+++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/RouterExt.kt
@@ -9,11 +9,18 @@ fun <C : Any> Router<C, *>.navigate(transformer: (stack: List<C>) -> List<C>) {
     navigate(transformer = transformer, onComplete = { _, _ -> })
 }
 
+@Deprecated(message = "For binary compatibility", level = DeprecationLevel.HIDDEN)
+fun <C : Any> Router<C, *>.push(configuration: C) {
+    push(configuration = configuration, onComplete = {})
+}
+
 /**
- * Pushes the provided [configuration] at the top of the stack..
+ * Pushes the provided [configuration] at the top of the stack.
+ *
+ * @param onComplete called when the navigation is finished (either synchronously or asynchronously).
  */
-fun <C : Any> Router<C, *>.push(configuration: C) {
-    navigate { it + configuration }
+fun <C : Any> Router<C, *>.push(configuration: C, onComplete: () -> Unit = {}) {
+    navigate(transformer = { it + configuration }, onComplete = { _, _ -> onComplete() })
 }
 
 /**
@@ -26,20 +33,15 @@ fun <C : Any> Router<C, *>.push(configuration: C) {
 fun <C : Any> Router<C, *>.pop(onComplete: (isSuccess: Boolean) -> Unit = {}) {
     navigate(
         transformer = { stack -> stack.takeIf { it.size > 1 }?.dropLast(1) ?: stack },
-        onComplete = { newStack, oldStack -> onComplete(newStack.size < oldStack.size) }
+        onComplete = { newStack, oldStack -> onComplete(newStack.size < oldStack.size) },
     )
 }
 
 /**
- * Drops the configurations at the top of the stack while the [predicate] returns true
+ * Drops the configurations at the top of the stack while the [predicate] returns `true`.
  */
-inline fun <C : Any> Router<C, *>.popWhile(
-    crossinline predicate: (C) -> Boolean
-) {
-    popWhile(
-        predicate = predicate,
-        onComplete = {}
-    )
+inline fun <C : Any> Router<C, *>.popWhile(crossinline predicate: (C) -> Boolean) {
+    popWhile(predicate = predicate, onComplete = {})
 }
 
 /**
@@ -50,29 +52,45 @@ inline fun <C : Any> Router<C, *>.popWhile(
  */
 inline fun <C : Any> Router<C, *>.popWhile(
     crossinline predicate: (C) -> Boolean,
-    crossinline onComplete: (isSuccess: Boolean) -> Unit
+    crossinline onComplete: (isSuccess: Boolean) -> Unit,
 ) {
     navigate(
         transformer = { stack -> stack.dropLastWhile(predicate) },
-        onComplete = { newStack, oldStack -> onComplete(newStack.size < oldStack.size) }
+        onComplete = { newStack, oldStack -> onComplete(newStack.size < oldStack.size) },
     )
 }
 
+@Deprecated(message = "For binary compatibility", level = DeprecationLevel.HIDDEN)
+fun <C : Any> Router<C, *>.replaceCurrent(configuration: C) {
+    replaceCurrent(configuration = configuration, onComplete = {})
+}
+
 /**
  * Replaces the current configuration at the top of the stack with the provided [configuration].
+ *
+ * @param onComplete called when the navigation is finished (either synchronously or asynchronously).
  */
-fun <C : Any> Router<C, *>.replaceCurrent(configuration: C) {
-    navigate { it.dropLast(1) + configuration }
+fun <C : Any> Router<C, *>.replaceCurrent(configuration: C, onComplete: () -> Unit = {}) {
+    navigate(
+        transformer = { it.dropLast(1) + configuration },
+        onComplete = { _, _ -> onComplete() },
+    )
+}
+
+@Deprecated(message = "For binary compatibility", level = DeprecationLevel.HIDDEN)
+fun <C : Any> Router<C, *>.bringToFront(configuration: C) {
+    bringToFront(configuration = configuration, onComplete = {})
 }
 
 /**
  * Removes all components with configurations of [configuration]'s class, and adds the provided [configuration] to the top of the stack.
  * The operation is performed as one transaction. If there is already a component with the same configuration, it will not be recreated.
  */
-fun <C : Any> Router<C, *>.bringToFront(configuration: C) {
-    navigate { stack ->
-        stack.filterNot { it::class == configuration::class } + configuration
-    }
+fun <C : Any> Router<C, *>.bringToFront(configuration: C, onComplete: () -> Unit = {}) {
+    navigate(
+        transformer = { stack -> stack.filterNot { it::class == configuration::class } + configuration },
+        onComplete = { _, _ -> onComplete() },
+    )
 }
 
 val <C : Any, T : Any> Router<C, T>.activeChild: Child.Created<C, T> get() = state.value.activeChild


Test patch:
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/RouterPopWhileTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/RouterPopWhileTest.kt
new file mode 100644
index 00000000..edd55965
--- /dev/null
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/RouterPopWhileTest.kt
@@ -0,0 +1,28 @@
+package com.arkivanov.decompose.router
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+@Suppress("TestFunctionName")
+class RouterPopWhileTest {
+
+    @Test
+    fun WHEN_popWhile_THEN_popped() {
+        val router = TestRouter(listOf(1, 2, 3, 4))
+
+        router.popWhile { it != 2 }
+
+        assertEquals(listOf(1, 2), router.stack)
+    }
+
+    @Test
+    fun WHEN_popWhile_THEN_onComplete_called() {
+        val router = TestRouter(listOf(1, 2, 3, 4))
+        var isCalled = false
+
+        router.popWhile(predicate = { it != 2 }, onComplete = { isCalled = true })
+
+        assertTrue(isCalled)
+    }
+}
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/RouterPushTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/RouterPushTest.kt
new file mode 100644
index 00000000..d29a8a61
--- /dev/null
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/RouterPushTest.kt
@@ -0,0 +1,28 @@
+package com.arkivanov.decompose.router
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+@Suppress("TestFunctionName")
+class RouterPushTest {
+
+    @Test
+    fun WHEN_push_THEN_pushed() {
+        val router = TestRouter(listOf(1, 2))
+
+        router.push(3)
+
+        assertEquals(listOf(1, 2, 3), router.stack)
+    }
+
+    @Test
+    fun WHEN_push_THEN_onComplete_called() {
+        val router = TestRouter(listOf(1, 2))
+        var isCalled = false
+
+        router.push(3) { isCalled = true }
+
+        assertTrue(isCalled)
+    }
+}
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/RouterReplaceCurrentTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/RouterReplaceCurrentTest.kt
new file mode 100644
index 00000000..6008ae9f
--- /dev/null
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/RouterReplaceCurrentTest.kt
@@ -0,0 +1,28 @@
+package com.arkivanov.decompose.router
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+@Suppress("TestFunctionName")
+class RouterReplaceCurrentTest {
+
+    @Test
+    fun WHEN_replaceCurrent_THEN_last_configuration_replaced() {
+        val router = TestRouter(listOf(1, 2, 3))
+
+        router.replaceCurrent(4)
+
+        assertEquals(listOf(1, 2, 4), router.stack)
+    }
+
+    @Test
+    fun WHEN_replaceCurrent_THEN_onComplete_called() {
+        val router = TestRouter(listOf(1, 2, 3))
+        var isCalled = false
+
+        router.replaceCurrent(4) { isCalled = true }
+
+        assertTrue(isCalled)
+    }
+}