From 3357082feca0270078811e4b83c5143ceaf83e41 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Wed, 14 Sep 2022 20:51:15 +0100 Subject: [PATCH] Added Child Overlay --- decompose/api/android/decompose.api | 49 ++++ decompose/api/jvm/decompose.api | 49 ++++ .../com/arkivanov/decompose/Optional.kt | 11 + .../decompose/router/overlay/ChildOverlay.kt | 10 + .../router/overlay/ChildOverlayFactory.kt | 125 ++++++++ .../router/overlay/OverlayNavigation.kt | 6 + .../overlay/OverlayNavigationFactory.kt | 30 ++ .../router/overlay/OverlayNavigationSource.kt | 18 ++ .../router/overlay/OverlayNavigator.kt | 28 ++ .../router/overlay/OverlayNavigatorExt.kt | 29 ++ .../decompose/router/stack/ChildStack.kt | 8 +- .../router/stack/ChildStackFactory.kt | 105 ++++--- .../router/stack/StackNavigationSource.kt | 2 +- .../decompose/router/stack/StackNavigator.kt | 2 +- .../decompose/router/stack/StackSaverImpl.kt | 14 +- .../DefaultChildBackHandlerTest.kt | 244 ++++++---------- .../overlay/ChildOverlayIntegrationTest.kt | 276 ++++++++++++++++++ 17 files changed, 807 insertions(+), 199 deletions(-) create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/Optional.kt create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/ChildOverlay.kt create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/ChildOverlayFactory.kt create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigation.kt create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigationFactory.kt create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigationSource.kt create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigator.kt create mode 100644 decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigatorExt.kt create mode 100644 decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/overlay/ChildOverlayIntegrationTest.kt diff --git a/decompose/api/android/decompose.api b/decompose/api/android/decompose.api index a81a29405..37da4d6fa 100644 --- a/decompose/api/android/decompose.api +++ b/decompose/api/android/decompose.api @@ -70,6 +70,55 @@ public final class com/arkivanov/decompose/lifecycle/MergedLifecycle : com/arkiv public fun unsubscribe (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V } +public final class com/arkivanov/decompose/router/overlay/ChildOverlay { + public fun ()V + public fun (Lcom/arkivanov/decompose/Child$Created;)V + public synthetic fun (Lcom/arkivanov/decompose/Child$Created;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lcom/arkivanov/decompose/Child$Created; + public final fun copy (Lcom/arkivanov/decompose/Child$Created;)Lcom/arkivanov/decompose/router/overlay/ChildOverlay; + public static synthetic fun copy$default (Lcom/arkivanov/decompose/router/overlay/ChildOverlay;Lcom/arkivanov/decompose/Child$Created;ILjava/lang/Object;)Lcom/arkivanov/decompose/router/overlay/ChildOverlay; + public fun equals (Ljava/lang/Object;)Z + public final fun getOverlay ()Lcom/arkivanov/decompose/Child$Created; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/arkivanov/decompose/router/overlay/ChildOverlayFactoryKt { + public static final fun childOverlay (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/overlay/OverlayNavigationSource;Ljava/lang/String;Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function0;ZZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; + public static synthetic fun childOverlay$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/overlay/OverlayNavigationSource;Ljava/lang/String;Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function0;ZZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; +} + +public abstract interface class com/arkivanov/decompose/router/overlay/OverlayNavigation : com/arkivanov/decompose/router/overlay/OverlayNavigationSource, com/arkivanov/decompose/router/overlay/OverlayNavigator { +} + +public final class com/arkivanov/decompose/router/overlay/OverlayNavigationFactoryKt { + public static final fun OverlayNavigation ()Lcom/arkivanov/decompose/router/overlay/OverlayNavigation; +} + +public abstract interface class com/arkivanov/decompose/router/overlay/OverlayNavigationSource { + public abstract fun subscribe (Lkotlin/jvm/functions/Function1;)V + public abstract fun unsubscribe (Lkotlin/jvm/functions/Function1;)V +} + +public final class com/arkivanov/decompose/router/overlay/OverlayNavigationSource$Event { + public fun (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V + public synthetic fun (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getOnComplete ()Lkotlin/jvm/functions/Function2; + public final fun getTransformer ()Lkotlin/jvm/functions/Function1; +} + +public abstract interface class com/arkivanov/decompose/router/overlay/OverlayNavigator { + public abstract fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V +} + +public final class com/arkivanov/decompose/router/overlay/OverlayNavigatorExtKt { + public static final fun activate (Lcom/arkivanov/decompose/router/overlay/OverlayNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V + public static synthetic fun activate$default (Lcom/arkivanov/decompose/router/overlay/OverlayNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V + public static final fun dismiss (Lcom/arkivanov/decompose/router/overlay/OverlayNavigator;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun dismiss$default (Lcom/arkivanov/decompose/router/overlay/OverlayNavigator;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static final fun navigate (Lcom/arkivanov/decompose/router/overlay/OverlayNavigator;Lkotlin/jvm/functions/Function1;)V +} + public final class com/arkivanov/decompose/router/stack/ChildStack { public fun (Lcom/arkivanov/decompose/Child$Created;Ljava/util/List;)V public synthetic fun (Lcom/arkivanov/decompose/Child$Created;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/decompose/api/jvm/decompose.api b/decompose/api/jvm/decompose.api index f432e62f7..445a96a36 100644 --- a/decompose/api/jvm/decompose.api +++ b/decompose/api/jvm/decompose.api @@ -57,6 +57,55 @@ public final class com/arkivanov/decompose/lifecycle/MergedLifecycle : com/arkiv public fun unsubscribe (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V } +public final class com/arkivanov/decompose/router/overlay/ChildOverlay { + public fun ()V + public fun (Lcom/arkivanov/decompose/Child$Created;)V + public synthetic fun (Lcom/arkivanov/decompose/Child$Created;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lcom/arkivanov/decompose/Child$Created; + public final fun copy (Lcom/arkivanov/decompose/Child$Created;)Lcom/arkivanov/decompose/router/overlay/ChildOverlay; + public static synthetic fun copy$default (Lcom/arkivanov/decompose/router/overlay/ChildOverlay;Lcom/arkivanov/decompose/Child$Created;ILjava/lang/Object;)Lcom/arkivanov/decompose/router/overlay/ChildOverlay; + public fun equals (Ljava/lang/Object;)Z + public final fun getOverlay ()Lcom/arkivanov/decompose/Child$Created; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/arkivanov/decompose/router/overlay/ChildOverlayFactoryKt { + public static final fun childOverlay (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/overlay/OverlayNavigationSource;Ljava/lang/String;Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function0;ZZLkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/value/Value; + public static synthetic fun childOverlay$default (Lcom/arkivanov/decompose/ComponentContext;Lcom/arkivanov/decompose/router/overlay/OverlayNavigationSource;Ljava/lang/String;Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function0;ZZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/value/Value; +} + +public abstract interface class com/arkivanov/decompose/router/overlay/OverlayNavigation : com/arkivanov/decompose/router/overlay/OverlayNavigationSource, com/arkivanov/decompose/router/overlay/OverlayNavigator { +} + +public final class com/arkivanov/decompose/router/overlay/OverlayNavigationFactoryKt { + public static final fun OverlayNavigation ()Lcom/arkivanov/decompose/router/overlay/OverlayNavigation; +} + +public abstract interface class com/arkivanov/decompose/router/overlay/OverlayNavigationSource { + public abstract fun subscribe (Lkotlin/jvm/functions/Function1;)V + public abstract fun unsubscribe (Lkotlin/jvm/functions/Function1;)V +} + +public final class com/arkivanov/decompose/router/overlay/OverlayNavigationSource$Event { + public fun (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V + public synthetic fun (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getOnComplete ()Lkotlin/jvm/functions/Function2; + public final fun getTransformer ()Lkotlin/jvm/functions/Function1; +} + +public abstract interface class com/arkivanov/decompose/router/overlay/OverlayNavigator { + public abstract fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V +} + +public final class com/arkivanov/decompose/router/overlay/OverlayNavigatorExtKt { + public static final fun activate (Lcom/arkivanov/decompose/router/overlay/OverlayNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V + public static synthetic fun activate$default (Lcom/arkivanov/decompose/router/overlay/OverlayNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V + public static final fun dismiss (Lcom/arkivanov/decompose/router/overlay/OverlayNavigator;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun dismiss$default (Lcom/arkivanov/decompose/router/overlay/OverlayNavigator;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static final fun navigate (Lcom/arkivanov/decompose/router/overlay/OverlayNavigator;Lkotlin/jvm/functions/Function1;)V +} + public final class com/arkivanov/decompose/router/stack/ChildStack { public fun (Lcom/arkivanov/decompose/Child$Created;Ljava/util/List;)V public synthetic fun (Lcom/arkivanov/decompose/Child$Created;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Optional.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Optional.kt new file mode 100644 index 000000000..92b3a21a7 --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/Optional.kt @@ -0,0 +1,11 @@ +package com.arkivanov.decompose + +internal data class Optional(val value: T?) { + + companion object { + val EMPTY: Optional = Optional(null) + } +} + +internal fun optionalOf(value: T? = null): Optional = + if (value == null) Optional.EMPTY else Optional(value) diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/ChildOverlay.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/ChildOverlay.kt new file mode 100644 index 000000000..70c27c3af --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/ChildOverlay.kt @@ -0,0 +1,10 @@ +package com.arkivanov.decompose.router.overlay + +import com.arkivanov.decompose.Child + +/** + * A state holder for `Child Overlay`. + */ +data class ChildOverlay( + val overlay: Child.Created? = null, +) diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/ChildOverlayFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/ChildOverlayFactory.kt new file mode 100644 index 000000000..f92c8f94f --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/ChildOverlayFactory.kt @@ -0,0 +1,125 @@ +package com.arkivanov.decompose.router.overlay + +import com.arkivanov.decompose.Child +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.Optional +import com.arkivanov.decompose.instancekeeper.attachTo +import com.arkivanov.decompose.optionalOf +import com.arkivanov.decompose.router.stack.StackNavigationSource +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.value.Value +import com.arkivanov.decompose.value.operator.map +import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher +import com.arkivanov.essenty.parcelable.Parcelable +import com.arkivanov.essenty.parcelable.ParcelableContainer +import com.arkivanov.essenty.statekeeper.StateKeeperDispatcher +import kotlin.reflect.KClass + +/** + * Initializes and manages component overlay. An overlay component can be either active or dismissed (destroyed). + * + * @param source a source of navigation events + * @param key a key of the overlay, must be unique within the parent component + * @param configurationClass a [KClass] of the component configurations + * @param initialConfiguration a component configuration that should be shown if there is + * no saved state, return `null` to show nothing. + * @param persistent determines whether the navigation state should pre preserved or not, + * default is `true`. + * @param handleBackButton determines whether the overlay should be automatically dismissed + * on back button press or not, default is `false`. + * @param childFactory a factory function that creates new child instances + * @return an observable [Value] of [ChildOverlay] + */ +fun ComponentContext.childOverlay( + source: OverlayNavigationSource, + key: String, + configurationClass: KClass, + initialConfiguration: () -> C? = { null }, + persistent: Boolean = true, + handleBackButton: Boolean = false, + childFactory: (configuration: C, ComponentContext) -> T, +): Value> = + childStack( + lifecycle = lifecycle, + stateKeeper = if (persistent) stateKeeper else StateKeeperDispatcher(), + instanceKeeper = if (persistent) instanceKeeper else InstanceKeeperDispatcher().attachTo(lifecycle), + backHandler = backHandler, + source = StackNavigationSourceImpl(source), + initialStack = { configurationStack(initialConfiguration()) }, + saveConfiguration = { ParcelableContainer(it.value) }, + restoreConfiguration = { optionalOf(it.consume(configurationClass)) }, + key = key, + handleBackButton = handleBackButton, + childFactory = { configuration, componentContext -> + if (configuration.value != null) { + optionalOf(childFactory(configuration.value, componentContext)) + } else { + optionalOf() + } + }, + ).map { it.active.toChildOverlay() } + +/** + * A convenience extension function for [ComponentContext.childOverlay]. + */ +inline fun ComponentContext.childOverlay( + source: OverlayNavigationSource, + key: String, + noinline initialConfiguration: () -> C? = { null }, + persistent: Boolean = true, + handleBackButton: Boolean = false, + noinline childFactory: (configuration: C, ComponentContext) -> T, +): Value> = + childOverlay( + source = source, + key = key, + configurationClass = C::class, + initialConfiguration = initialConfiguration, + persistent = persistent, + handleBackButton = handleBackButton, + childFactory = childFactory, + ) + +private fun Child.Created, Optional>.toChildOverlay(): ChildOverlay = + if ((configuration.value != null) && (instance.value != null)) { + ChildOverlay( + overlay = Child.Created( + configuration = configuration.value, + instance = instance.value, + ) + ) + } else { + ChildOverlay() + } + +private fun configurationStack(configuration: C?): List> = + listOfNotNull(optionalOf(), configuration?.let(::optionalOf)) + +private class StackNavigationSourceImpl( + private val delegate: OverlayNavigationSource, +) : StackNavigationSource> { + + private var map = HashMap<(StackNavigationSource.Event>) -> Unit, (OverlayNavigationSource.Event) -> Unit>() + + override fun subscribe(observer: (StackNavigationSource.Event>) -> Unit) { + check(observer !in map) + + val sourceObserver: (OverlayNavigationSource.Event) -> Unit = { observer(it.toStackEvent()) } + map += observer to sourceObserver + delegate.subscribe(sourceObserver) + } + + private fun OverlayNavigationSource.Event.toStackEvent(): StackNavigationSource.Event> = + StackNavigationSource.Event( + transformer = { stack -> configurationStack(transformer(stack.last().value)) }, + onComplete = { newStack, oldStack -> + onComplete(newStack.lastOrNull()?.value, oldStack.lastOrNull()?.value) + }, + ) + + override fun unsubscribe(observer: (StackNavigationSource.Event>) -> Unit) { + map.remove(observer)?.also { + delegate.unsubscribe(it) + } + } +} diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigation.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigation.kt new file mode 100644 index 000000000..43ec42373 --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigation.kt @@ -0,0 +1,6 @@ +package com.arkivanov.decompose.router.overlay + +/** + * Represents [OverlayNavigator] and [OverlayNavigationSource] at the same time. + */ +interface OverlayNavigation : OverlayNavigator, OverlayNavigationSource diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigationFactory.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigationFactory.kt new file mode 100644 index 000000000..4d5b77292 --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigationFactory.kt @@ -0,0 +1,30 @@ +package com.arkivanov.decompose.router.overlay + +import com.arkivanov.decompose.Relay +import com.arkivanov.decompose.router.overlay.OverlayNavigationSource.Event + +/** + * Returns a default implementation of [OverlayNavigation]. + * Broadcasts navigation events to all subscribed observers. + */ +fun OverlayNavigation(): OverlayNavigation = OverlayNavigationImpl() + +private class OverlayNavigationImpl : OverlayNavigation { + + private val relay = Relay>() + + override fun navigate( + transformer: (configuration: C?) -> C?, + onComplete: (newConfiguration: C?, oldConfiguration: C?) -> Unit, + ) { + relay.accept(Event(transformer, onComplete)) + } + + override fun subscribe(observer: (Event) -> Unit) { + relay.subscribe(observer) + } + + override fun unsubscribe(observer: (Event) -> Unit) { + relay.unsubscribe(observer) + } +} diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigationSource.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigationSource.kt new file mode 100644 index 000000000..e7fe88b63 --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigationSource.kt @@ -0,0 +1,18 @@ +package com.arkivanov.decompose.router.overlay + +/** + * Represents a source of navigation events for `Child Overlay`. + * + * @see OverlayNavigator + */ +interface OverlayNavigationSource { + + fun subscribe(observer: (Event) -> Unit) + + fun unsubscribe(observer: (Event) -> Unit) + + class Event( + val transformer: (configuration: C?) -> C?, + val onComplete: (newConfiguration: C?, oldConfiguration: C?) -> Unit = { _, _ -> }, + ) +} diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigator.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigator.kt new file mode 100644 index 000000000..eea9f2e0a --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigator.kt @@ -0,0 +1,28 @@ +package com.arkivanov.decompose.router.overlay + +interface OverlayNavigator { + + /** + * Transforms the current configuration into a new one. Configuration `null` means that the + * component is not shown. + * + * During the navigation process, the `Child Overlay` compares the new configuration with + * the previous one. The `Child Overlay` ensures that the current component is resumed, and a + * dismissed component is destroyed. + * + * The `Child Overlay` 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. `dismiss` is called from `onResume` lifecycle callback of a + * component being shown. All recursive invocations are queued and performed one by one once + * the current navigation is finished. + * + * @param transformer transforms the current configuration to a new one, `null` means that the + * component is not shown. + * @param onComplete called when the navigation is finished (either synchronously or asynchronously). + */ + fun navigate( + transformer: (configuration: C?) -> C?, + onComplete: (newConfiguration: C?, oldConfiguration: C?) -> Unit, + ) +} diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigatorExt.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigatorExt.kt new file mode 100644 index 000000000..41ae52118 --- /dev/null +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/overlay/OverlayNavigatorExt.kt @@ -0,0 +1,29 @@ +package com.arkivanov.decompose.router.overlay + +/** + * A convenience method for [OverlayNavigator.navigate]. + */ +fun OverlayNavigator.navigate(transformer: (configuration: C?) -> C?) { + navigate(transformer = transformer, onComplete = { _, _ -> }) +} + +/** + * Activates an overlay component represented by the provided [configuration], + * and dismisses (destroys) any currently active component. Does nothing if the provided [configuration] + * is equal to the currently active one. + * + * @param onComplete called when the navigation is finished (either synchronously or asynchronously). + */ +fun OverlayNavigator.activate(configuration: C, onComplete: () -> Unit = {}) { + navigate(transformer = { configuration }, onComplete = { _, _ -> onComplete() }) +} + +/** + * Dismisses (destroys) the currently active overlay component, if any. + * + * @param onComplete called when the navigation is finished (either synchronously or asynchronously). + * The `isSuccess` argument is `true` if there was an active overlay, `false` otherwise. + */ +fun OverlayNavigator.dismiss(onComplete: (isSuccess: Boolean) -> Unit = {}) { + navigate(transformer = { null }, onComplete = { _, oldConfiguration -> onComplete(oldConfiguration != null) }) +} diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStack.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStack.kt index 72b30733d..9ca14471b 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStack.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStack.kt @@ -2,6 +2,9 @@ package com.arkivanov.decompose.router.stack import com.arkivanov.decompose.Child +/** + * A state holder for `Child Stack`. + */ data class ChildStack( val active: Child.Created, val backStack: List> = emptyList(), @@ -14,9 +17,12 @@ data class ChildStack( ), ) + /** + * Returns the full stack of component configurations, ordered from tail to head. + */ val items: List> = Items(active = active, backStack = backStack) - private class Items( + private class Items( private val active: Child.Created, private val backStack: List> = emptyList(), ) : AbstractList>() { 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 e29db2028..6ab16a113 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 @@ -3,19 +3,25 @@ package com.arkivanov.decompose.router.stack import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.backhandler.child import com.arkivanov.decompose.value.Value +import com.arkivanov.essenty.backhandler.BackHandler +import com.arkivanov.essenty.instancekeeper.InstanceKeeper +import com.arkivanov.essenty.lifecycle.Lifecycle import com.arkivanov.essenty.parcelable.Parcelable import com.arkivanov.essenty.parcelable.ParcelableContainer +import com.arkivanov.essenty.parcelable.consumeRequired +import com.arkivanov.essenty.statekeeper.StateKeeper import kotlin.reflect.KClass /** * Initializes and manages a stack of components. * * @param source a source of navigation events - * @param initialStack a stack of component configurations (from tail to head) that should be set if there is - * no saved state, must be not empty and unique + * @param initialStack a stack of component configurations (ordered from tail to head) 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 stack, should be unique if there are multiple stacks in the same component - * @param handleBackButton determines whether the stack should be automatically popped on back button press or not + * @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 childFactory a factory function that creates new child instances * @return an observable [Value] of [ChildStack] */ @@ -26,38 +32,20 @@ fun ComponentContext.childStack( key: String = "DefaultChildStack", handleBackButton: Boolean = false, childFactory: (configuration: C, ComponentContext) -> T, -): Value> { - val routerBackHandler = backHandler.takeIf { handleBackButton }?.child() - - val routerEntryFactory = - RouterEntryFactoryImpl( - lifecycle = lifecycle, - backHandler = backHandler.child(), - childFactory = childFactory, - ) - - val controller = - ChildStackController( - lifecycle = lifecycle, - backHandler = routerBackHandler, - stackHolder = StackHolderImpl( - initialStack = initialStack, - lifecycle = lifecycle, - key = key, - stackSaver = StackSaverImpl( - configurationClass = configurationClass, - stateKeeper = stateKeeper, - parcelableContainerFactory = ::ParcelableContainer - ), - instanceKeeper = instanceKeeper, - routerEntryFactory = routerEntryFactory - ), - controller = StackControllerImpl(routerEntryFactory = routerEntryFactory), - source = source, - ) - - return controller.stack -} +): Value> = + childStack( + lifecycle = lifecycle, + stateKeeper = stateKeeper, + instanceKeeper = instanceKeeper, + backHandler = backHandler, + source = source, + initialStack = initialStack, + saveConfiguration = { ParcelableContainer(it) }, + restoreConfiguration = { it.consumeRequired(configurationClass) }, + key = key, + handleBackButton = handleBackButton, + childFactory = childFactory, + ) /** * A convenience extension function for [ComponentContext.childStack]. @@ -96,3 +84,48 @@ inline fun ComponentContext.childStack( handleBackButton = handleBackButton, childFactory = childFactory, ) + +internal fun childStack( + lifecycle: Lifecycle, + stateKeeper: StateKeeper, + instanceKeeper: InstanceKeeper, + backHandler: BackHandler, + source: StackNavigationSource, + initialStack: () -> List, + saveConfiguration: (C) -> ParcelableContainer, + restoreConfiguration: (ParcelableContainer) -> C, + key: String, + handleBackButton: Boolean = false, + childFactory: (configuration: C, ComponentContext) -> T, +): Value> { + val routerBackHandler = backHandler.takeIf { handleBackButton }?.child() + + val routerEntryFactory = + RouterEntryFactoryImpl( + lifecycle = lifecycle, + backHandler = backHandler.child(), + childFactory = childFactory, + ) + + val controller = + ChildStackController( + lifecycle = lifecycle, + backHandler = routerBackHandler, + stackHolder = StackHolderImpl( + initialStack = initialStack, + lifecycle = lifecycle, + key = key, + stackSaver = StackSaverImpl( + stateKeeper = stateKeeper, + saveConfiguration = saveConfiguration, + restoreConfiguration = restoreConfiguration, + ), + instanceKeeper = instanceKeeper, + routerEntryFactory = routerEntryFactory + ), + controller = StackControllerImpl(routerEntryFactory = routerEntryFactory), + source = source, + ) + + return controller.stack +} diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigationSource.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigationSource.kt index 6d4cc265d..eb9b6d6a6 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigationSource.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigationSource.kt @@ -1,7 +1,7 @@ package com.arkivanov.decompose.router.stack /** - * Represents a source of navigation events. + * Represents a source of navigation events for `Child Stack`. * * @see StackNavigator */ diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigator.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigator.kt index 0d603489e..756db6ef7 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigator.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigator.kt @@ -14,7 +14,7 @@ interface StackNavigator { * * The `Child Stack` 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 + * 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. diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackSaverImpl.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackSaverImpl.kt index 5c4fd7165..b5fdf7a48 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackSaverImpl.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackSaverImpl.kt @@ -4,15 +4,13 @@ import com.arkivanov.decompose.router.stack.StackSaver.RestoredStack 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 com.arkivanov.essenty.statekeeper.StateKeeper import com.arkivanov.essenty.statekeeper.consume -import kotlin.reflect.KClass -internal class StackSaverImpl( - private val configurationClass: KClass, +internal class StackSaverImpl( private val stateKeeper: StateKeeper, - private val parcelableContainerFactory: (Parcelable?) -> ParcelableContainer, + private val saveConfiguration: (C) -> ParcelableContainer, + private val restoreConfiguration: (ParcelableContainer) -> C, ) : StackSaver { override fun register(key: String, supplier: () -> RouterStack) { @@ -28,12 +26,12 @@ internal class StackSaverImpl( private fun RouterStack.save(): SavedState = SavedState( active = SavedEntry( - configuration = parcelableContainerFactory(active.configuration), + configuration = saveConfiguration(active.configuration), savedState = active.stateKeeperDispatcher.save() ), backStack = backStack.map { SavedEntry( - configuration = parcelableContainerFactory(it.configuration), + configuration = saveConfiguration(it.configuration), savedState = it.savedState ) } @@ -56,7 +54,7 @@ internal class StackSaverImpl( private fun SavedEntry.restore(): RouterEntry.Destroyed = RouterEntry.Destroyed( - configuration = configuration.consumeRequired(configurationClass), + configuration = restoreConfiguration(configuration), savedState = savedState ) diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/backhandler/DefaultChildBackHandlerTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/backhandler/DefaultChildBackHandlerTest.kt index 2b1a8a150..7af49ea22 100644 --- a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/backhandler/DefaultChildBackHandlerTest.kt +++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/backhandler/DefaultChildBackHandlerTest.kt @@ -10,32 +10,24 @@ import kotlin.test.assertTrue class DefaultChildBackHandlerTest { private val parent = TestBackDispatcher() - private val handler = DefaultChildBackHandler(parent = parent, isEnabled = false) @Test - fun WHEN_created_THEN_not_registered_in_parent() { - assertEquals(0, parent.size) - } - - @Test - fun GIVEN_no_callbacks_registered_WHEN_start_THEN_registered_in_parent() { - handler.start() + fun WHEN_created_disabled_THEN_not_registered_in_parent() { + handler(isEnabled = false) - assertEquals(1, parent.size) + assertEquals(0, parent.size) } @Test - fun GIVEN_started_and_no_callbacks_registered_WHEN_stop_THEN_removed_from_parent() { - handler.start() - - handler.stop() + fun WHEN_created_enabled_THEN_not_registered_in_parent() { + handler(isEnabled = true) assertEquals(0, parent.size) } @Test - fun GIVEN_enabled_callback_registered_WHEN_start_THEN_registered_in_parent() { - handler.register(callback(isEnabled = true)) + fun GIVEN_created_disabled_WHEN_started_THEN_registered_in_parent() { + val handler = handler(isEnabled = false) handler.start() @@ -43,8 +35,8 @@ class DefaultChildBackHandlerTest { } @Test - fun GIVEN_disabled_callback_registered_WHEN_start_THEN_registered_in_parent() { - handler.register(callback(isEnabled = false)) + fun GIVEN_created_enabled_WHEN_started_THEN_registered_in_parent() { + val handler = handler(isEnabled = true) handler.start() @@ -52,8 +44,8 @@ class DefaultChildBackHandlerTest { } @Test - fun GIVEN_callback_registered_and_started_WHEN_stop_THEN_removed_from_parent() { - handler.register(callback()) + fun GIVEN_disabled_and_started_WHEN_stopped_THEN_not_registered_in_parent() { + val handler = handler(isEnabled = false) handler.start() handler.stop() @@ -62,63 +54,50 @@ class DefaultChildBackHandlerTest { } @Test - fun GIVEN_not_started_WHEN_callback_registered_THEN_not_registered_in_parent() { - handler.register(callback()) - - assertEquals(0, parent.size) - } - - @Test - fun WHEN_created_THEN_parent_disabled() { - assertFalse(parent.isEnabled) - } - - @Test - fun GIVEN_disabled_and_not_started_WHEN_enabled_callback_registered_THEN_parent_disabled() { - handler.register(callback(isEnabled = true)) - - assertFalse(parent.isEnabled) - } + fun GIVEN_enabled_and_started_WHEN_stopped_THEN_not_registered_in_parent() { + val handler = handler(isEnabled = true) + handler.start() - @Test - fun GIVEN_disabled_and_not_started_WHEN_disabled_callback_registered_THEN_parent_disabled() { - handler.register(callback(isEnabled = false)) + handler.stop() - assertFalse(parent.isEnabled) + assertEquals(0, parent.size) } @Test - fun GIVEN_disabled_and_not_started_and_two_enabled_callbacks_registered_WHEN_one_callback_disabled_THEN_parent_disabled() { - val callback1 = callback(isEnabled = true) - handler.register(callback1) - handler.register(callback(isEnabled = true)) + fun GIVEN_disabled_stopped_WHEN_started_THEN_registered_in_parent() { + val handler = handler(isEnabled = false) + handler.start() + handler.stop() - callback1.isEnabled = false + handler.start() - assertFalse(parent.isEnabled) + assertEquals(1, parent.size) } @Test - fun GIVEN_disabled_and_not_started_and_two_enabled_callbacks_registered_WHEN_all_callbacks_disabled_THEN_parent_disabled() { - val callback1 = callback(isEnabled = true) - handler.register(callback1) - handler.register(callback(isEnabled = true)) + fun GIVEN_enabled_stopped_WHEN_started_THEN_registered_in_parent() { + val handler = handler(isEnabled = true) + handler.start() + handler.stop() - callback1.isEnabled = false - callback1.isEnabled = false + handler.start() - assertFalse(parent.isEnabled) + assertEquals(1, parent.size) } @Test - fun GIVEN_disabled_and_no_callbacks_registered_WHEN_start_THEN_parent_disabled() { + fun GIVEN_started_and_disabled_WHEN_disabled_callback_registered_THEN_parent_disabled() { + val handler = handler(isEnabled = false) handler.start() + handler.register(callback(isEnabled = false)) + assertFalse(parent.isEnabled) } @Test - fun GIVEN_disabled_and_started_WHEN_enabled_callback_registered_THEN_parent_disabled() { + fun GIVEN_started_and_disabled_WHEN_enabled_callback_registered_THEN_parent_disabled() { + val handler = handler(isEnabled = false) handler.start() handler.register(callback(isEnabled = true)) @@ -127,7 +106,8 @@ class DefaultChildBackHandlerTest { } @Test - fun GIVEN_disabled_and_started_WHEN_disabled_callback_registered_THEN_parent_disabled() { + fun GIVEN_started_and_enabled_WHEN_disabled_callback_registered_THEN_parent_disabled() { + val handler = handler(isEnabled = true) handler.start() handler.register(callback(isEnabled = false)) @@ -136,176 +116,136 @@ class DefaultChildBackHandlerTest { } @Test - fun GIVEN_disabled_and_started_and_one_enabled_callback_registered_WHEN_callback_disabled_THEN_parent_disabled() { + fun GIVEN_started_and_enabled_WHEN_enabled_callback_registered_THEN_parent_enabled() { + val handler = handler(isEnabled = true) handler.start() - val callback = callback(isEnabled = true) - handler.register(callback) - callback.isEnabled = false + handler.register(callback(isEnabled = true)) - assertFalse(parent.isEnabled) + assertTrue(parent.isEnabled) } @Test - fun GIVEN_disabled_and_started_and_two_enabled_callbacks_registered_WHEN_one_callback_disabled_THEN_parent_disabled() { + fun GIVEN_started_and_disabled_and_disabled_callback_registered_WHEN_callback_enabled_THEN_parent_disabled() { + val handler = handler(isEnabled = false) handler.start() - val callback1 = callback(isEnabled = true) - handler.register(callback1) - handler.register(callback(isEnabled = true)) + val callback = callback(isEnabled = false) + handler.register(callback) - callback1.isEnabled = false + callback.isEnabled = true assertFalse(parent.isEnabled) } @Test - fun GIVEN_disabled_and_started_and_two_enabled_callbacks_registered_WHEN_all_callbacks_disabled_THEN_parent_disabled() { + fun GIVEN_started_and_disabled_and_disabled_callback_registered_WHEN_enabled_THEN_parent_disabled() { + val handler = handler(isEnabled = false) handler.start() - val callback1 = callback(isEnabled = true) - val callback2 = callback(isEnabled = true) - handler.register(callback1) - handler.register(callback2) - - callback1.isEnabled = false - callback2.isEnabled = false - - assertFalse(parent.isEnabled) - } + handler.register(callback(isEnabled = false)) - @Test - fun GIVEN_enabled_and_not_started_WHEN_enabled_callback_registered_THEN_parent_disabled() { handler.isEnabled = true - handler.register(callback(isEnabled = true)) - assertFalse(parent.isEnabled) } @Test - fun GIVEN_enabled_and_not_started_WHEN_disabled_callback_registered_THEN_parent_disabled() { - handler.isEnabled = true + fun GIVEN_started_and_enabled_and_disabled_callback_registered_WHEN_callback_enabled_THEN_parent_enabled() { + val handler = handler(isEnabled = true) + handler.start() + val callback = callback(isEnabled = false) + handler.register(callback) - handler.register(callback(isEnabled = false)) + callback.isEnabled = true - assertFalse(parent.isEnabled) + assertTrue(parent.isEnabled) } @Test - fun GIVEN_enabled_and_not_started_and_two_enabled_callbacks_registered_WHEN_one_callback_disabled_THEN_parent_disabled() { - handler.isEnabled = true - val callback1 = callback(isEnabled = true) - handler.register(callback1) + fun GIVEN_started_and_disabled_and_enabled_callback_registered_WHEN_enabled_THEN_parent_enabled() { + val handler = handler(isEnabled = false) + handler.start() handler.register(callback(isEnabled = true)) - callback1.isEnabled = false - - assertFalse(parent.isEnabled) - } - - @Test - fun GIVEN_enabled_and_not_started_and_two_enabled_callbacks_registered_WHEN_all_callbacks_disabled_THEN_parent_disabled() { handler.isEnabled = true - val callback1 = callback(isEnabled = true) - handler.register(callback1) - handler.register(callback(isEnabled = true)) - - callback1.isEnabled = false - callback1.isEnabled = false - assertFalse(parent.isEnabled) + assertTrue(parent.isEnabled) } @Test - fun GIVEN_enabled_and_no_callbacks_registered_WHEN_start_THEN_parent_disabled() { - handler.isEnabled = true - + fun GIVEN_started_and_enabled_WHEN_two_disabled_callbacks_registered_THEN_parent_disabled() { + val handler = handler(isEnabled = true) handler.start() + handler.register(callback(isEnabled = false)) + handler.register(callback(isEnabled = false)) + assertFalse(parent.isEnabled) } @Test - fun GIVEN_enabled_and_started_WHEN_enabled_callback_registered_THEN_parent_enabled() { - handler.isEnabled = true + fun GIVEN_started_and_enabled_WHEN_two_callbacks_registered_enabled_disabled_THEN_parent_enabled() { + val handler = handler(isEnabled = true) handler.start() handler.register(callback(isEnabled = true)) + handler.register(callback(isEnabled = false)) assertTrue(parent.isEnabled) } @Test - fun GIVEN_enabled_and_started_WHEN_disabled_callback_registered_THEN_parent_disabled() { - handler.isEnabled = true + fun GIVEN_started_and_enabled_and_two_disabled_callbacks_registered_WHEN_one_callback_enabled_THEN_parent_enabled() { + val handler = handler(isEnabled = true) handler.start() - handler.register(callback(isEnabled = false)) + val callback = callback(isEnabled = false) + handler.register(callback) - assertFalse(parent.isEnabled) + callback.isEnabled = true + + assertTrue(parent.isEnabled) } @Test - fun GIVEN_enabled_and_started_and_one_enabled_callback_registered_WHEN_callback_disabled_THEN_parent_disabled() { - handler.isEnabled = true + fun GIVEN_started_and_enabled_and_two_enabled_callbacks_registered_WHEN_one_callback_disabled_THEN_parent_enabled() { + val handler = handler(isEnabled = true) handler.start() + handler.register(callback(isEnabled = true)) val callback = callback(isEnabled = true) handler.register(callback) callback.isEnabled = false - assertFalse(parent.isEnabled) + assertTrue(parent.isEnabled) } @Test - fun GIVEN_enabled_and_started_and_two_enabled_callbacks_registered_WHEN_one_callback_disabled_THEN_parent_enabled() { - handler.isEnabled = true - handler.start() - val callback1 = callback(isEnabled = true) - handler.register(callback1) + fun GIVEN_not_started_and_enabled_enabled_callback_registered_WHEN_started_THEN_parent_enabled() { + val handler = handler(isEnabled = true) handler.register(callback(isEnabled = true)) - callback1.isEnabled = false - - assertTrue(parent.isEnabled) - } - - @Test - fun GIVEN_enabled_and_started_and_two_enabled_callbacks_registered_WHEN_all_callbacks_disabled_THEN_parent_disabled() { - handler.isEnabled = true handler.start() - val callback1 = callback(isEnabled = true) - val callback2 = callback(isEnabled = true) - handler.register(callback1) - handler.register(callback2) - - callback1.isEnabled = false - callback2.isEnabled = false - assertFalse(parent.isEnabled) + assertTrue(parent.isEnabled) } @Test - fun GIVEN_enabled_and_started_and_enabled_callback_registered_WHEN_disabled_THEN_parent_disabled() { - handler.isEnabled = true + fun GIVEN_started_and_enabled_and_multiple_callbacks_registered_WHEN_parent_back_THEN_last_enabled_callback_called() { + val handler = handler(isEnabled = true) handler.start() - handler.register(callback(isEnabled = true)) + val items = ArrayList() + handler.register(callback(isEnabled = true) { items += 1 }) + handler.register(callback(isEnabled = false) { items += 2 }) + handler.register(callback(isEnabled = true) { items += 3 }) + handler.register(callback(isEnabled = false) { items += 4 }) - handler.isEnabled = false + parent.back() - assertFalse(parent.isEnabled) + assertEquals(listOf(3), items) } - @Test - fun GIVEN_enabled_and_started_and_two_enabled_callbacks_registered_WHEN_disabled_THEN_parent_disabled() { - handler.isEnabled = true - handler.start() - handler.register(callback(isEnabled = true)) - handler.register(callback(isEnabled = true)) - - handler.isEnabled = false - - assertFalse(parent.isEnabled) - } + private fun handler(isEnabled: Boolean = false): DefaultChildBackHandler = + DefaultChildBackHandler(parent = parent, isEnabled = isEnabled) private fun callback(isEnabled: Boolean = true, onBack: () -> Unit = {}): BackCallback = BackCallback(isEnabled = isEnabled, onBack = onBack) diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/overlay/ChildOverlayIntegrationTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/overlay/ChildOverlayIntegrationTest.kt new file mode 100644 index 000000000..2f0fa4b69 --- /dev/null +++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/router/overlay/ChildOverlayIntegrationTest.kt @@ -0,0 +1,276 @@ +package com.arkivanov.decompose.router.overlay + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.DefaultComponentContext +import com.arkivanov.decompose.router.TestInstance +import com.arkivanov.decompose.statekeeper.TestStateKeeperDispatcher +import com.arkivanov.decompose.value.Value +import com.arkivanov.essenty.backhandler.BackDispatcher +import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.arkivanov.essenty.lifecycle.Lifecycle +import com.arkivanov.essenty.lifecycle.LifecycleRegistry +import com.arkivanov.essenty.lifecycle.resume +import com.arkivanov.essenty.parcelable.Parcelable +import com.arkivanov.essenty.parcelable.Parcelize +import com.arkivanov.essenty.statekeeper.consume +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlin.test.assertTrue + +@Suppress("TestFunctionName") +class ChildOverlayIntegrationTest { + + private val navigation = OverlayNavigation() + private val lifecycle = LifecycleRegistry() + private val stateKeeperDispatcher = TestStateKeeperDispatcher() + private val instanceKeeperDispatcher = InstanceKeeperDispatcher() + private val backDispatcher = BackDispatcher() + + private val context = + DefaultComponentContext( + lifecycle = lifecycle, + stateKeeper = stateKeeperDispatcher, + instanceKeeper = instanceKeeperDispatcher, + backHandler = backDispatcher, + ) + + @BeforeTest + fun before() { + lifecycle.resume() + } + + @Test + fun WHEN_created_without_configuration_THEN_overlay_not_shown() { + val overlay = context.childOverlay(initialConfiguration = null) + + overlay.value.assertOverlay(null) + } + + @Test + fun WHEN_created_with_configuration_THEN_overlay_shown() { + val overlay = context.childOverlay(initialConfiguration = Config(1)) + + overlay.value.assertOverlay(1) + } + + @Test + fun GIVEN_not_shown_WHEN_show_THEN_overlay_shown() { + val overlay = context.childOverlay(initialConfiguration = null) + + navigation.show(Config(1)) + + overlay.value.assertOverlay(1) + } + + @Test + fun GIVEN_shown_WHEN_dismiss_THEN_overlay_not_shown() { + val overlay = context.childOverlay(initialConfiguration = Config(1)) + + navigation.dismiss() + + overlay.value.assertOverlay(null) + } + + @Test + fun GIVEN_shown_WHEN_show_with_same_configuration_THEN_same_overlay_shown() { + val overlay = context.childOverlay(initialConfiguration = Config(1)) + + navigation.show(Config(1)) + + overlay.value.assertOverlay(1) + } + + @Test + fun GIVEN_shown_WHEN_show_with_same_configuration_THEN_same_instance_shown() { + val overlay = context.childOverlay(initialConfiguration = Config(1)) + val instance = overlay.value.overlay?.instance + + navigation.show(Config(1)) + + assertSame(instance, overlay.value.overlay?.instance) + } + + @Test + fun GIVEN_shown_WHEN_show_with_other_configuration_THEN_other_overlay_shown() { + val overlay = context.childOverlay(initialConfiguration = Config(1)) + + navigation.show(Config(2)) + + overlay.value.assertOverlay(2) + } + + @Test + fun GIVEN_not_shown_WHEN_show_THEN_lifecycle_resumed() { + val overlay = context.childOverlay(initialConfiguration = null) + + navigation.show(Config(1)) + + assertEquals(Lifecycle.State.RESUMED, overlay.value.overlay?.instance?.lifecycle?.state) + } + + @Test + fun GIVEN_shown_WHEN_dismiss_THEN_lifecycle_destroyed() { + val overlay = context.childOverlay(initialConfiguration = Config(1)) + val lifecycle = overlay.value.overlay?.instance?.lifecycle + + navigation.dismiss() + + assertEquals(Lifecycle.State.DESTROYED, lifecycle?.state) + } + + @Test + fun GIVEN_not_shown_WHEN_parent_backDispatcher_isEnabled_THEN_false() { + context.childOverlay(initialConfiguration = null) + + assertFalse(backDispatcher.isEnabled) + } + + @Test + fun GIVEN_shown_WHEN_parent_backDispatcher_isEnabled_THEN_true() { + context.childOverlay(initialConfiguration = Config(1)) + + assertTrue(backDispatcher.isEnabled) + } + + @Test + fun GIVEN_shown_WHEN_back_pressed_THEN_overlay_not_shown() { + val overlay = context.childOverlay(initialConfiguration = Config(1)) + + backDispatcher.back() + + overlay.value.assertOverlay(null) + } + + @Test + fun GIVEN_hidden_via_back_pressed_WHEN_parent_backDispatcher_isEnabled_THEN_false() { + context.childOverlay(initialConfiguration = Config(1)) + backDispatcher.back() + + assertFalse(backDispatcher.isEnabled) + } + + @Test + fun GIVEN_persistent_WHEN_recreated_THEN_overlay_shown() { + val oldStateKeeper = TestStateKeeperDispatcher() + val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper) + oldContext.childOverlay(initialConfiguration = Config(1), persistent = true) + + val savedState = oldStateKeeper.save() + val newStateKeeper = TestStateKeeperDispatcher(savedState) + val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper) + val newOverlay = newContext.childOverlay(initialConfiguration = null, persistent = true) + + newOverlay.value.assertOverlay(1) + } + + @Test + fun GIVEN_persistent_WHEN_recreated_THEN_overlay_state_restored() { + val oldStateKeeper = TestStateKeeperDispatcher() + val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper) + val overlay = oldContext.childOverlay(initialConfiguration = Config(1), persistent = true) + overlay.value.overlay?.instance?.stateKeeper?.register("key") { Config(10) } + + val savedState = oldStateKeeper.save() + val newStateKeeper = TestStateKeeperDispatcher(savedState) + val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper) + val newOverlay = newContext.childOverlay(initialConfiguration = null, persistent = true) + val restoredState = newOverlay.value.overlay?.instance?.stateKeeper?.consume("key") + + assertEquals(Config(10), restoredState) + } + + @Test + fun GIVEN_not_persistent_WHEN_recreated_THEN_overlay_not_shown() { + val oldStateKeeper = TestStateKeeperDispatcher() + val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper) + oldContext.childOverlay(initialConfiguration = Config(1), persistent = false) + + val savedState = oldStateKeeper.save() + val newStateKeeper = TestStateKeeperDispatcher(savedState) + val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper) + val newOverlay = newContext.childOverlay(initialConfiguration = null, persistent = false) + + newOverlay.value.assertOverlay(null) + } + + @Test + fun WHEN_created_persistent_THEN_registered_in_parent_StateKeeper() { + context.childOverlay(initialConfiguration = Config(1), persistent = true) + + stateKeeperDispatcher.assertSupplierRegistered(key = "key") + } + + @Test + fun WHEN_created_not_persistent_THEN_not_registered_in_parent_StateKeeper() { + context.childOverlay(initialConfiguration = Config(1), persistent = false) + + stateKeeperDispatcher.assertSupplierNotRegistered(key = "key") + } + + @Test + fun GIVEN_persistent_WHEN_recreated_THEN_overlay_instance_retained() { + val oldStateKeeper = TestStateKeeperDispatcher() + val instanceKeeper = InstanceKeeperDispatcher() + val oldContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = oldStateKeeper, instanceKeeper = instanceKeeper) + val overlay = oldContext.childOverlay(initialConfiguration = Config(1), persistent = true) + val oldInstance = overlay.value.overlay?.instance?.instanceKeeper?.getOrCreate(::TestInstance) + + val savedState = oldStateKeeper.save() + val newStateKeeper = TestStateKeeperDispatcher(savedState) + val newContext = DefaultComponentContext(lifecycle = lifecycle, stateKeeper = newStateKeeper, instanceKeeper = instanceKeeper) + val newOverlay = newContext.childOverlay(initialConfiguration = null, persistent = true) + val retainedInstance = newOverlay.value.overlay?.instance?.instanceKeeper?.getOrCreate(::TestInstance) + + assertSame(oldInstance, retainedInstance) + } + + @Test + fun WHEN_created_persistent_THEN_registered_in_parent_InstanceKeeper() { + context.childOverlay(initialConfiguration = Config(1), persistent = true) + + assertNotNull(instanceKeeperDispatcher.get(key = "key")) + } + + @Test + fun WHEN_created_not_persistent_THEN_not_registered_in_parent_InstanceKeeper() { + context.childOverlay(initialConfiguration = Config(1), persistent = false) + + assertNull(instanceKeeperDispatcher.get(key = "key")) + } + + private fun ComponentContext.childOverlay( + initialConfiguration: Config?, + persistent: Boolean = true, + ): Value> = + childOverlay( + source = navigation, + key = "key", + initialConfiguration = { initialConfiguration }, + handleBackButton = true, + persistent = persistent, + childFactory = ::Component, + ) + + private fun ChildOverlay.assertOverlay(id: Int?) { + assertEquals(id, overlay?.configuration?.id) + assertEquals(id, overlay?.instance?.id) + } + + @Parcelize + private data class Config( + val id: Int, + ) : Parcelable + + private class Component( + config: Config, + componentContext: ComponentContext, + ) : ComponentContext by componentContext { + val id: Int = config.id + } +}