Skip to content

Commit

Permalink
Added onComplete callback to Router.navigate
Browse files Browse the repository at this point in the history
  • Loading branch information
arkivanov committed Apr 8, 2022
1 parent cd56772 commit 309fb39
Show file tree
Hide file tree
Showing 16 changed files with 362 additions and 227 deletions.
6 changes: 4 additions & 2 deletions decompose/api/android/decompose.api
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,15 @@ public final class com/arkivanov/decompose/lifecycle/MergedLifecycle : com/arkiv

public abstract interface class com/arkivanov/decompose/router/Router {
public abstract fun getState ()Lcom/arkivanov/decompose/value/Value;
public abstract fun navigate (Lkotlin/jvm/functions/Function1;)V
public abstract fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
}

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 fun getActiveChild (Lcom/arkivanov/decompose/router/Router;)Lcom/arkivanov/decompose/Child$Created;
public static final fun pop (Lcom/arkivanov/decompose/router/Router;)V
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 push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
Expand Down
6 changes: 4 additions & 2 deletions decompose/api/jvm/decompose.api
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,15 @@ public final class com/arkivanov/decompose/lifecycle/MergedLifecycle : com/arkiv

public abstract interface class com/arkivanov/decompose/router/Router {
public abstract fun getState ()Lcom/arkivanov/decompose/value/Value;
public abstract fun navigate (Lkotlin/jvm/functions/Function1;)V
public abstract fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
}

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 fun getActiveChild (Lcom/arkivanov/decompose/router/Router;)Lcom/arkivanov/decompose/Child$Created;
public static final fun pop (Lcom/arkivanov/decompose/router/Router;)V
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 push (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
public static final fun replaceCurrent (Lcom/arkivanov/decompose/router/Router;Ljava/lang/Object;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,20 @@ interface Router<C : Any, out T : Any> {
* The stack is represented as [List], where the last element is the top of the stack,
* and the first element is the bottom of the stack. The returned stack must not be empty.
*
* The [Router] compares the current stack with new one returned by the [transformer] function.
* New components are created for all new configurations in the stack, and all components
* that are no longer in the stack are destroyed. The amount and order of components in the
* resulting stack matches the amount and order of configurations returned by the [transformer].
* During the navigation process, the `Router` compares the new stack of configurations with
* the previous one. The `Router` ensures that all removed components are destroyed, and that
* there is only one component resumed at a time - the top one. All components in the back stack
* are always either stopped or destroyed.
*
* The `Router` usually performs the navigation synchronously, which means that by the time
* the `navigate` method returns, the navigation is finished and all component lifecycles are
* moved into required states. However the navigation is performed asynchronously in case of
* recursive invocations - e.g. `pop` is called from `onResume` lifecycle callback of a
* component being pushed. All recursive invocations are queued and performed one by one once
* the current navigation is finished.
*
* @param transformer transforms the current configuration stack to a new one.
* @param onComplete called when the navigation is finished (either synchronously or asynchronously).
*/
fun navigate(transformer: (stack: List<C>) -> List<C>)
fun navigate(transformer: (stack: List<C>) -> List<C>, onComplete: (newStack: List<C>, oldStack: List<C>) -> Unit)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,31 @@ package com.arkivanov.decompose.router
import com.arkivanov.decompose.Child

/**
* Pushes the provided [configuration] at the top of the stack
* A convenience method for [Router.navigate].
*/
fun <C : Any> Router<C, *>.navigate(transformer: (stack: List<C>) -> List<C>) {
navigate(transformer = transformer, onComplete = { _, _ -> })
}

/**
* Pushes the provided [configuration] at the top of the stack..
*/
fun <C : Any> Router<C, *>.push(configuration: C) {
navigate { it + configuration }
}

/**
* Pops the latest configuration at the top of the stack
* Pops the latest configuration at the top of the stack.
*
* @param onComplete called when the navigation is finished (either synchronously or asynchronously).
* The `isSuccess` argument is `true` if the stack size was greater than 1 and a component was popped,
* `false` otherwise.
*/
fun <C : Any> Router<C, *>.pop() {
navigate { it.dropLast(1) }
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) }
)
}

/**
Expand All @@ -24,7 +38,7 @@ inline fun <C : Any> Router<C, *>.popWhile(crossinline predicate: (C) -> Boolean
}

/**
* Replaces the current configuration at the top of the stack with the provided [configuration]
* Replaces the current configuration at the top of the stack with the provided [configuration].
*/
fun <C : Any> Router<C, *>.replaceCurrent(configuration: C) {
navigate { it.dropLast(1) + configuration }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@ internal class RouterImpl<C : Any, T : Any>(
backPressedHandler.unregister(onBackPressedHandler)
}

override fun navigate(transformer: (stack: List<C>) -> List<C>) {
queue.offer(transformer)
override fun navigate(transformer: (stack: List<C>) -> List<C>, onComplete: (newStack: List<C>, oldStack: List<C>) -> Unit) {
queue.offer(NavigationItem(transformer = transformer, onComplete = onComplete))
}

private fun navigateActual(transformer: (stack: List<C>) -> List<C>) {
val newStack = navigator.navigate(oldStack = stackHolder.stack, transformer = transformer)
private fun navigateActual(item: NavigationItem<C>) {
val oldStack = stackHolder.stack
val newStack = navigator.navigate(oldStack = oldStack, transformer = item.transformer)
stackHolder.stack = newStack
state.value = newStack.toState()
item.onComplete(newStack.configurationStack, oldStack.configurationStack)
}

private fun onBackPressed(): Boolean =
Expand All @@ -66,4 +68,9 @@ internal class RouterImpl<C : Any, T : Any>(
is RouterEntry.Created -> Child.Created(configuration = configuration, instance = instance)
is RouterEntry.Destroyed -> Child.Destroyed(configuration = configuration)
}

private class NavigationItem<C : Any>(
val transformer: (stack: List<C>) -> List<C>,
val onComplete: (newStack: List<C>, oldStack: List<C>) -> Unit,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.arkivanov.decompose.router

internal val <C : Any> RouterStack<C, *>.configurationBackStack: List<C>
get() =
object : AbstractList<C>() {
override val size: Int get() = backStack.size

override fun get(index: Int): C = backStack[index].configuration
}

internal val <C : Any> RouterStack<C, *>.configurationStack: List<C>
get() =
object : AbstractList<C>() {
override val size: Int get() = backStack.size + 1

override fun get(index: Int): C = (backStack.getOrNull(index) ?: active).configuration
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ internal class StackNavigatorImpl<C : Any, T : Any>(
) : StackNavigator<C, T> {

override fun navigate(oldStack: RouterStack<C, T>, transformer: (stack: List<C>) -> List<C>): RouterStack<C, T> {
val newConfigurationStack = transformer((oldStack.backStack + oldStack.active).map(RouterEntry<C, *>::configuration))
val oldConfigurationStack = oldStack.configurationStack
val newConfigurationStack = transformer(oldConfigurationStack)

if (newConfigurationStack === oldConfigurationStack) {
return oldStack
}

check(newConfigurationStack.isNotEmpty()) { "Configuration stack can not be empty" }
check(newConfigurationStack.isUnique()) { "Configurations in the stack must be unique" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.arkivanov.decompose.router

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

@Suppress("TestFunctionName")
class RouterPopTest {

@Test
fun GIVEN_stack_size_2_WHEN_pop_THEN_popped() {
val router = TestRouter(listOf(Config.A, Config.B))

router.pop()

assertEquals(listOf(Config.A), router.stack)
}

@Test
fun GIVEN_stack_size_2_WHEN_pop_THEN_onComplete_success() {
val router = TestRouter(listOf(Config.A, Config.B))
var isSuccess = false

router.pop { isSuccess = it }

assertTrue(isSuccess)
}

@Test
fun GIVEN_stack_size_1_WHEN_pop_THEN_not_popped() {
val router = TestRouter(listOf(Config.A))

router.pop()

assertEquals(listOf(Config.A), router.stack)
}


@Test
fun GIVEN_stack_size_1_WHEN_pop_THEN_onComplete_not_success() {
val router = TestRouter(listOf(Config.A))
var isSuccess = true

router.pop { isSuccess = it }

assertFalse(isSuccess)
}

private sealed class Config {
object A : Config()
object B : Config()
}
}
Loading

0 comments on commit 309fb39

Please sign in to comment.