Skip to content

Commit

Permalink
Added Child Overlay
Browse files Browse the repository at this point in the history
  • Loading branch information
arkivanov committed Oct 2, 2022
1 parent 38b6008 commit 5f291e3
Show file tree
Hide file tree
Showing 17 changed files with 807 additions and 199 deletions.
49 changes: 49 additions & 0 deletions decompose/api/android/decompose.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()V
public fun <init> (Lcom/arkivanov/decompose/Child$Created;)V
public synthetic fun <init> (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 <init> (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
public synthetic fun <init> (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 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 static final fun show (Lcom/arkivanov/decompose/router/overlay/OverlayNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
public static synthetic fun show$default (Lcom/arkivanov/decompose/router/overlay/OverlayNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
}

public final class com/arkivanov/decompose/router/stack/ChildStack {
public fun <init> (Lcom/arkivanov/decompose/Child$Created;Ljava/util/List;)V
public synthetic fun <init> (Lcom/arkivanov/decompose/Child$Created;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
49 changes: 49 additions & 0 deletions decompose/api/jvm/decompose.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()V
public fun <init> (Lcom/arkivanov/decompose/Child$Created;)V
public synthetic fun <init> (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 <init> (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
public synthetic fun <init> (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 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 static final fun show (Lcom/arkivanov/decompose/router/overlay/OverlayNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
public static synthetic fun show$default (Lcom/arkivanov/decompose/router/overlay/OverlayNavigator;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
}

public final class com/arkivanov/decompose/router/stack/ChildStack {
public fun <init> (Lcom/arkivanov/decompose/Child$Created;Ljava/util/List;)V
public synthetic fun <init> (Lcom/arkivanov/decompose/Child$Created;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.arkivanov.decompose

internal data class Optional<out T : Any>(val value: T?) {

companion object {
val EMPTY: Optional<Nothing> = Optional(null)
}
}

internal fun <T : Any> optionalOf(value: T? = null): Optional<T> =
if (value == null) Optional.EMPTY else Optional(value)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.arkivanov.decompose.router.overlay

import com.arkivanov.decompose.Child

/**
* A state holder for `Child Overlay`.
*/
data class ChildOverlay<out C : Any, out T : Any>(
val overlay: Child.Created<C, T>? = null,
)
Original file line number Diff line number Diff line change
@@ -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 <C : Parcelable, T : Any> ComponentContext.childOverlay(
source: OverlayNavigationSource<C>,
key: String,
configurationClass: KClass<out C>,
initialConfiguration: () -> C? = { null },
persistent: Boolean = true,
handleBackButton: Boolean = false,
childFactory: (configuration: C, ComponentContext) -> T,
): Value<ChildOverlay<C, T>> =
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 <reified C : Parcelable, T : Any> ComponentContext.childOverlay(
source: OverlayNavigationSource<C>,
key: String,
noinline initialConfiguration: () -> C? = { null },
persistent: Boolean = true,
handleBackButton: Boolean = false,
noinline childFactory: (configuration: C, ComponentContext) -> T,
): Value<ChildOverlay<C, T>> =
childOverlay(
source = source,
key = key,
configurationClass = C::class,
initialConfiguration = initialConfiguration,
persistent = persistent,
handleBackButton = handleBackButton,
childFactory = childFactory,
)

private fun <C : Parcelable, T : Any> Child.Created<Optional<C>, Optional<T>>.toChildOverlay(): ChildOverlay<C, T> =
if ((configuration.value != null) && (instance.value != null)) {
ChildOverlay(
overlay = Child.Created(
configuration = configuration.value,
instance = instance.value,
)
)
} else {
ChildOverlay()
}

private fun <C : Any> configurationStack(configuration: C?): List<Optional<C>> =
listOfNotNull(optionalOf(), configuration?.let(::optionalOf))

private class StackNavigationSourceImpl<C : Parcelable>(
private val delegate: OverlayNavigationSource<C>,
) : StackNavigationSource<Optional<C>> {

private var map = HashMap<(StackNavigationSource.Event<Optional<C>>) -> Unit, (OverlayNavigationSource.Event<C>) -> Unit>()

override fun subscribe(observer: (StackNavigationSource.Event<Optional<C>>) -> Unit) {
check(observer !in map)

val sourceObserver: (OverlayNavigationSource.Event<C>) -> Unit = { observer(it.toStackEvent()) }
map += observer to sourceObserver
delegate.subscribe(sourceObserver)
}

private fun OverlayNavigationSource.Event<C>.toStackEvent(): StackNavigationSource.Event<Optional<C>> =
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<Optional<C>>) -> Unit) {
map.remove(observer)?.also {
delegate.unsubscribe(it)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.arkivanov.decompose.router.overlay

/**
* Represents [OverlayNavigator] and [OverlayNavigationSource] at the same time.
*/
interface OverlayNavigation<C : Any> : OverlayNavigator<C>, OverlayNavigationSource<C>
Original file line number Diff line number Diff line change
@@ -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 <C : Any> OverlayNavigation(): OverlayNavigation<C> = OverlayNavigationImpl()

private class OverlayNavigationImpl<C : Any> : OverlayNavigation<C> {

private val relay = Relay<Event<C>>()

override fun navigate(
transformer: (configuration: C?) -> C?,
onComplete: (newConfiguration: C?, oldConfiguration: C?) -> Unit,
) {
relay.accept(Event(transformer, onComplete))
}

override fun subscribe(observer: (Event<C>) -> Unit) {
relay.subscribe(observer)
}

override fun unsubscribe(observer: (Event<C>) -> Unit) {
relay.unsubscribe(observer)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.arkivanov.decompose.router.overlay

/**
* Represents a source of navigation events for `Child Overlay`.
*
* @see OverlayNavigator
*/
interface OverlayNavigationSource<C : Any> {

fun subscribe(observer: (Event<C>) -> Unit)

fun unsubscribe(observer: (Event<C>) -> Unit)

class Event<C : Any>(
val transformer: (configuration: C?) -> C?,
val onComplete: (newConfiguration: C?, oldConfiguration: C?) -> Unit = { _, _ -> },
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.arkivanov.decompose.router.overlay

interface OverlayNavigator<C : Any> {

/**
* 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,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.arkivanov.decompose.router.overlay

/**
* A convenience method for [OverlayNavigator.navigate].
*/
fun <C : Any> OverlayNavigator<C>.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 <C : Any> OverlayNavigator<C>.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 <C : Any> OverlayNavigator<C>.dismiss(onComplete: (isSuccess: Boolean) -> Unit = {}) {
navigate(transformer = { null }, onComplete = { _, oldConfiguration -> onComplete(oldConfiguration != null) })
}
Loading

0 comments on commit 5f291e3

Please sign in to comment.