From 223dcbebbcfc2aae5b567ca6b7d310169d86ff03 Mon Sep 17 00:00:00 2001 From: Isaac Udy Date: Sun, 28 Apr 2024 15:06:16 +1200 Subject: [PATCH] Update forwarded result handling to resolve several bugs with behaviour when result handling was occurring outside of the same container --- CHANGELOG.md | 2 + .../java/dev/enro/core/NavigationContext.kt | 41 ++- .../core/container/NavigationBackstack.kt | 7 + .../core/container/NavigationContainer.kt | 14 +- .../container/NavigationInstructionFilter.kt | 16 +- .../controller/usecase/AddPendingResult.kt | 3 +- .../ExecuteContainerOperationInstruction.kt | 3 +- .../handle/NavigationHandleViewModel.kt | 15 +- .../java/dev/enro/core/result/EnroResult.kt | 4 + .../result/ForwardingResultInterceptor.kt | 10 +- .../activity/ActivityNavigationContainer.kt | 9 + .../compose/ComposableNavigationResult.kt | 23 +- .../ComposableNavigationContainer.kt | 79 ++++-- .../destination/ComposableDestinationOwner.kt | 4 +- .../compose/rememberNavigationContainer.kt | 34 ++- .../container/FragmentNavigationContainer.kt | 15 +- .../java/dev/enro/test/PresentationTests.kt | 1 - .../compose/ListDetailComposable.kt | 1 - .../application/SelectDestinationRobot.kt | 11 + .../java/dev/enro/test/application/Utility.kt | 8 +- .../results/ComposeEmbeddedResultFlowRobot.kt | 185 +++++++++++++ .../results/ComposeEmbeddedResultFlowTest.kt | 71 +++++ .../application/src/main/AndroidManifest.xml | 1 + .../tests/application/compose/FindContext.kt | 1 - .../results/ComposeEmbeddedResultFlow.kt | 248 ++++++++++++++++++ 25 files changed, 742 insertions(+), 64 deletions(-) create mode 100644 tests/application/src/androidTest/java/dev/enro/test/application/compose/results/ComposeEmbeddedResultFlowRobot.kt create mode 100644 tests/application/src/androidTest/java/dev/enro/test/application/compose/results/ComposeEmbeddedResultFlowTest.kt create mode 100644 tests/application/src/main/java/dev/enro/tests/application/compose/results/ComposeEmbeddedResultFlow.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b5f3a7f..a848926b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ * Added extensions for `findContext` and `findActiveContext` to `NavigationContext` to allow for finding other NavigationContexts from a context reference * Updated `NavigationContainer` to add `getChildContext` which allows finding specific Active/ActivePushed/ActivePresented/Specific contexts from a container reference * Added `instruction` property to `NavigationContext`, and marked `NavigationContext` as `@AdvancedEnroApi` +* Updated `NavigationContext` and `NavigationHandle` to bind each other to allow for easier access to the other from either reference, and to ensure the lazy references are still available while the context is being referenced +* Updated result handling for forwarding results to fix several bugs and improve behaviour (including correctly handling forwarded results through Activities) ## 2.3.0 * Updated NavigationFlow to return from `next` after `onCompleted` is called, rather than continuing to set the backstack from the flow diff --git a/enro-core/src/main/java/dev/enro/core/NavigationContext.kt b/enro-core/src/main/java/dev/enro/core/NavigationContext.kt index 4d9e36f3..f46f3585 100644 --- a/enro-core/src/main/java/dev/enro/core/NavigationContext.kt +++ b/enro-core/src/main/java/dev/enro/core/NavigationContext.kt @@ -26,6 +26,14 @@ import dev.enro.core.container.NavigationContainerManager import dev.enro.core.controller.NavigationController import dev.enro.core.internal.handle.getNavigationHandleViewModel +/** + * NavigationContext represents a context in which navigation can occur. In Android, this may be a Fragment, Activity, or Composable. + * + * When constructing a NavigationContext, the contextReference is the actual object that the NavigationContext represents + * (e.g. a Fragment, Activity or Composable), and the other parameters are functions that can be used to retrieve information + * about the context. The get functions are invoked lazily, either when the are accessed for the first time, + * or once the NavigationContext is bound to a NavigationHandle. + */ public class NavigationContext internal constructor( public val contextReference: ContextType, private val getController: () -> NavigationController, @@ -35,8 +43,8 @@ public class NavigationContext internal constructor( private val getSavedStateRegistryOwner: () -> SavedStateRegistryOwner, private val getLifecycleOwner: () -> LifecycleOwner, ) { - public val controller: NavigationController get() = getController() - public val parentContext: NavigationContext<*>? get() = getParentContext() + public val controller: NavigationController by lazy { getController() } + public val parentContext: NavigationContext<*>? by lazy { getParentContext() } /** * The arguments provided to this NavigationContext. It is possible to read the open instruction from these arguments, @@ -47,15 +55,34 @@ public class NavigationContext internal constructor( * Generally it should be preferred to read the instruction property, rather than read the instruction from the arguments. */ @AdvancedEnroApi - public val arguments: Bundle get() = getArguments() + public val arguments: Bundle by lazy { getArguments() } - public val instruction: NavigationInstruction.Open<*> by lazy { getNavigationHandle().instruction } - public val viewModelStoreOwner: ViewModelStoreOwner get() = getViewModelStoreOwner() - public val savedStateRegistryOwner: SavedStateRegistryOwner get() = getSavedStateRegistryOwner() - public val lifecycleOwner: LifecycleOwner get() = getLifecycleOwner() + private lateinit var _instruction: NavigationInstruction.Open<*> + public val instruction: NavigationInstruction.Open<*> get() = _instruction + + public val viewModelStoreOwner: ViewModelStoreOwner by lazy { getViewModelStoreOwner() } + public val savedStateRegistryOwner: SavedStateRegistryOwner by lazy { getSavedStateRegistryOwner() } + public val lifecycleOwner: LifecycleOwner by lazy { getLifecycleOwner() } public val lifecycle: Lifecycle get() = lifecycleOwner.lifecycle public val containerManager: NavigationContainerManager = NavigationContainerManager() + + private var _navigationHandle: NavigationHandle? = null + public val navigationHandle: NavigationHandle get() = requireNotNull(_navigationHandle) + + internal fun bind(navigationHandle: NavigationHandle) { + _navigationHandle = navigationHandle + _instruction = navigationHandle.instruction + + // Invoke hashcode on all lazy items to ensure they are initialized + + controller.hashCode() + parentContext.hashCode() + arguments.hashCode() + viewModelStoreOwner.hashCode() + savedStateRegistryOwner.hashCode() + lifecycleOwner.hashCode() + } } public val NavigationContext.fragment: Fragment get() = contextReference diff --git a/enro-core/src/main/java/dev/enro/core/container/NavigationBackstack.kt b/enro-core/src/main/java/dev/enro/core/container/NavigationBackstack.kt index 184b2964..3834aa66 100644 --- a/enro-core/src/main/java/dev/enro/core/container/NavigationBackstack.kt +++ b/enro-core/src/main/java/dev/enro/core/container/NavigationBackstack.kt @@ -4,6 +4,7 @@ import android.os.Parcelable import dev.enro.core.AnyOpenInstruction import dev.enro.core.EnroException import dev.enro.core.NavigationContext +import dev.enro.core.NavigationDirection import dev.enro.core.controller.interceptor.InstructionOpenedByInterceptor import kotlinx.parcelize.Parcelize @@ -11,6 +12,12 @@ import kotlinx.parcelize.Parcelize @Parcelize public value class NavigationBackstack(private val backstack: List) : List by backstack, Parcelable { public val active: AnyOpenInstruction? get() = lastOrNull() + + public val activePushed: AnyOpenInstruction? get() = lastOrNull { it.navigationDirection == NavigationDirection.Push } + + public val activePresented: AnyOpenInstruction? get() = takeWhile { it.navigationDirection != NavigationDirection.Push } + .lastOrNull { it.navigationDirection == NavigationDirection.Push } + internal val identity get() = System.identityHashCode(backstack) } diff --git a/enro-core/src/main/java/dev/enro/core/container/NavigationContainer.kt b/enro-core/src/main/java/dev/enro/core/container/NavigationContainer.kt index 3e4d7e14..b142bd5d 100644 --- a/enro-core/src/main/java/dev/enro/core/container/NavigationContainer.kt +++ b/enro-core/src/main/java/dev/enro/core/container/NavigationContainer.kt @@ -34,6 +34,7 @@ import dev.enro.core.getNavigationHandle import dev.enro.core.leafContext import dev.enro.core.parentContainer import dev.enro.core.requestClose +import dev.enro.core.result.EnroResult import dev.enro.core.rootContext import dev.enro.extensions.getParcelableListCompat import kotlinx.coroutines.Job @@ -126,13 +127,14 @@ public abstract class NavigationContainer( if (Looper.myLooper() != Looper.getMainLooper()) throw EnroException.NavigationContainerWrongThread( "A NavigationContainer's setBackstack method must only be called from the main thread" ) - if (backstack == backstackFlow.value) return@synchronized renderJob?.cancel() val processedBackstack = Compatibility.NavigationContainer .processBackstackForDeprecatedInstructionTypes(backstack, instructionFilter) + .filterBackstackForForwardedResults() .ensureOpeningTypeIsSet(context) .processBackstackForPreviouslyActiveContainer() + if (processedBackstack == backstackFlow.value) return@synchronized if (handleEmptyBehaviour(processedBackstack)) return val lastBackstack = mutableBackstack mutableBackstack = processedBackstack @@ -263,6 +265,16 @@ public abstract class NavigationContainer( }.toBackstack() } + // When using result forwarding, a NavigationContainer can be restored (or otherwise have the backstack set) for + // instructions that have a pending result already applied in the EnroResult result manager. In these cases, + // we want to filter out the instructions that already have a result applied. This is to ensure that when result + // forwarding happens across multiple containers, all destinations providing the result are closed, even if those + // destinations aren't visible/active when the forwarded result is added to EnroResult. + private fun NavigationBackstack.filterBackstackForForwardedResults(): NavigationBackstack { + val enroResult = EnroResult.from(context.controller) + return filter { !enroResult.hasPendingResultFrom(it) }.toBackstack() + } + public sealed class ContextFilter { public data object Active : ContextFilter() public data object ActivePresented : ContextFilter() diff --git a/enro-core/src/main/java/dev/enro/core/container/NavigationInstructionFilter.kt b/enro-core/src/main/java/dev/enro/core/container/NavigationInstructionFilter.kt index f0825a7e..fc50e4d8 100644 --- a/enro-core/src/main/java/dev/enro/core/container/NavigationInstructionFilter.kt +++ b/enro-core/src/main/java/dev/enro/core/container/NavigationInstructionFilter.kt @@ -21,6 +21,14 @@ public class NavigationContainerFilterBuilder internal constructor() { filters.add(NavigationInstructionFilter { predicate(it.navigationKey) }) } + public fun key(key: NavigationKey) { + key { it == key } + } + + public inline fun key() { + key { it is T } + } + public fun instruction(predicate: (NavigationInstruction.Open<*>) -> Boolean) { filters.add(NavigationInstructionFilter(predicate)) } @@ -32,14 +40,6 @@ public class NavigationContainerFilterBuilder internal constructor() { } } -public fun NavigationContainerFilterBuilder.key(key: NavigationKey) { - key { it == key } -} - -public inline fun NavigationContainerFilterBuilder.key() { - key { it is T } -} - /** * A [NavigationInstructionFilter] that accepts all [NavigationInstruction.Open] instructions. */ diff --git a/enro-core/src/main/java/dev/enro/core/controller/usecase/AddPendingResult.kt b/enro-core/src/main/java/dev/enro/core/controller/usecase/AddPendingResult.kt index 5d33ec8a..5e01f3a9 100644 --- a/enro-core/src/main/java/dev/enro/core/controller/usecase/AddPendingResult.kt +++ b/enro-core/src/main/java/dev/enro/core/controller/usecase/AddPendingResult.kt @@ -4,7 +4,6 @@ import dev.enro.core.NavigationContext import dev.enro.core.NavigationInstruction import dev.enro.core.NavigationKey import dev.enro.core.controller.NavigationController -import dev.enro.core.readOpenInstruction import dev.enro.core.result.AdvancedResultExtensions import dev.enro.core.result.EnroResult import dev.enro.core.result.internal.PendingResult @@ -18,7 +17,7 @@ internal class AddPendingResult( navigationContext: NavigationContext<*>, instruction: NavigationInstruction.Close ) { - val openInstruction = navigationContext.arguments.readOpenInstruction() ?: return + val openInstruction = navigationContext.instruction val navigationKey = openInstruction.internal.resultKey ?: openInstruction.navigationKey diff --git a/enro-core/src/main/java/dev/enro/core/controller/usecase/ExecuteContainerOperationInstruction.kt b/enro-core/src/main/java/dev/enro/core/controller/usecase/ExecuteContainerOperationInstruction.kt index e6278bd8..85337967 100644 --- a/enro-core/src/main/java/dev/enro/core/controller/usecase/ExecuteContainerOperationInstruction.kt +++ b/enro-core/src/main/java/dev/enro/core/controller/usecase/ExecuteContainerOperationInstruction.kt @@ -4,7 +4,6 @@ import dev.enro.core.NavigationContext import dev.enro.core.NavigationInstruction import dev.enro.core.findContainer import dev.enro.core.parentContainer -import dev.enro.core.readOpenInstruction internal interface ExecuteContainerOperationInstruction { operator fun invoke( @@ -29,7 +28,7 @@ internal class ExecuteContainerOperationInstructionImpl(): ExecuteContainerOpera NavigationInstruction.ContainerOperation.Target.ActiveContainer -> "ActiveContainer" is NavigationInstruction.ContainerOperation.Target.TargetContainer -> "TargetContainer(${instruction.target.key})" } - val contextKeyName = navigationContext.arguments.readOpenInstruction()!!.navigationKey::class.java.simpleName + val contextKeyName = navigationContext.instruction.navigationKey::class.java.simpleName "Failed to perform container instruction for $targetName in context with key $contextKeyName: Could not find valid container to perform instruction on" } instruction.operation.invoke(container) diff --git a/enro-core/src/main/java/dev/enro/core/internal/handle/NavigationHandleViewModel.kt b/enro-core/src/main/java/dev/enro/core/internal/handle/NavigationHandleViewModel.kt index 0cf61170..b88010ac 100644 --- a/enro-core/src/main/java/dev/enro/core/internal/handle/NavigationHandleViewModel.kt +++ b/enro-core/src/main/java/dev/enro/core/internal/handle/NavigationHandleViewModel.kt @@ -4,8 +4,18 @@ import android.annotation.SuppressLint import android.os.Looper import androidx.activity.ComponentActivity import androidx.fragment.app.Fragment -import androidx.lifecycle.* -import dev.enro.core.* +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.ViewModel +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.withStarted +import dev.enro.core.AnyOpenInstruction +import dev.enro.core.NavigationContext +import dev.enro.core.NavigationHandle +import dev.enro.core.NavigationInstruction +import dev.enro.core.NavigationKey +import dev.enro.core.close import dev.enro.core.compose.ComposableDestination import dev.enro.core.controller.usecase.ExecuteCloseInstruction import dev.enro.core.controller.usecase.ExecuteContainerOperationInstruction @@ -44,6 +54,7 @@ internal open class NavigationHandleViewModel( field = value if (value == null) return + value.bind(this) registerLifecycleObservers(value) executePendingInstruction() diff --git a/enro-core/src/main/java/dev/enro/core/result/EnroResult.kt b/enro-core/src/main/java/dev/enro/core/result/EnroResult.kt index cba3b357..e7a2a25d 100644 --- a/enro-core/src/main/java/dev/enro/core/result/EnroResult.kt +++ b/enro-core/src/main/java/dev/enro/core/result/EnroResult.kt @@ -1,5 +1,6 @@ package dev.enro.core.result +import dev.enro.core.AnyOpenInstruction import dev.enro.core.EnroException import dev.enro.core.NavigationHandle import dev.enro.core.controller.NavigationController @@ -38,6 +39,9 @@ internal class EnroResult: EnroPlugin() { } } + internal fun hasPendingResultFrom(instruction: AnyOpenInstruction): Boolean { + return pendingResults[instruction.internal.resultId] != null + } private fun consumePendingResult(resultChannelId: ResultChannelId): PendingResult? { val result = pendingResults[resultChannelId] ?: return null diff --git a/enro-core/src/main/java/dev/enro/core/result/ForwardingResultInterceptor.kt b/enro-core/src/main/java/dev/enro/core/result/ForwardingResultInterceptor.kt index c45f4ae6..3b5d8ea6 100644 --- a/enro-core/src/main/java/dev/enro/core/result/ForwardingResultInterceptor.kt +++ b/enro-core/src/main/java/dev/enro/core/result/ForwardingResultInterceptor.kt @@ -1,10 +1,17 @@ package dev.enro.core.result -import dev.enro.core.* +import dev.enro.core.NavigationContext +import dev.enro.core.NavigationInstruction +import dev.enro.core.NavigationKey +import dev.enro.core.activity +import dev.enro.core.activity.ActivityNavigationContainer import dev.enro.core.container.toBackstack import dev.enro.core.controller.get import dev.enro.core.controller.interceptor.NavigationInstructionInterceptor import dev.enro.core.controller.usecase.AddPendingResult +import dev.enro.core.navigationContext +import dev.enro.core.readOpenInstruction +import dev.enro.core.rootContext internal object ForwardingResultInterceptor : NavigationInstructionInterceptor { override fun intercept( @@ -23,6 +30,7 @@ internal object ForwardingResultInterceptor : NavigationInstructionInterceptor val containers = context.rootContext() .containerManager .containers + .plus(ActivityNavigationContainer(context.activity.navigationContext)) .toMutableList() while (containers.isNotEmpty()) { diff --git a/enro-core/src/main/java/dev/enro/destination/activity/ActivityNavigationContainer.kt b/enro-core/src/main/java/dev/enro/destination/activity/ActivityNavigationContainer.kt index 7186f637..5f4dbbb8 100644 --- a/enro-core/src/main/java/dev/enro/destination/activity/ActivityNavigationContainer.kt +++ b/enro-core/src/main/java/dev/enro/destination/activity/ActivityNavigationContainer.kt @@ -20,6 +20,7 @@ import dev.enro.core.container.backstackOf import dev.enro.core.controller.get import dev.enro.core.controller.usecase.GetNavigationBinding import dev.enro.core.controller.usecase.HostInstructionAs +import dev.enro.core.result.EnroResult internal class ActivityNavigationContainer internal constructor( activityContext: NavigationContext, @@ -47,6 +48,14 @@ internal class ActivityNavigationContainer internal constructor( } override fun onBackstackUpdated(transition: NavigationBackstackTransition): Boolean { + // When the backstack is updated, we need to check if there are pending results and close + // immediately to ensure forwarding results work correctly + val result = EnroResult.from(context.controller) + if (result.hasPendingResultFrom(context.instruction)) { + context.activity.finish() + return true + } + if (transition.activeBackstack.singleOrNull()?.instructionId == rootInstruction.instructionId) return true val childContext = requireNotNull(childContext) setBackstack(backstackOf(rootInstruction)) diff --git a/enro-core/src/main/java/dev/enro/destination/compose/ComposableNavigationResult.kt b/enro-core/src/main/java/dev/enro/destination/compose/ComposableNavigationResult.kt index 4b832aaf..59b09c42 100644 --- a/enro-core/src/main/java/dev/enro/destination/compose/ComposableNavigationResult.kt +++ b/enro-core/src/main/java/dev/enro/destination/compose/ComposableNavigationResult.kt @@ -5,10 +5,12 @@ import androidx.compose.runtime.DisallowComposableCalls import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver import dev.enro.core.NavigationKey import dev.enro.core.controller.usecase.createResultChannel import dev.enro.core.result.NavigationResultChannel -import java.util.* +import java.util.UUID @Composable @@ -38,8 +40,25 @@ public inline fun registerForNavigationResult( } DisposableEffect(true) { - resultChannel.attach() + // In some cases, particularly with navigation to Activities, + // Composables aren't actually called through to onDispose, meaning the + // result channel sticks around as being "active" even though the associated + // activity is not started. We're adding a lifecycle observer here to ensure this + // is managed correctly. + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START) { + resultChannel.attach() + } + if (event == Lifecycle.Event.ON_STOP) { + resultChannel.detach() + } + } + navigationHandle.lifecycle.addObserver(observer) + if (navigationHandle.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { + resultChannel.attach() + } onDispose { + navigationHandle.lifecycle.removeObserver(observer) resultChannel.detach() } } diff --git a/enro-core/src/main/java/dev/enro/destination/compose/container/ComposableNavigationContainer.kt b/enro-core/src/main/java/dev/enro/destination/compose/container/ComposableNavigationContainer.kt index 710fb4e3..9898393d 100644 --- a/enro-core/src/main/java/dev/enro/destination/compose/container/ComposableNavigationContainer.kt +++ b/enro-core/src/main/java/dev/enro/destination/compose/container/ComposableNavigationContainer.kt @@ -10,6 +10,9 @@ import androidx.compose.runtime.key import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle @@ -48,7 +51,6 @@ public class ComposableNavigationContainer internal constructor( emptyBehavior: EmptyBehavior, interceptor: NavigationInterceptorBuilder.() -> Unit, animations: NavigationAnimationOverrideBuilder.() -> Unit, - initialBackstack: NavigationBackstack, ) : NavigationContainer( key = key, context = parentContext, @@ -100,10 +102,6 @@ public class ComposableNavigationContainer internal constructor( } } - init { - restoreOrSetBackstack(initialBackstack) - } - public override fun save(): Bundle { val savedState = super.save() destinationOwners @@ -126,29 +124,28 @@ public class ComposableNavigationContainer internal constructor( restoredDestinationState[instructionId] = restoredState } super.restore(bundle) + + // After the backstack has been set, we're going to remove the restored states which aren't in the backstack + val instructionsInBackstack = backstack.map { it.instructionId }.toSet() + restoredDestinationState.keys.minus(instructionsInBackstack).forEach { + restoredDestinationState.remove(it) + } } override fun getChildContext(contextFilter: ContextFilter): NavigationContext<*>? { return when (contextFilter) { is ContextFilter.Active -> destinationOwners - .lastOrNull { it.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED) } + .lastOrNull { it.instruction == backstack.active } ?.destination ?.context is ContextFilter.ActivePushed -> destinationOwners - .lastOrNull { - it.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED) && - it.instruction.navigationDirection == NavigationDirection.Push - } + .lastOrNull { it.instruction == backstack.activePushed } ?.destination ?.context is ContextFilter.ActivePresented -> destinationOwners - .takeLastWhile { it.instruction.navigationDirection != NavigationDirection.Push } - .lastOrNull { - it.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED) && - it.instruction.navigationDirection == NavigationDirection.Push - } + .lastOrNull { it.instruction == backstack.activePresented } ?.destination ?.context @@ -285,17 +282,42 @@ public class ComposableNavigationContainer internal constructor( @Composable internal fun registerWithContainerManager( - registrationStrategy: ContainerRegistrationStrategy + registrationStrategy: ContainerRegistrationStrategy, + initialBackstack: NavigationBackstack, ): Boolean { val registration = remember(key, registrationStrategy) { val containerManager = context.containerManager containerManager.addContainer(this@ComposableNavigationContainer) Closeable { destroy() } } + + rememberSaveable ( + init = { + if (currentTransition === initialTransition) { + restoreOrSetBackstack(initialBackstack) + } + mutableStateOf(Unit) + }, + stateSaver = object : Saver { + override fun restore(value: Bundle) = when(registrationStrategy) { + ContainerRegistrationStrategy.DisposeWithComposition -> this@ComposableNavigationContainer.restore(value) + ContainerRegistrationStrategy.DisposeWithCompositionDoNotSave -> Unit + ContainerRegistrationStrategy.DisposeWithLifecycle -> Unit + } + + override fun SaverScope.save(value: Unit): Bundle? = when(registrationStrategy) { + ContainerRegistrationStrategy.DisposeWithComposition -> this@ComposableNavigationContainer.save() + ContainerRegistrationStrategy.DisposeWithCompositionDoNotSave -> null + ContainerRegistrationStrategy.DisposeWithLifecycle -> null + } + } + ) + DisposableEffect(key, registrationStrategy) { onDispose { when (registrationStrategy) { ContainerRegistrationStrategy.DisposeWithComposition -> registration.close() + ContainerRegistrationStrategy.DisposeWithCompositionDoNotSave -> registration.close() ContainerRegistrationStrategy.DisposeWithLifecycle -> {} // handled by init } } @@ -316,6 +338,7 @@ public class ComposableNavigationContainer internal constructor( val lifecycleObserver = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME || event == Lifecycle.Event.ON_PAUSE) { setVisibilityForBackstack(NavigationBackstackTransition(backstack to backstack)) + setBackstack(backstack) } } context.lifecycle.addObserver(lifecycleObserver) @@ -329,8 +352,32 @@ public class ComposableNavigationContainer internal constructor( } } +/** + * The ContainerRegistrationStrategy defines how ComposableNavigationContainers are managed within the context + * of a Composable function. This is used to determine when the container should destroy child destinations (and associated + * resources such as ViewModels) and when the container should save and restore its state. + * + * By default, containers with dynamic NavigationContainerKeys use DisposeWithComposition, and containers with defined keys + * are managed with DisposeWithLifecycle. + * + * DisposeWithLifecycle will keep a container active while the parent lifecycle is active. This means that ViewModels and other + * resources will be kept alive, even if the container is not currently being rendered within the composition. + * + * DisposeWithComposition will keep a container active only while the container is in the composition, but will save the container's + * state using the Composable rememberSaveable. This means that ViewModels and other resources will be destroyed when the + * container is removed from the composition, but that when the container returns to the composition, it's state should be restored. + * + * DisposeWithCompositionDoNotSave will keep a container active only while the container is in the composition, and will not + * save the container's state. This means that ViewModels and other resources will be destroyed when the container is removed from + * the composition, and that when the container returns to the composition, it's state will not be restored. This behaviour + * should be used only in advanced cases where multiple dynamic navigation containers are required, and there is some other + * state saving management defined in application code using NavigationContainer's save/restore functions. + * + * This is an Advanced Enro API, and should only be used in cases where you are sure that you want to change the default behavior. + */ @AdvancedEnroApi public sealed interface ContainerRegistrationStrategy { public data object DisposeWithComposition : ContainerRegistrationStrategy + public data object DisposeWithCompositionDoNotSave : ContainerRegistrationStrategy public data object DisposeWithLifecycle : ContainerRegistrationStrategy } \ No newline at end of file diff --git a/enro-core/src/main/java/dev/enro/destination/compose/destination/ComposableDestinationOwner.kt b/enro-core/src/main/java/dev/enro/destination/compose/destination/ComposableDestinationOwner.kt index 5e1f5cff..2add9e3a 100644 --- a/enro-core/src/main/java/dev/enro/destination/compose/destination/ComposableDestinationOwner.kt +++ b/enro-core/src/main/java/dev/enro/destination/compose/destination/ComposableDestinationOwner.kt @@ -56,6 +56,7 @@ import dev.enro.core.controller.usecase.OnNavigationContextCreated import dev.enro.core.controller.usecase.OnNavigationContextSaved import dev.enro.core.getNavigationHandle import dev.enro.extensions.rememberLifecycleState +import java.lang.ref.WeakReference internal class ComposableDestinationOwner( parentContainer: NavigationContainer, @@ -76,7 +77,8 @@ internal class ComposableDestinationOwner( internal lateinit var transition: Transition internal var animationOverride by mutableStateOf(null) private var _parentContainer: NavigationContainer? = parentContainer - internal val parentContainer get() = _parentContainer!! + private var weakParentContainerReference: WeakReference = WeakReference(parentContainer) + internal val parentContainer get() = weakParentContainerReference.get() ?: _parentContainer!! @SuppressLint("StaticFieldLeak") @Suppress("LeakingThis") diff --git a/enro-core/src/main/java/dev/enro/destination/compose/rememberNavigationContainer.kt b/enro-core/src/main/java/dev/enro/destination/compose/rememberNavigationContainer.kt index 361640f0..173779a9 100644 --- a/enro-core/src/main/java/dev/enro/destination/compose/rememberNavigationContainer.kt +++ b/enro-core/src/main/java/dev/enro/destination/compose/rememberNavigationContainer.kt @@ -30,6 +30,12 @@ public fun rememberNavigationContainer( interceptor: NavigationInterceptorBuilder.() -> Unit = {}, animations: NavigationAnimationOverrideBuilder.() -> Unit = {}, filter: NavigationInstructionFilter = acceptAll(), + registrationStrategy: ContainerRegistrationStrategy = remember(key) { + when(key) { + is NavigationContainerKey.Dynamic -> ContainerRegistrationStrategy.DisposeWithComposition + else -> ContainerRegistrationStrategy.DisposeWithLifecycle + } + } ): ComposableNavigationContainer { return rememberNavigationContainer( key = key, @@ -39,7 +45,8 @@ public fun rememberNavigationContainer( emptyBehavior = emptyBehavior, interceptor = interceptor, animations = animations, - filter = filter + filter = filter, + registrationStrategy = registrationStrategy, ) } @@ -51,6 +58,12 @@ public fun rememberNavigationContainer( interceptor: NavigationInterceptorBuilder.() -> Unit = {}, animations: NavigationAnimationOverrideBuilder.() -> Unit = {}, filter: NavigationInstructionFilter = acceptAll(), + registrationStrategy: ContainerRegistrationStrategy = remember(key) { + when(key) { + is NavigationContainerKey.Dynamic -> ContainerRegistrationStrategy.DisposeWithComposition + else -> ContainerRegistrationStrategy.DisposeWithLifecycle + } + } ): ComposableNavigationContainer { return rememberNavigationContainer( key = key, @@ -62,7 +75,8 @@ public fun rememberNavigationContainer( emptyBehavior = emptyBehavior, interceptor = interceptor, animations = animations, - filter = filter + filter = filter, + registrationStrategy = registrationStrategy, ) } @@ -75,6 +89,12 @@ public fun rememberNavigationContainer( interceptor: NavigationInterceptorBuilder.() -> Unit = {}, animations: NavigationAnimationOverrideBuilder.() -> Unit = {}, filter: NavigationInstructionFilter = acceptAll(), + registrationStrategy: ContainerRegistrationStrategy = remember(key) { + when(key) { + is NavigationContainerKey.Dynamic -> ContainerRegistrationStrategy.DisposeWithComposition + else -> ContainerRegistrationStrategy.DisposeWithLifecycle + } + } ): ComposableNavigationContainer { val localNavigationHandle = navigationHandle() val context = LocalContext.current @@ -96,16 +116,8 @@ public fun rememberNavigationContainer( emptyBehavior = emptyBehavior, interceptor = interceptor, animations = animations, - initialBackstack = initialBackstack, ) } - - navigationContainer.registerWithContainerManager( - when(key) { - is NavigationContainerKey.Dynamic -> ContainerRegistrationStrategy.DisposeWithComposition - is NavigationContainerKey.FromId -> ContainerRegistrationStrategy.DisposeWithLifecycle - is NavigationContainerKey.FromName -> ContainerRegistrationStrategy.DisposeWithLifecycle - } - ) + navigationContainer.registerWithContainerManager(registrationStrategy, initialBackstack) return navigationContainer } diff --git a/enro-core/src/main/java/dev/enro/destination/fragment/container/FragmentNavigationContainer.kt b/enro-core/src/main/java/dev/enro/destination/fragment/container/FragmentNavigationContainer.kt index 0f85ab18..7ee8b76b 100644 --- a/enro-core/src/main/java/dev/enro/destination/fragment/container/FragmentNavigationContainer.kt +++ b/enro-core/src/main/java/dev/enro/destination/fragment/container/FragmentNavigationContainer.kt @@ -108,22 +108,27 @@ public class FragmentNavigationContainer internal constructor( } ownedFragments.addAll(bundle.getStringArrayList(OWNED_FRAGMENTS_KEY).orEmpty()) super.restore(bundle) + + // After the backstack has been set, we're going to remove the restored states which aren't in the backstack + val instructionsInBackstack = backstack.map { it.instructionId }.toSet() + restoredFragmentStates.keys.minus(instructionsInBackstack).forEach { + restoredFragmentStates.remove(it) + } } override fun getChildContext(contextFilter: ContextFilter): NavigationContext<*>? { val fragment = when(contextFilter) { is ContextFilter.Active -> { - backstack.lastOrNull()?.let { fragmentManager.findFragmentByTag(it.instructionId) } + backstack.active + ?.let { fragmentManager.findFragmentByTag(it.instructionId) } ?: fragmentManager.findFragmentById(containerId) } is ContextFilter.ActivePushed -> { - backstack - .lastOrNull { it.navigationDirection == NavigationDirection.Push } + backstack.activePushed ?.let { fragmentManager.findFragmentByTag(it.instructionId) } } is ContextFilter.ActivePresented -> { - backstack.takeLastWhile { it.navigationDirection != NavigationDirection.Push } - .lastOrNull { it.navigationDirection == NavigationDirection.Present } + backstack.activePresented ?.let { fragmentManager.findFragmentByTag(it.instructionId) } } is ContextFilter.WithId -> { diff --git a/enro/src/androidTest/java/dev/enro/test/PresentationTests.kt b/enro/src/androidTest/java/dev/enro/test/PresentationTests.kt index c917b03c..c757c9df 100644 --- a/enro/src/androidTest/java/dev/enro/test/PresentationTests.kt +++ b/enro/src/androidTest/java/dev/enro/test/PresentationTests.kt @@ -25,7 +25,6 @@ import dev.enro.core.compose.rememberNavigationContainer import dev.enro.core.container.EmptyBehavior import dev.enro.core.container.acceptKey import dev.enro.core.container.doNotAccept -import dev.enro.core.container.key import dev.enro.core.directParentContainer import dev.enro.core.fragment.container.navigationContainer import dev.enro.core.getNavigationHandle diff --git a/example/src/main/java/dev/enro/example/destinations/listdetail/compose/ListDetailComposable.kt b/example/src/main/java/dev/enro/example/destinations/listdetail/compose/ListDetailComposable.kt index 737d9ca4..513004a7 100644 --- a/example/src/main/java/dev/enro/example/destinations/listdetail/compose/ListDetailComposable.kt +++ b/example/src/main/java/dev/enro/example/destinations/listdetail/compose/ListDetailComposable.kt @@ -16,7 +16,6 @@ import dev.enro.core.NavigationKey import dev.enro.core.compose.rememberNavigationContainer import dev.enro.core.container.EmptyBehavior import dev.enro.core.container.accept -import dev.enro.core.container.key import kotlinx.parcelize.Parcelize @Parcelize diff --git a/tests/application/src/androidTest/java/dev/enro/test/application/SelectDestinationRobot.kt b/tests/application/src/androidTest/java/dev/enro/test/application/SelectDestinationRobot.kt index 28af398f..bf85a20c 100644 --- a/tests/application/src/androidTest/java/dev/enro/test/application/SelectDestinationRobot.kt +++ b/tests/application/src/androidTest/java/dev/enro/test/application/SelectDestinationRobot.kt @@ -13,6 +13,7 @@ import dev.enro.test.application.compose.BottomSheetCloseAndPresentRobot import dev.enro.test.application.compose.FindContextRobot import dev.enro.test.application.compose.LegacyBottomSheetsRobot import dev.enro.test.application.compose.SyntheticViewModelAccessRobot +import dev.enro.test.application.compose.results.ComposeEmbeddedResultFlowRobot import dev.enro.test.application.fragment.UnboundBottomSheetRobot import dev.enro.tests.application.SelectDestination @@ -106,4 +107,14 @@ class SelectDestinationRobot( return FindContextRobot(composeRule) } + + fun openComposeEmbeddedResultFlow(): ComposeEmbeddedResultFlowRobot { + composeRule.onNode(hasText("Compose Embedded Result Flow")) + .performScrollTo() + .onSiblings() + .filterToOne(hasText("Push")) + .performClick() + + return ComposeEmbeddedResultFlowRobot(composeRule) + } } \ No newline at end of file diff --git a/tests/application/src/androidTest/java/dev/enro/test/application/Utility.kt b/tests/application/src/androidTest/java/dev/enro/test/application/Utility.kt index 953e0d41..4efc7b31 100644 --- a/tests/application/src/androidTest/java/dev/enro/test/application/Utility.kt +++ b/tests/application/src/androidTest/java/dev/enro/test/application/Utility.kt @@ -19,9 +19,11 @@ fun ComposeTestRule.waitForNavigationHandle( ): NavigationHandle { var navigationHandle: NavigationHandle? = null waitUntil(5_000) { - val activity = runOnUiThread { - ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED) - .singleOrNull() as? ComponentActivity + val activity = runOnIdle { + runOnUiThread { + ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED) + .singleOrNull() as? ComponentActivity + } } ?: return@waitUntil false var activeContext: NavigationContext<*>? = activity.navigationContext diff --git a/tests/application/src/androidTest/java/dev/enro/test/application/compose/results/ComposeEmbeddedResultFlowRobot.kt b/tests/application/src/androidTest/java/dev/enro/test/application/compose/results/ComposeEmbeddedResultFlowRobot.kt new file mode 100644 index 00000000..7455d835 --- /dev/null +++ b/tests/application/src/androidTest/java/dev/enro/test/application/compose/results/ComposeEmbeddedResultFlowRobot.kt @@ -0,0 +1,185 @@ +package dev.enro.test.application.compose.results + +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import dev.enro.test.application.waitForNavigationHandle +import dev.enro.tests.application.compose.results.ComposeEmbeddedResultFlow + +class ComposeEmbeddedResultFlowRobot( + private val composeRule: ComposeTestRule +) { + init { + composeRule.waitForNavigationHandle { + it.key is ComposeEmbeddedResultFlow + } + } + + fun getRoot(): RootRobot { + return RootRobot() + } + + inner class RootRobot() { + init { + composeRule.waitForNavigationHandle { + it.key is ComposeEmbeddedResultFlow.Root + } + } + + fun pushInside(): InsideContainer { + composeRule.onNodeWithText("Navigate Inside Container") + .performClick() + return InsideContainer("in") + } + + fun pushOutside(): OutsideContainer { + composeRule.onNodeWithText("Navigate Outside Container") + .performClick() + + return OutsideContainer("out") + } + + fun pushActivity(): ActivityRobot { + composeRule.onNodeWithText("Navigate Activity") + .performClick() + + return ActivityRobot("act") + } + + fun assertResult(expectedResult: String): RootRobot { + composeRule.onNodeWithText("Last Result: $expectedResult") + .assertExists() + return this + } + } + + inner class InsideContainer( + private val currentResult: String + ) { + init { + composeRule.waitForNavigationHandle { + it.key is ComposeEmbeddedResultFlow.InsideContainer && (it.key as ComposeEmbeddedResultFlow.InsideContainer).currentResult == currentResult + } + } + + fun pushInsideA(): InsideContainer { + composeRule.onNodeWithText("Navigate Inside Container (a)") + .performClick() + return InsideContainer("$currentResult-> in a") + } + + fun pushInsideB(): InsideContainer { + composeRule.onNodeWithText("Navigate Inside Container (b)") + .performClick() + return InsideContainer("$currentResult-> in b") + } + + fun pushOutside1(): OutsideContainer { + composeRule.onNodeWithText("Navigate Outside Container (1)") + .performClick() + + return OutsideContainer("$currentResult-> out 1") + } + + fun pushOutside2(): OutsideContainer { + composeRule.onNodeWithText("Navigate Outside Container (2)") + .performClick() + + return OutsideContainer("$currentResult-> out 2") + } + + fun pushActivityX(): ActivityRobot { + composeRule.onNodeWithText("Navigate Activity (x)") + .performClick() + + return ActivityRobot("$currentResult-> act x") + } + + fun pushActivityY(): ActivityRobot { + composeRule.onNodeWithText("Navigate Activity (y)") + .performClick() + + return ActivityRobot("$currentResult-> act y") + } + + fun finish(): RootRobot { + composeRule.onNodeWithText("Finish") + .performClick() + return RootRobot() + } + } + + inner class OutsideContainer( + private val currentResult: String + ) { + init { + composeRule.waitForNavigationHandle { + it.key is ComposeEmbeddedResultFlow.OutsideContainer && (it.key as ComposeEmbeddedResultFlow.OutsideContainer).currentResult == currentResult + } + } + + fun pushOutside1(): OutsideContainer { + composeRule.onNodeWithText("Navigate Outside Container (1)") + .performClick() + + return OutsideContainer("$currentResult-> out 1") + } + + fun pushOutside2(): OutsideContainer { + composeRule.onNodeWithText("Navigate Outside Container (2)") + .performClick() + + return OutsideContainer("$currentResult-> out 2") + } + + fun pushActivityX(): ActivityRobot { + composeRule.onNodeWithText("Navigate Activity (x)") + .performClick() + + return ActivityRobot("$currentResult-> act x") + } + + fun pushActivityY(): ActivityRobot { + composeRule.onNodeWithText("Navigate Activity (y)") + .performClick() + + return ActivityRobot("$currentResult-> act y") + } + + fun finish(): RootRobot { + composeRule.onNodeWithText("Finish") + .performClick() + return RootRobot() + } + } + + inner class ActivityRobot( + private val currentResult: String + ) { + init { + composeRule.waitForNavigationHandle { + it.key is ComposeEmbeddedResultFlow.Activity && (it.key as ComposeEmbeddedResultFlow.Activity).currentResult == currentResult + } + } + + fun pushActivityX(): ActivityRobot { + composeRule.onNodeWithText("Navigate Activity (x)") + .performClick() + + return ActivityRobot("$currentResult-> act x") + } + + fun pushActivityY(): ActivityRobot { + composeRule.onNodeWithText("Navigate Activity (y)") + .performClick() + + return ActivityRobot("$currentResult-> act y") + } + + fun finish(): RootRobot { + composeRule.onNodeWithText("Finish") + .performClick() + return RootRobot() + } + } +} \ No newline at end of file diff --git a/tests/application/src/androidTest/java/dev/enro/test/application/compose/results/ComposeEmbeddedResultFlowTest.kt b/tests/application/src/androidTest/java/dev/enro/test/application/compose/results/ComposeEmbeddedResultFlowTest.kt new file mode 100644 index 00000000..2d78f43b --- /dev/null +++ b/tests/application/src/androidTest/java/dev/enro/test/application/compose/results/ComposeEmbeddedResultFlowTest.kt @@ -0,0 +1,71 @@ +package dev.enro.test.application.compose.results + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import dev.enro.test.application.SelectDestinationRobot +import dev.enro.tests.application.TestActivity +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +class ComposeEmbeddedResultFlowTest { + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun testInside() { + SelectDestinationRobot(composeRule) + .openComposeEmbeddedResultFlow() + .getRoot() + .pushInside() + .pushInsideA() + .pushInsideB() + .finish() + .assertResult("in-> in a-> in b") + } + + @Test + @Ignore(""" + This works on devices, but fails during tests. It appears that the ComposeRule freezes when the activity is closed, + and doesn't automatically update as it should, so when the activity returns a result and attempts to finish, + the activity doesn't actually finish like it should. + """) + fun testActivity() { + + SelectDestinationRobot(composeRule) + .openComposeEmbeddedResultFlow() + .getRoot() + .pushActivity() + .pushActivityX() + .pushActivityY() + .finish() + .assertResult("act-> act x-> act y") + } + + @Test + fun testOutside() { + SelectDestinationRobot(composeRule) + .openComposeEmbeddedResultFlow() + .getRoot() + .pushOutside() + .pushOutside2() + .pushOutside1() + .finish() + .assertResult("out-> out 2-> out 1") + } + + @Test + fun testMixed() { + SelectDestinationRobot(composeRule) + .openComposeEmbeddedResultFlow() + .getRoot() + .pushInside() + .pushInsideB() + .pushInsideA() + .pushOutside2() + .pushOutside1() + .pushActivityX() // we can only really do one activity result in the test, for the same reason the testActivity test is ignored + .finish() + .assertResult("in-> in b-> in a-> out 2-> out 1-> act x") + } +} \ No newline at end of file diff --git a/tests/application/src/main/AndroidManifest.xml b/tests/application/src/main/AndroidManifest.xml index fe6e50cc..d3a30e14 100644 --- a/tests/application/src/main/AndroidManifest.xml +++ b/tests/application/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ + \ No newline at end of file diff --git a/tests/application/src/main/java/dev/enro/tests/application/compose/FindContext.kt b/tests/application/src/main/java/dev/enro/tests/application/compose/FindContext.kt index 5bfbc83c..f27e3164 100644 --- a/tests/application/src/main/java/dev/enro/tests/application/compose/FindContext.kt +++ b/tests/application/src/main/java/dev/enro/tests/application/compose/FindContext.kt @@ -34,7 +34,6 @@ import dev.enro.core.compose.navigationHandle import dev.enro.core.compose.rememberNavigationContainer import dev.enro.core.container.EmptyBehavior import dev.enro.core.container.accept -import dev.enro.core.container.key import dev.enro.core.findActiveContext import dev.enro.core.findContext import dev.enro.core.navigationContext diff --git a/tests/application/src/main/java/dev/enro/tests/application/compose/results/ComposeEmbeddedResultFlow.kt b/tests/application/src/main/java/dev/enro/tests/application/compose/results/ComposeEmbeddedResultFlow.kt new file mode 100644 index 00000000..753a5975 --- /dev/null +++ b/tests/application/src/main/java/dev/enro/tests/application/compose/results/ComposeEmbeddedResultFlow.kt @@ -0,0 +1,248 @@ +package dev.enro.tests.application.compose.results + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import dev.enro.annotations.NavigationDestination +import dev.enro.core.NavigationKey +import dev.enro.core.closeWithResult +import dev.enro.core.compose.navigationHandle +import dev.enro.core.compose.registerForNavigationResult +import dev.enro.core.compose.rememberNavigationContainer +import dev.enro.core.container.EmptyBehavior +import dev.enro.core.container.accept +import dev.enro.core.navigationHandle +import dev.enro.core.result.deliverResultFromPresent +import dev.enro.core.result.deliverResultFromPush +import dev.enro.tests.application.compose.common.TitledColumn +import kotlinx.parcelize.Parcelize + +@Parcelize +object ComposeEmbeddedResultFlow : NavigationKey.SupportsPush { + @Parcelize + internal object Root : NavigationKey.SupportsPush + + @Parcelize + internal data class InsideContainer( + val currentResult: String, + ) : NavigationKey.SupportsPush.WithResult + + @Parcelize + internal data class OutsideContainer( + val currentResult: String, + ) : NavigationKey.SupportsPush.WithResult + + @Parcelize + internal data class Activity( + val currentResult: String, + ) : NavigationKey.SupportsPresent.WithResult +} + +@NavigationDestination(ComposeEmbeddedResultFlow::class) +@Composable +fun ComposeEmbeddedResultFlowScreen() { + val container = rememberNavigationContainer( + root = ComposeEmbeddedResultFlow.Root, + emptyBehavior = EmptyBehavior.CloseParent, + filter = accept { + key() + key() + } + ) + Box(modifier = Modifier.fillMaxSize()) { + container.Render() + } +} + +@NavigationDestination(ComposeEmbeddedResultFlow.Root::class) +@Composable +fun ComposeEmbeddedResultFlowRoot() { + var lastResult by rememberSaveable { + mutableStateOf("(none)") + } + val resultChannel = registerForNavigationResult { + lastResult = it + } + + TitledColumn(title = "Embedded Result Flow") { + Text("Last Result: $lastResult") + + Button(onClick = { + resultChannel.push(ComposeEmbeddedResultFlow.InsideContainer("in")) + }) { + Text("Navigate Inside Container") + } + + Button(onClick = { + resultChannel.push(ComposeEmbeddedResultFlow.OutsideContainer("out")) + }) { + Text("Navigate Outside Container") + } + + Button(onClick = { + resultChannel.present(ComposeEmbeddedResultFlow.Activity("act")) + }) { + Text("Navigate Activity") + } + } +} + +@NavigationDestination(ComposeEmbeddedResultFlow.InsideContainer::class) +@Composable +fun ComposeEmbeddedResultFlowInsideContainer() { + val navigation = navigationHandle() + + TitledColumn(title = "Embedded Result Flow Inside Container") { + Button(onClick = { + navigation.deliverResultFromPush( + ComposeEmbeddedResultFlow.InsideContainer(navigation.key.currentResult + "-> in a") + ) + }) { + Text("Navigate Inside Container (a)") + } + Button(onClick = { + navigation.deliverResultFromPush( + ComposeEmbeddedResultFlow.InsideContainer(navigation.key.currentResult + "-> in b") + ) + }) { + Text("Navigate Inside Container (b)") + } + + Button(onClick = { + navigation.deliverResultFromPush( + ComposeEmbeddedResultFlow.OutsideContainer(navigation.key.currentResult + "-> out 1") + ) + }) { + Text("Navigate Outside Container (1)") + } + + Button(onClick = { + navigation.deliverResultFromPush( + ComposeEmbeddedResultFlow.OutsideContainer(navigation.key.currentResult + "-> out 2") + ) + }) { + Text("Navigate Outside Container (2)") + } + + Button(onClick = { + navigation.deliverResultFromPresent( + ComposeEmbeddedResultFlow.Activity(navigation.key.currentResult + "-> act x") + ) + }) { + Text("Navigate Activity (x)") + } + + Button(onClick = { + navigation.deliverResultFromPresent( + ComposeEmbeddedResultFlow.Activity(navigation.key.currentResult + "-> act y") + ) + }) { + Text("Navigate Activity (y)") + } + + Button(onClick = { + navigation.closeWithResult(navigation.key.currentResult) + }) { + Text("Finish") + } + } +} + +@NavigationDestination(ComposeEmbeddedResultFlow.OutsideContainer::class) +@Composable +fun ComposeEmbeddedResultFlowOutsideContainerContainer() { + val navigation = navigationHandle() + + TitledColumn(title = "Embedded Result Flow Outside Container") { + Button(onClick = { + navigation.deliverResultFromPush( + ComposeEmbeddedResultFlow.OutsideContainer(navigation.key.currentResult + "-> out 1") + ) + }) { + Text("Navigate Outside Container (1)") + } + + Button(onClick = { + navigation.deliverResultFromPush( + ComposeEmbeddedResultFlow.OutsideContainer(navigation.key.currentResult + "-> out 2") + ) + }) { + Text("Navigate Outside Container (2)") + } + + Button(onClick = { + navigation.deliverResultFromPresent( + ComposeEmbeddedResultFlow.Activity(navigation.key.currentResult + "-> act x") + ) + }) { + Text("Navigate Activity (x)") + } + + Button(onClick = { + navigation.deliverResultFromPresent( + ComposeEmbeddedResultFlow.Activity(navigation.key.currentResult + "-> act y") + ) + }) { + Text("Navigate Activity (y)") + } + + Button(onClick = { + navigation.closeWithResult(navigation.key.currentResult) + }) { + Text("Finish") + } + } +} + +@NavigationDestination(ComposeEmbeddedResultFlow.Activity::class) +class ComposeEmbeddedResultFlowActivity : AppCompatActivity() { + + private val navigation by navigationHandle() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MaterialTheme { + Content() + } + } + } + + @Composable + private fun Content() { + TitledColumn(title = "Embedded Result Flow Activity") { + Button(onClick = { + navigation.deliverResultFromPresent( + ComposeEmbeddedResultFlow.Activity(navigation.key.currentResult + "-> act x") + ) + }) { + Text("Navigate Activity (x)") + } + + Button(onClick = { + navigation.deliverResultFromPresent( + ComposeEmbeddedResultFlow.Activity(navigation.key.currentResult + "-> act y") + ) + }) { + Text("Navigate Activity (y)") + } + + Button(onClick = { + navigation.closeWithResult(navigation.key.currentResult) + }) { + Text("Finish") + } + } + } +} \ No newline at end of file