Skip to content

Commit

Permalink
Disallow destroying child ComponentContext
Browse files Browse the repository at this point in the history
  • Loading branch information
arkivanov committed Sep 28, 2022
1 parent 1ae96ec commit bdbee2f
Show file tree
Hide file tree
Showing 21 changed files with 572 additions and 212 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package com.arkivanov.decompose

/**
* Marks internal declarations in Decompose. Internal declarations must not be used outside the library.
* There are no backward compatibility guarantees between different versions of Decompose.
*/
@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
@Retention(AnnotationRetention.BINARY)
annotation class InternalDecomposeApi

/**
* Marks experimental API in Decompose. An experimental API can be changed or removed at any time.
*/
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalDecomposeApi
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@ import com.arkivanov.decompose.instancekeeper.child
import com.arkivanov.decompose.lifecycle.MergedLifecycle
import com.arkivanov.decompose.statekeeper.child
import com.arkivanov.essenty.lifecycle.Lifecycle
import com.arkivanov.essenty.lifecycle.doOnDestroy

/**
* Creates a new instance of [ComponentContext] and attaches it the parent (`this`) [ComponentContext].
* Creates a new instance of child [ComponentContext] and attaches it to the parent (`this`) [ComponentContext].
*
* @param key A key of the child [ComponentContext], must be unique within the parent [ComponentContext]
* @param lifecycle An optional [Lifecycle] of the child [ComponentContext], can be used if the child component
* needs to be destroyed earlier, or if you need manual control. If supplied, then the following conditions apply:
* - the resulting [Lifecycle] of the child component will honour both the parent (`this`) [Lifecycle] and the supplied one
* - when the supplied [Lifecycle] is explicitly destroyed, the child [ComponentContext] detaches from its parent
* @param key A key of the child [ComponentContext], must be unique within the parent [ComponentContext].
* @param lifecycle An optional [Lifecycle] of the child [ComponentContext] to be merged with the parent [Lifecycle],
* can be used for manual control (see [LifecycleRegistry][com.arkivanov.essenty.lifecycle.LifecycleRegistry]).
* The following conditions apply:
* - The resulting [Lifecycle] of the child component will honour both the parent (`this`) [Lifecycle] and the supplied one.
* - The supplied [Lifecycle] must never be destroyed manually.
*/
fun ComponentContext.childContext(key: String, lifecycle: Lifecycle? = null): ComponentContext =
DefaultComponentContext(
fun ComponentContext.childContext(key: String, lifecycle: Lifecycle? = null): ComponentContext {
lifecycle?.doOnDestroy { error("The lifecycle of a child ComponentContext must never be destroyed manually.") }

return DefaultComponentContext(
lifecycle = if (lifecycle == null) this.lifecycle else MergedLifecycle(this.lifecycle, lifecycle),
stateKeeper = stateKeeper.child(key, lifecycle),
instanceKeeper = instanceKeeper.child(key, lifecycle),
backHandler = backHandler.child(lifecycle),
)
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
package com.arkivanov.decompose

import com.arkivanov.essenty.lifecycle.Lifecycle

internal expect fun Any.ensureNeverFrozen()

internal val Lifecycle.isDestroyed: Boolean get() = state == Lifecycle.State.DESTROYED
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
package com.arkivanov.decompose.backhandler

import com.arkivanov.decompose.isDestroyed
import com.arkivanov.essenty.backhandler.BackHandler
import com.arkivanov.essenty.lifecycle.Lifecycle
import com.arkivanov.essenty.lifecycle.doOnDestroy
import com.arkivanov.essenty.lifecycle.subscribe

internal interface ChildBackHandler : BackHandler {

var isEnabled: Boolean

fun start()

fun stop()
}

internal fun BackHandler.child(lifecycle: Lifecycle?): BackHandler {
val handler = DefaultChildBackHandler(parent = this)
internal fun BackHandler.child(lifecycle: Lifecycle? = null): BackHandler {
val handler = childBackHandler(isEnabled = false)

if (lifecycle == null) {
handler.isEnabled = true
handler.start()
} else if (lifecycle.state != Lifecycle.State.DESTROYED) {
} else if (!lifecycle.isDestroyed) {
handler.isEnabled = lifecycle.state >= Lifecycle.State.STARTED
handler.start()
lifecycle.doOnDestroy(handler::stop)

lifecycle.subscribe(
onStart = { handler.isEnabled = true },
onStop = { handler.isEnabled = false },
onDestroy = handler::stop,
)
}

return handler
}

internal fun BackHandler.child(): ChildBackHandler = DefaultChildBackHandler(parent = this)
internal fun BackHandler.childBackHandler(isEnabled: Boolean = true): ChildBackHandler =
DefaultChildBackHandler(
parent = this,
isEnabled = isEnabled,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ package com.arkivanov.decompose.backhandler

import com.arkivanov.essenty.backhandler.BackCallback
import com.arkivanov.essenty.backhandler.BackHandler
import kotlin.properties.Delegates.observable

internal class DefaultChildBackHandler(
private val parent: BackHandler,
isEnabled: Boolean,
) : ChildBackHandler {

private val parentCallback = BackCallback(isEnabled = false, onBack = ::onBack)
private var set = emptySet<BackCallback>()
private val enabledChangedListener: (Boolean) -> Unit = { onEnabledChanged() }
private val enabledChangedListener: (Boolean) -> Unit = { updateParentCallbackEnabledState() }

override var isEnabled: Boolean by observable(isEnabled) { _, _, _ -> updateParentCallbackEnabledState() }

override fun start() {
parent.register(parentCallback)
Expand All @@ -24,19 +28,19 @@ internal class DefaultChildBackHandler(

this.set += callback
callback.addEnabledChangedListener(enabledChangedListener)
onEnabledChanged()
updateParentCallbackEnabledState()
}

override fun unregister(callback: BackCallback) {
check(callback in set) { "Callback is not registered" }

callback.removeEnabledChangedListener(enabledChangedListener)
this.set -= callback
onEnabledChanged()
updateParentCallbackEnabledState()
}

private fun onEnabledChanged() {
parentCallback.isEnabled = set.any(BackCallback::isEnabled)
private fun updateParentCallbackEnabledState() {
parentCallback.isEnabled = isEnabled && set.any(BackCallback::isEnabled)
}

private fun onBack() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
package com.arkivanov.decompose.instancekeeper

import com.arkivanov.decompose.isDestroyed
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.arkivanov.essenty.lifecycle.Lifecycle
import com.arkivanov.essenty.lifecycle.doOnDestroy

internal fun InstanceKeeper.child(key: String, lifecycle: Lifecycle?): InstanceKeeper {
val registry = getOrCreate(key, ::ChildInstanceKeeperProvider).instanceKeeperRegistry
internal fun InstanceKeeper.child(key: String, lifecycle: Lifecycle? = null): InstanceKeeper =
if ((lifecycle == null) || !lifecycle.isDestroyed) {
val registry = getOrCreate(key, ::ChildInstanceKeeperProvider).instanceKeeperRegistry

lifecycle?.doOnDestroy {
remove(key)?.onDestroy()
}
lifecycle?.doOnDestroy {
remove(key)?.onDestroy()
}

return registry
}
registry
} else {
InstanceKeeperDispatcher()
}

private class ChildInstanceKeeperProvider : InstanceKeeper.Instance {
val instanceKeeperRegistry = InstanceKeeperDispatcher()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ fun <C : Parcelable, T : Any> ComponentContext.childStack(
handleBackButton: Boolean = false,
childFactory: (configuration: C, ComponentContext) -> T,
): Value<ChildStack<C, T>> {
val routerBackHandler = backHandler.takeIf { handleBackButton }?.child(lifecycle = null)
val routerBackHandler = backHandler.takeIf { handleBackButton }?.child()

val routerEntryFactory =
RouterEntryFactoryImpl(
lifecycle = lifecycle,
backHandler = backHandler.child(lifecycle = null),
backHandler = backHandler.child(),
childFactory = childFactory,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.arkivanov.decompose.router.stack

import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.DefaultComponentContext
import com.arkivanov.decompose.backhandler.child
import com.arkivanov.decompose.backhandler.childBackHandler
import com.arkivanov.decompose.lifecycle.MergedLifecycle
import com.arkivanov.essenty.backhandler.BackHandler
import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
Expand All @@ -26,7 +26,7 @@ internal class RouterEntryFactoryImpl<C : Any, out T : Any>(
val mergedLifecycle = MergedLifecycle(lifecycle, componentLifecycleRegistry)
val stateKeeperDispatcher = StateKeeperDispatcher(savedState)
val instanceKeeperRegistry = instanceKeeperDispatcher ?: InstanceKeeperDispatcher()
val backHandler = backHandler.child()
val backHandler = backHandler.childBackHandler()

val component =
childFactory(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
package com.arkivanov.decompose.statekeeper

import com.arkivanov.decompose.isDestroyed
import com.arkivanov.essenty.lifecycle.Lifecycle
import com.arkivanov.essenty.lifecycle.doOnDestroy
import com.arkivanov.essenty.statekeeper.StateKeeper
import com.arkivanov.essenty.statekeeper.StateKeeperDispatcher
import com.arkivanov.essenty.statekeeper.consume

internal fun StateKeeper.child(key: String, lifecycle: Lifecycle?): StateKeeper {
internal fun StateKeeper.child(key: String, lifecycle: Lifecycle? = null): StateKeeper {
check(!isRegistered(key = key)) { "The key \"$key\" is already in use." }

val stateKeeper = StateKeeperDispatcher(consume(key))
register(key, stateKeeper::save)
lifecycle?.doOnDestroy { unregister(key) }

if (lifecycle == null) {
register(key, stateKeeper::save)
} else if (!lifecycle.isDestroyed) {
register(key, stateKeeper::save)
lifecycle.doOnDestroy { unregister(key) }
}

return stateKeeper
}
Loading

0 comments on commit bdbee2f

Please sign in to comment.