Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Child Overlay #217

Merged
merged 1 commit into from
Oct 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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 <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 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 <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