Skip to content

Commit

Permalink
Merge pull request #482 from arkivanov/merge-v2.1-into-master
Browse files Browse the repository at this point in the history
Merge v2.1 into master
  • Loading branch information
arkivanov authored Sep 20, 2023
2 parents e5afc98 + abe16af commit 4b7faa7
Show file tree
Hide file tree
Showing 81 changed files with 2,454 additions and 1,073 deletions.
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ plugins {

setupDefaults(
multiplatformConfigurator = {
android()
androidTarget()
jvm()
js(BOTH) { browser() }
js { browser() }
iosCompat()
watchosCompat()
tvosCompat()
Expand Down
18 changes: 13 additions & 5 deletions decompose/api/android/decompose.api
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
public final class com/arkivanov/decompose/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
public fun <init> ()V
public abstract interface class com/arkivanov/decompose/Cancellation {
public abstract fun cancel ()V
}

public abstract class com/arkivanov/decompose/Child {
Expand Down Expand Up @@ -70,6 +67,13 @@ public abstract interface annotation class com/arkivanov/decompose/FaultyDecompo
public abstract interface annotation class com/arkivanov/decompose/InternalDecomposeApi : java/lang/annotation/Annotation {
}

public final class com/arkivanov/decompose/RetainedComponentKt {
public static final fun retainedComponent (Landroidx/activity/ComponentActivity;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static final fun retainedComponent (Landroidx/fragment/app/Fragment;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static synthetic fun retainedComponent$default (Landroidx/activity/ComponentActivity;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
public static synthetic fun retainedComponent$default (Landroidx/fragment/app/Fragment;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
}

public final class com/arkivanov/decompose/UtilsKt {
}

Expand All @@ -87,6 +91,7 @@ public final class com/arkivanov/decompose/router/children/ChildNavState$Status
public static final field ACTIVE Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
public static final field DESTROYED Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
public static final field INACTIVE Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
public static fun values ()[Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
}
Expand Down Expand Up @@ -398,6 +403,7 @@ public final class com/arkivanov/decompose/router/stack/ValueExtKt {

public abstract interface class com/arkivanov/decompose/router/stack/webhistory/WebHistoryController {
public abstract fun attach (Lcom/arkivanov/decompose/router/stack/StackNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
public abstract fun getHistoryPaths ()Ljava/util/List;
}

public abstract class com/arkivanov/decompose/value/MutableValue : com/arkivanov/decompose/value/Value {
Expand All @@ -421,13 +427,15 @@ public final class com/arkivanov/decompose/value/ObserveLifecycleMode : java/lan
public static final field CREATE_DESTROY Lcom/arkivanov/decompose/value/ObserveLifecycleMode;
public static final field RESUME_PAUSE Lcom/arkivanov/decompose/value/ObserveLifecycleMode;
public static final field START_STOP Lcom/arkivanov/decompose/value/ObserveLifecycleMode;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/decompose/value/ObserveLifecycleMode;
public static fun values ()[Lcom/arkivanov/decompose/value/ObserveLifecycleMode;
}

public abstract class com/arkivanov/decompose/value/Value {
public fun <init> ()V
public abstract fun getValue ()Ljava/lang/Object;
public final fun observe (Lkotlin/jvm/functions/Function1;)Lcom/arkivanov/decompose/Cancellation;
public abstract fun subscribe (Lkotlin/jvm/functions/Function1;)V
public abstract fun unsubscribe (Lkotlin/jvm/functions/Function1;)V
}
Expand Down
8 changes: 8 additions & 0 deletions decompose/api/jvm/decompose.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
public abstract interface class com/arkivanov/decompose/Cancellation {
public abstract fun cancel ()V
}

public abstract class com/arkivanov/decompose/Child {
public abstract fun getConfiguration ()Ljava/lang/Object;
public abstract fun getInstance ()Ljava/lang/Object;
Expand Down Expand Up @@ -73,6 +77,7 @@ public final class com/arkivanov/decompose/router/children/ChildNavState$Status
public static final field ACTIVE Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
public static final field DESTROYED Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
public static final field INACTIVE Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
public static fun values ()[Lcom/arkivanov/decompose/router/children/ChildNavState$Status;
}
Expand Down Expand Up @@ -360,6 +365,7 @@ public final class com/arkivanov/decompose/router/stack/ValueExtKt {

public abstract interface class com/arkivanov/decompose/router/stack/webhistory/WebHistoryController {
public abstract fun attach (Lcom/arkivanov/decompose/router/stack/StackNavigator;Lcom/arkivanov/decompose/value/Value;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
public abstract fun getHistoryPaths ()Ljava/util/List;
}

public abstract class com/arkivanov/decompose/value/MutableValue : com/arkivanov/decompose/value/Value {
Expand All @@ -383,13 +389,15 @@ public final class com/arkivanov/decompose/value/ObserveLifecycleMode : java/lan
public static final field CREATE_DESTROY Lcom/arkivanov/decompose/value/ObserveLifecycleMode;
public static final field RESUME_PAUSE Lcom/arkivanov/decompose/value/ObserveLifecycleMode;
public static final field START_STOP Lcom/arkivanov/decompose/value/ObserveLifecycleMode;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/decompose/value/ObserveLifecycleMode;
public static fun values ()[Lcom/arkivanov/decompose/value/ObserveLifecycleMode;
}

public abstract class com/arkivanov/decompose/value/Value {
public fun <init> ()V
public abstract fun getValue ()Ljava/lang/Object;
public final fun observe (Lkotlin/jvm/functions/Function1;)Lcom/arkivanov/decompose/Cancellation;
public abstract fun subscribe (Lkotlin/jvm/functions/Function1;)V
public abstract fun unsubscribe (Lkotlin/jvm/functions/Function1;)V
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package com.arkivanov.decompose

import androidx.activity.BackEventCompat
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.activity.OnBackPressedDispatcher
import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStoreOwner
import androidx.savedstate.SavedStateRegistryOwner
import com.arkivanov.essenty.backhandler.BackHandler
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.arkivanov.essenty.instancekeeper.instanceKeeper
import com.arkivanov.essenty.lifecycle.LifecycleRegistry
import com.arkivanov.essenty.lifecycle.create
import com.arkivanov.essenty.lifecycle.destroy
import com.arkivanov.essenty.lifecycle.essentyLifecycle
import com.arkivanov.essenty.lifecycle.pause
import com.arkivanov.essenty.lifecycle.resume
import com.arkivanov.essenty.lifecycle.start
import com.arkivanov.essenty.lifecycle.stop
import com.arkivanov.essenty.lifecycle.subscribe
import com.arkivanov.essenty.parcelable.ParcelableContainer
import com.arkivanov.essenty.statekeeper.StateKeeperDispatcher
import com.arkivanov.essenty.statekeeper.consume
import com.arkivanov.essenty.statekeeper.stateKeeper

/**
* Returns (creating if needed) a component that is retained over configuration changes.
* This is typically used to create a retained root component.
*
* Please pay attention when supplying dependencies to the component to avoid leaking the `Activity`.
*
* @param key a key of the component, must be unique within the `Activity`.
* @param handleBackButton a flag that determines whether back button handling is enabled or not, default is `true`.
* @param factory a function that returns a new instance of the component.
*/
@ExperimentalDecomposeApi
fun <T> ComponentActivity.retainedComponent(
key: String = "RootRetainedComponent",
handleBackButton: Boolean = true,
factory: (ComponentContext) -> T,
): T =
retainedComponent(
key = key,
onBackPressedDispatcher = if (handleBackButton) onBackPressedDispatcher else null,
isChangingConfigurations = ::isChangingConfigurations,
factory = factory,
)

/**
* Returns (creating if needed) a component that is retained over configuration changes.
* This is typically used to create a retained root component.
*
* Please pay attention when supplying dependencies to the component to avoid leaking the `Fragment`.
*
* @param key a key of the component, must be unique within the `Fragment`.
* @param handleBackButton a flag that determines whether back button handling is enabled or not, default is `true`.
* @param factory a function that returns a new instance of the component.
*/
@ExperimentalDecomposeApi
fun <T> Fragment.retainedComponent(
key: String = "RootRetainedComponent",
handleBackButton: Boolean = true,
factory: (ComponentContext) -> T,
): T =
retainedComponent(
key = key,
onBackPressedDispatcher = if (handleBackButton) requireActivity().onBackPressedDispatcher else null,
isChangingConfigurations = { activity?.isChangingConfigurations ?: false },
factory = factory,
)

private fun <T, O> O.retainedComponent(
key: String,
onBackPressedDispatcher: OnBackPressedDispatcher?,
isChangingConfigurations: () -> Boolean,
factory: (ComponentContext) -> T,
): T where O : LifecycleOwner, O : SavedStateRegistryOwner, O : ViewModelStoreOwner {
val lifecycle = essentyLifecycle()
val stateKeeper = stateKeeper()
val instanceKeeper = instanceKeeper()

check(!stateKeeper.isRegistered(key = key)) { "Another retained component is already registered with the key: $key" }

val holder =
instanceKeeper.getOrCreate(key = key) {
RetainedComponentHolder(
savedState = stateKeeper.consume(key = key),
factory = factory,
)
}

lifecycle.subscribe(
onCreate = { holder.lifecycle.create() },
onStart = { holder.lifecycle.start() },
onResume = { holder.lifecycle.resume() },
onPause = {
if (!isChangingConfigurations()) {
holder.lifecycle.pause()
}
},
onStop = {
if (!isChangingConfigurations()) {
holder.lifecycle.stop()
}
},
onDestroy = {
if (!isChangingConfigurations()) {
holder.lifecycle.destroy()
}
},
)

stateKeeper.register(key = key) { holder.stateKeeper.save() }

if (onBackPressedDispatcher != null) {
val onBackPressedCallback = DelegateOnBackPressedCallback(holder.onBackPressedDispatcher)
holder.onBackEnabledChangedListener = { onBackPressedCallback.isEnabled = it }
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}

return holder.component
}

private class DelegateOnBackPressedCallback(
private val dispatcher: OnBackPressedDispatcher,
) : OnBackPressedCallback(enabled = dispatcher.hasEnabledCallbacks()) {
override fun handleOnBackPressed() {
dispatcher.onBackPressed()
}

override fun handleOnBackStarted(backEvent: BackEventCompat) {
dispatcher.dispatchOnBackStarted(backEvent)
}

override fun handleOnBackProgressed(backEvent: BackEventCompat) {
dispatcher.dispatchOnBackProgressed(backEvent)
}

override fun handleOnBackCancelled() {
dispatcher.dispatchOnBackCancelled()
}
}

private class RetainedComponentHolder<out T>(
savedState: ParcelableContainer?,
factory: (ComponentContext) -> T,
) : InstanceKeeper.Instance {
val lifecycle: LifecycleRegistry = LifecycleRegistry()
val stateKeeper: StateKeeperDispatcher = StateKeeperDispatcher(savedState = savedState)
private val instanceKeeper: InstanceKeeperDispatcher = InstanceKeeperDispatcher()

var onBackEnabledChangedListener: ((Boolean) -> Unit)? = null

val onBackPressedDispatcher: OnBackPressedDispatcher =
OnBackPressedDispatcher(
fallbackOnBackPressed = null,
onHasEnabledCallbacksChanged = { onBackEnabledChangedListener?.invoke(it) },
)

val component: T =
factory(
DefaultComponentContext(
lifecycle = lifecycle,
stateKeeper = stateKeeper,
instanceKeeper = instanceKeeper,
backHandler = BackHandler(onBackPressedDispatcher),
)
)

override fun onDestroy() {
instanceKeeper.destroy()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.arkivanov.decompose

/** A cancellation handle, returned by various functions where cancellation is required. */
fun interface Cancellation {

fun cancel()
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.arkivanov.decompose.backhandler

import com.arkivanov.decompose.isDestroyed
import com.arkivanov.essenty.backhandler.BackCallback
import com.arkivanov.essenty.backhandler.BackHandler
import com.arkivanov.essenty.lifecycle.Lifecycle
import com.arkivanov.essenty.lifecycle.subscribe
Expand All @@ -14,8 +15,11 @@ internal interface ChildBackHandler : BackHandler {
fun stop()
}

internal fun BackHandler.child(lifecycle: Lifecycle? = null): BackHandler {
val handler = childBackHandler(isEnabled = false)
internal fun BackHandler.child(
lifecycle: Lifecycle? = null,
priority: Int = BackCallback.PRIORITY_DEFAULT,
): BackHandler {
val handler = childBackHandler(priority = priority, isEnabled = false)

if (lifecycle == null) {
handler.isEnabled = true
Expand All @@ -34,8 +38,12 @@ internal fun BackHandler.child(lifecycle: Lifecycle? = null): BackHandler {
return handler
}

internal fun BackHandler.childBackHandler(isEnabled: Boolean = true): ChildBackHandler =
internal fun BackHandler.childBackHandler(
isEnabled: Boolean = true,
priority: Int = BackCallback.PRIORITY_DEFAULT,
): ChildBackHandler =
DefaultChildBackHandler(
parent = this,
isEnabled = isEnabled,
priority = priority,
)
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package com.arkivanov.decompose.backhandler

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

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

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

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

Expand Down Expand Up @@ -50,9 +51,24 @@ internal class DefaultChildBackHandler(
parentCallback.isEnabled = isEnabled && set.any(BackCallback::isEnabled)
}

private fun onBack() {
set.lastOrNull(BackCallback::isEnabled)?.also {
it.onBack()
private fun Iterable<BackCallback>.findMostImportant(): BackCallback? =
sortedBy(BackCallback::priority).lastOrNull(BackCallback::isEnabled)

private inner class BackCallbackImpl(priority: Int) : BackCallback(isEnabled = false, priority = priority) {
override fun onBackStarted(backEvent: BackEvent) {
set.findMostImportant()?.onBackStarted(backEvent)
}

override fun onBackProgressed(backEvent: BackEvent) {
set.findMostImportant()?.onBackProgressed(backEvent)
}

override fun onBackCancelled() {
set.findMostImportant()?.onBackCancelled()
}

override fun onBack() {
set.findMostImportant()?.onBack()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ fun <C : Any, T : Any, E : Any, N : NavState<C>, S : Any> ComponentContext.child
retainedInstanceSupplier = { factory -> instanceKeeper.getOrCreate(key = key, factory = factory) },
childItemFactory = DefaultChildItemFactory(
lifecycle = lifecycle,
backHandler = backHandler.child(),
backHandler = backHandler.child(priority = BackCallback.PRIORITY_DEFAULT + 1),
childFactory = childFactory,
),
navState = restoredNavState ?: initialState(),
Expand Down
Loading

0 comments on commit 4b7faa7

Please sign in to comment.