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

Add new Stack that requires the caller #302

Closed
wants to merge 2 commits into from

Conversation

Velord
Copy link

@Velord Velord commented Jan 13, 2024

ISSUES SOLVED:
#265 and #190

Added new WithLastActionStack. Stack is based on the principle - you can't do anything without passing the caller.
Navigator now based on that stack.

Api of Navigator(Stack) slightly changes.
From ->navigator.pop()
To -> navigator.pop(this@Screen) Now you must always provide screen.

Added extensions for Navigator that mitigate that requirement. Extensions work only on Android platform as they use "context receiver" feature. With context receiver API of Navigator looks like this.
From ->navigator.pop()
To -> navigator.pop() If you inside a Screen.

Added sample "Transition" that shows all the advantages of this approach.
Check simple custom transition based on target and invoker.

@Composable
fun TransitionDemo(
    navigator: Navigator,
    modifier: Modifier = Modifier,
    content: ScreenTransitionContent = { it.Content() }
) {
    val transition: AnimatedContentTransitionScope<Screen>.() -> ContentTransform = {
        // Define any StackEvent you want transition to be
        val isPush = navigator.lastAction.event == StackEvent.Push
        val isPop = navigator.lastAction.event == StackEvent.Pop
        // Define any Screen you want transition must be from
        val isInvokerTransitionScreen = navigator.lastAction.invoker == TransitionScreen
        val isInvokerFadeScreen = navigator.lastAction.invoker == FadeScreen
        val isInvokerShrinkScreen = navigator.lastAction.invoker == ShrinkScreen
        val isInvokerScaleScreen = navigator.lastAction.invoker == ScaleScreen
        // Define any Screen you want transition must be to
        val isTargetTransitionScreen = navigator.lastItem == TransitionScreen
        val isTargetFadeScreen = navigator.lastItem == FadeScreen
        val isTargetShrinkScreen = navigator.lastItem == ShrinkScreen
        val isTargetScaleScreen = navigator.lastItem == ScaleScreen

        val tweenOffset: FiniteAnimationSpec<IntOffset> = tween(
            ...
        )
        val tweenSize: FiniteAnimationSpec<IntSize> = tween(
            ...
        )

        val sizeDefault = ({ size: Int -> size })
        val sizeMinus = ({ size: Int -> -size })
        val (initialOffset, targetOffset) = when {
            isPush && isInvokerTransitionScreen -> {
                if (isTargetFadeScreen || isTargetShrinkScreen) sizeMinus to sizeDefault
                else sizeDefault to sizeMinus
            }
            isPop && isInvokerFadeScreen && isTargetTransitionScreen -> sizeDefault to sizeMinus
            else -> sizeDefault to sizeMinus
        }

        val fadeInFrames = keyframes {
           ....
        }
        val fadeOutFrames = keyframes {
            ....
        }

        val scaleInFrames = keyframes {
            ....
        }
        val scaleOutFrames = keyframes {
            ....
        }

        when {
            // Define any transition you want based on the StackEvent, invoker and target
            isPush && isInvokerTransitionScreen && isTargetFadeScreen ||
                    isPop && isInvokerFadeScreen && isTargetTransitionScreen -> {
                val enter = slideInHorizontally(tweenOffset, initialOffset) + fadeIn(fadeInFrames)
                val exit = slideOutHorizontally(tweenOffset, targetOffset) + fadeOut(fadeOutFrames)
                enter togetherWith exit
            }
            isPush && isInvokerTransitionScreen && isTargetShrinkScreen ||
                isPop && isInvokerShrinkScreen && isTargetTransitionScreen -> {
                val enter = slideInVertically(tweenOffset, initialOffset)
                val exit = shrinkVertically(animationSpec = tweenSize, shrinkTowards = Alignment.Top)
                enter togetherWith exit
            }
            isPush && isInvokerTransitionScreen && isTargetScaleScreen -> {
                val enter = slideInVertically(tweenOffset, initialOffset) + fadeIn(fadeInFrames) + scaleIn(scaleInFrames)
                val exit = slideOutVertically(tweenOffset, targetOffset) + fadeOut(fadeOutFrames) + scaleOut(scaleOutFrames)
                enter togetherWith exit
            }
            isPop && isInvokerScaleScreen && isTargetTransitionScreen -> {
                val enter = slideInHorizontally(tweenOffset, initialOffset) + fadeIn(fadeInFrames) + scaleIn(scaleInFrames)
                val exit = slideOutHorizontally(tweenOffset, targetOffset) + fadeOut(fadeOutFrames) + scaleOut(scaleOutFrames)
                enter togetherWith exit
            }
            else -> {
                val animationSpec: FiniteAnimationSpec<IntOffset> = tween(
                    durationMillis = 500,
                    delayMillis = 100,
                    easing = LinearEasing
                )
                slideInHorizontally(animationSpec, initialOffset) togetherWith
                        slideOutHorizontally(animationSpec, targetOffset)
            }
        }
    }
    ScreenTransition(
        navigator = navigator,
        transition = transition,
        modifier = modifier,
        content = content,
    )
}
Voyager.Transition.Sample.mp4

Velord added 2 commits January 13, 2024 22:31
Rework: Navigator API
Rework: API of Stack and SnapshotStackState
@DevSrSouza
Copy link
Collaborator

This for the PR and the examples. This PRs change alot the main APIs from Voyager and also relay in Context Receivers that does not support Kotlin Multiplatform.

@Velord
Copy link
Author

Velord commented Feb 5, 2024

Indeed it does. Without rewriting the core you still will be adding only small extensions.
For example You are considering nearly a year to add result extension API.
Considerable amount of users took that library and tuned for their needs.
Release Voyager 2.0 or whatever you called with no backwards compatibility with features everyone ask you for years/ month.

@DevSrSouza
Copy link
Collaborator

We are not considering adding new extensions because the currently state of the library, everything should be able to be done as Extensions on top of the Core APIs, we don't want to commit currently, merging or writing a API from scratch that will not cover all cases of the library. We already have API that we regret the design for example TabNavigator that is a really simple API but does not cover alot of use cases and there is alot of issues about to support more use cases. Because of that reasos we are not committing to build extensions that we are not sure that will cover 90% of the use cases, instead, we have being trying to make the library more extensible as possible.

About this specific PR, I don't see why is required to the invoker be customizable, can you extend why the invoker can be other screen instead of the lastItem before the event being made?
If the invoker be always the screen that is on top and is doing the navigation event, we can, before applying the stack operation, save the last item in memory in a new variable, this way, we don't need a new API to only store this previous lastItem from the lastEvent.

@Velord
Copy link
Author

Velord commented Feb 5, 2024

"If the invoker be always the screen that is on top" - it's true in considerable amount of cases. Let's review some examples when it is not:

  1. But when you are using TabNavigator any tab can become as "invoker", not the last one.
  2. Consider application with nested graphs. In the same time on the screen can be X graph and Y Screen representing that graph respectively. So user can do action on any screen that can cause Navigator to do some job.
  3. Another example user go deep in certain graph. We need to track when user go back and see certain screen. Broadcast that event to our specific ViewModel. When Screen that represent that ViewModel will be partially (it can be TabNavigator or Bottom sheet) visible we will do our job.

So basic idea is invoker in most cases is the last caller. However when you need to perform navigation by any Screen in your stack you must be able to do this.

@Velord
Copy link
Author

Velord commented Feb 5, 2024

Oh course there is option to not break backward compatibility.
Let's assume we leave current Navigator as it is.
We add NavigatorV2 or NavigatorWithLastAction or NavigatorPrettyName. No API would change in that case. Every who wanna add in their project ability make transition by certain Screen and StackAction would be able to do this.
Would you review and consider this PR to merge?

@DevSrSouza
Copy link
Collaborator

Looking into your code, the way I think it could work and would not add any breaking changes is to provide this var invoker: Item? at Navigator as @Experimental and override the stack apis that updates the lastEvent to also, update the invoker property. The Navigator class is closed, so, no breaking API in the case of the Stack API.

In the case of the TabNavigator, it personally can have the push/pop/replace extension functions with the option of the invoker.

@DevSrSouza
Copy link
Collaborator

Here is a solution with zero breaking changes and does not need a NavigatorV2 and new Stack API (does not support TabNavigator, but it could)
diff --git a/samples/android/src/main/AndroidManifest.xml b/samples/android/src/main/AndroidManifest.xml
index 96782b7..3162418 100644
--- a/samples/android/src/main/AndroidManifest.xml
+++ b/samples/android/src/main/AndroidManifest.xml
@@ -25,6 +25,7 @@
         <activity android:name=".parcelableScreen.ParcelableActivity"/>
         <activity android:name=".screenModel.ScreenModelActivity"/>
         <activity android:name=".androidViewModel.AndroidViewModelActivity"/>
+        <activity android:name=".transitions.TransitionActivity"/>
         <activity android:name=".bottomSheetNavigation.BottomSheetNavigationActivity"/>
         <activity android:name=".rxJavaIntegration.RxJavaIntegrationActivity"/>
         <activity android:name=".liveDataIntegration.LiveDataIntegrationActivity"/>
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/SampleActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/SampleActivity.kt
index 8d9be55..5c1ea7a 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/SampleActivity.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/SampleActivity.kt
@@ -32,6 +32,7 @@ import cafe.adriel.voyager.sample.rxJavaIntegration.RxJavaIntegrationActivity
 import cafe.adriel.voyager.sample.screenModel.ScreenModelActivity
 import cafe.adriel.voyager.sample.stateStack.StateStackActivity
 import cafe.adriel.voyager.sample.tabNavigation.TabNavigationActivity
+import cafe.adriel.voyager.sample.transitions.TransitionActivity
 
 class SampleActivity : ComponentActivity() {
 
@@ -52,6 +53,7 @@ class SampleActivity : ComponentActivity() {
             contentPadding = PaddingValues(24.dp)
         ) {
             item {
+                StartSampleButton<TransitionActivity>("Transition")
                 StartSampleButton<StateStackActivity>("SnapshotStateStack")
                 StartSampleButton<BasicNavigationActivity>("Basic Navigation")
                 StartSampleButton<ParcelableActivity>("Basic Navigation with Parcelable")
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/FadeScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/FadeScreen.kt
new file mode 100644
index 0000000..4a0a871
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/FadeScreen.kt
@@ -0,0 +1,28 @@
+package cafe.adriel.voyager.sample.transitions
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.sp
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.screen.uniqueScreenKey
+
+data object FadeScreen : Screen {
+    override val key = uniqueScreenKey
+
+    @Composable
+    override fun Content() {
+        Box(modifier = Modifier.fillMaxSize()) {
+            Text(
+                text = "Fade Screen",
+                modifier = Modifier.align(alignment = Alignment.Center),
+                color = Color.Red,
+                fontSize = 30.sp
+            )
+        }
+    }
+}
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/ScaleScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/ScaleScreen.kt
new file mode 100644
index 0000000..4a7e573
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/ScaleScreen.kt
@@ -0,0 +1,27 @@
+package cafe.adriel.voyager.sample.transitions
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.sp
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.screen.uniqueScreenKey
+
+data object ScaleScreen : Screen {
+    override val key = uniqueScreenKey
+
+    @Composable
+    override fun Content() {
+        Box(modifier = Modifier.fillMaxSize()) {
+            Text(
+                text = "Scale Screen",
+                modifier = Modifier.align(alignment = Alignment.Center),
+                color = Color.Red,
+                fontSize = 30.sp
+            )
+        }
+    }
+}
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/ShrinkScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/ShrinkScreen.kt
new file mode 100644
index 0000000..023f007
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/ShrinkScreen.kt
@@ -0,0 +1,28 @@
+package cafe.adriel.voyager.sample.transitions
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.sp
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.screen.uniqueScreenKey
+
+data object ShrinkScreen : Screen {
+    override val key = uniqueScreenKey
+
+    @Composable
+    override fun Content() {
+        Box(modifier = Modifier.fillMaxSize()) {
+            Text(
+                text = "Shrink Screen",
+                modifier = Modifier.align(alignment = Alignment.Center),
+                color = Color.Red,
+                fontSize = 30.sp
+            )
+        }
+    }
+}
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/TransitionActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/TransitionActivity.kt
new file mode 100644
index 0000000..2755aa1
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/TransitionActivity.kt
@@ -0,0 +1,164 @@
+package cafe.adriel.voyager.sample.transitions
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.animation.AnimatedContentTransitionScope
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.animation.togetherWith
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.stack.StackEvent
+import cafe.adriel.voyager.navigator.Navigator
+import cafe.adriel.voyager.transitions.ScreenTransition
+import cafe.adriel.voyager.transitions.ScreenTransitionContent
+
+class TransitionActivity : ComponentActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setContent {
+            Navigator(TransitionScreen) {
+                TransitionDemo(it)
+            }
+        }
+    }
+}
+
+@Composable
+fun TransitionDemo(
+    navigator: Navigator,
+    modifier: Modifier = Modifier,
+    content: ScreenTransitionContent = { it.Content() }
+) {
+    val transition: AnimatedContentTransitionScope<Screen>.() -> ContentTransform = {
+        // Define any StackEvent you want transition to be
+        val isPush = navigator.lastEvent == StackEvent.Push
+        val isPop = navigator.lastEvent == StackEvent.Pop
+        // Define any Screen you want transition must be from
+        val isInvokerTransitionScreen = navigator.lastEventTrigger == TransitionScreen
+        val isInvokerFadeScreen = navigator.lastEventTrigger == FadeScreen
+        val isInvokerShrinkScreen = navigator.lastEventTrigger == ShrinkScreen
+        val isInvokerScaleScreen = navigator.lastEventTrigger == ScaleScreen
+        // Define any Screen you want transition must be to
+        val isTargetTransitionScreen = navigator.lastItem == TransitionScreen
+        val isTargetFadeScreen = navigator.lastItem == FadeScreen
+        val isTargetShrinkScreen = navigator.lastItem == ShrinkScreen
+        val isTargetScaleScreen = navigator.lastItem == ScaleScreen
+
+        val tweenOffset: FiniteAnimationSpec<IntOffset> = tween(
+            durationMillis = 2000,
+            delayMillis = 100,
+            easing = LinearEasing
+        )
+        val tweenSize: FiniteAnimationSpec<IntSize> = tween(
+            durationMillis = 2000,
+            delayMillis = 100,
+            easing = LinearEasing
+        )
+
+        val sizeDefault = ({ size: Int -> size })
+        val sizeMinus = ({ size: Int -> -size })
+        val (initialOffset, targetOffset) = when {
+            isPush && isInvokerTransitionScreen -> {
+                if (isTargetFadeScreen || isTargetShrinkScreen) sizeMinus to sizeDefault
+                else sizeDefault to sizeMinus
+            }
+            isPop && isInvokerFadeScreen && isTargetTransitionScreen -> sizeDefault to sizeMinus
+            else -> sizeDefault to sizeMinus
+        }
+
+        val fadeInFrames = keyframes {
+            durationMillis = 2000
+            0.1f at 0 with LinearEasing
+            0.2f at 1800 with LinearEasing
+            1.0f at 2000 with LinearEasing
+        }
+        val fadeOutFrames = keyframes {
+            durationMillis = 2000
+            0.9f at 0 with LinearEasing
+            0.8f at 100 with LinearEasing
+            0.7f at 200 with LinearEasing
+            0.6f at 300 with LinearEasing
+            0.5f at 400 with LinearEasing
+            0.4f at 500 with LinearEasing
+            0.3f at 600 with LinearEasing
+            0.2f at 1000 with LinearEasing
+            0.1f at 1500 with LinearEasing
+            0.0f at 2000 with LinearEasing
+        }
+
+        val scaleInFrames = keyframes {
+            durationMillis = 2000
+            0.1f at 0 with LinearEasing
+            0.3f at 1500 with LinearEasing
+            1.0f at 2000 with LinearEasing
+        }
+        val scaleOutFrames = keyframes {
+            durationMillis = 2000
+            0.9f at 0 with LinearEasing
+            0.7f at 500 with LinearEasing
+            0.3f at 700 with LinearEasing
+            0.0f at 2000 with LinearEasing
+        }
+
+        when {
+            // Define any transition you want based on the StackEvent, invoker and target
+            isPush && isInvokerTransitionScreen && isTargetFadeScreen ||
+                    isPop && isInvokerFadeScreen && isTargetTransitionScreen -> {
+                val enter = slideInHorizontally(tweenOffset, initialOffset) + fadeIn(fadeInFrames)
+                val exit = slideOutHorizontally(tweenOffset, targetOffset) + fadeOut(fadeOutFrames)
+                enter togetherWith exit
+            }
+            isPush && isInvokerTransitionScreen && isTargetShrinkScreen ||
+                isPop && isInvokerShrinkScreen && isTargetTransitionScreen -> {
+                val enter = slideInVertically(tweenOffset, initialOffset)
+                val exit = shrinkVertically(animationSpec = tweenSize, shrinkTowards = Alignment.Top)
+                enter togetherWith exit
+            }
+            isPush && isInvokerTransitionScreen && isTargetScaleScreen -> {
+                val enter = slideInVertically(tweenOffset, initialOffset) + fadeIn(fadeInFrames) + scaleIn(scaleInFrames)
+                val exit = slideOutVertically(tweenOffset, targetOffset) + fadeOut(fadeOutFrames) + scaleOut(scaleOutFrames)
+                enter togetherWith exit
+            }
+            isPop && isInvokerScaleScreen && isTargetTransitionScreen -> {
+                val enter = slideInHorizontally(tweenOffset, initialOffset) + fadeIn(fadeInFrames) + scaleIn(scaleInFrames)
+                val exit = slideOutHorizontally(tweenOffset, targetOffset) + fadeOut(fadeOutFrames) + scaleOut(scaleOutFrames)
+                enter togetherWith exit
+            }
+            else -> {
+                val animationSpec: FiniteAnimationSpec<IntOffset> = tween(
+                    durationMillis = 500,
+                    delayMillis = 100,
+                    easing = LinearEasing
+                )
+                slideInHorizontally(animationSpec, initialOffset) togetherWith
+                        slideOutHorizontally(animationSpec, targetOffset)
+            }
+        }
+    }
+    ScreenTransition(
+        navigator = navigator,
+        transition = transition,
+        modifier = modifier,
+        content = content,
+    )
+}
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/TransitionScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/TransitionScreen.kt
new file mode 100644
index 0000000..1118f87
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transitions/TransitionScreen.kt
@@ -0,0 +1,59 @@
+package cafe.adriel.voyager.sample.transitions
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.screen.uniqueScreenKey
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+
+data object TransitionScreen : Screen {
+
+    override val key = uniqueScreenKey
+
+    @Composable
+    override fun Content() {
+        val navigator = LocalNavigator.currentOrThrow
+
+        Column(
+            verticalArrangement = Arrangement.Center,
+            horizontalAlignment = Alignment.CenterHorizontally,
+            modifier = Modifier.fillMaxSize()
+        ) {
+            PushButton(text = "Push fade left\nPop fade right") {
+                navigator.push(FadeScreen)
+            }
+            Spacer(modifier = Modifier.height(50.dp))
+            PushButton(text = "Push shrink top\nPop shrink bottom") {
+                navigator.push(ShrinkScreen)
+            }
+            Spacer(modifier = Modifier.height(50.dp))
+            PushButton(text = "Push fade scale bottom\nPop scale right") {
+                navigator.push(ScaleScreen)
+            }
+        }
+    }
+}
+
+@Composable
+private fun PushButton(
+    text: String,
+    onClick: () -> Unit
+) {
+    Button(
+        onClick = onClick,
+        modifier = Modifier.sizeIn(minWidth = 200.dp, minHeight = 70.dp)
+    ) {
+        Text(text = text)
+    }
+}
diff --git a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/Navigator.kt b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/Navigator.kt
index 0aa0e18..12c82a9 100644
--- a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/Navigator.kt
+++ b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/Navigator.kt
@@ -6,10 +6,14 @@ import androidx.compose.runtime.ProvidableCompositionLocal
 import androidx.compose.runtime.currentCompositeKeyHash
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.neverEqualPolicy
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.SaveableStateHolder
 import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.runtime.setValue
 import androidx.compose.runtime.staticCompositionLocalOf
+import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi
 import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
 import cafe.adriel.voyager.core.concurrent.ThreadSafeMap
 import cafe.adriel.voyager.core.concurrent.ThreadSafeSet
@@ -19,6 +23,7 @@ import cafe.adriel.voyager.core.lifecycle.getNavigatorScreenLifecycleProvider
 import cafe.adriel.voyager.core.lifecycle.rememberScreenLifecycleOwner
 import cafe.adriel.voyager.core.screen.Screen
 import cafe.adriel.voyager.core.stack.Stack
+import cafe.adriel.voyager.core.stack.StackEvent
 import cafe.adriel.voyager.core.stack.toMutableStateStack
 import cafe.adriel.voyager.navigator.internal.ChildrenNavigationDisposableEffect
 import cafe.adriel.voyager.navigator.internal.LocalNavigatorStateHolder
@@ -107,8 +112,9 @@ public class Navigator @InternalVoyagerApi constructor(
     @InternalVoyagerApi public val key: String,
     private val stateHolder: SaveableStateHolder,
     public val disposeBehavior: NavigatorDisposeBehavior,
-    public val parent: Navigator? = null
-) : Stack<Screen> by screens.toMutableStateStack(minSize = 1) {
+    public val parent: Navigator? = null,
+    private val stack: Stack<Screen> = screens.toMutableStateStack(minSize = 1)
+) : Stack<Screen> by stack {
 
     public val level: Int =
         parent?.level?.inc() ?: 0
@@ -119,6 +125,10 @@ public class Navigator @InternalVoyagerApi constructor(
 
     private val stateKeys = ThreadSafeSet<String>()
 
+    @ExperimentalVoyagerApi
+    public var lastEventTrigger: Screen? by mutableStateOf(null, neverEqualPolicy())
+        private set
+
     internal val children = ThreadSafeMap<NavigatorKey, Navigator>()
 
     @Composable
@@ -178,6 +188,58 @@ public class Navigator @InternalVoyagerApi constructor(
                 stateKeys -= key
             }
     }
+
+    override fun push(item: Screen) {
+        lastEventTrigger = lastItemOrNull
+        stack.push(item)
+    }
+
+    override fun push(items: List<Screen>) {
+        lastEventTrigger = lastItemOrNull
+        stack.push(items)
+    }
+
+    override fun replace(item: Screen) {
+        lastEventTrigger = lastItemOrNull
+        stack.replace(item)
+    }
+
+    override fun replaceAll(item: Screen) {
+        lastEventTrigger = lastItemOrNull
+        stack.replaceAll(item)
+    }
+
+    override fun replaceAll(items: List<Screen>) {
+        lastEventTrigger = lastItemOrNull
+        stack.replaceAll(items)
+    }
+
+    override fun pop(): Boolean {
+        if(canPop) {
+            lastEventTrigger = lastItemOrNull
+        }
+        return stack.pop()
+    }
+
+    override fun popAll() {
+        lastEventTrigger = lastItemOrNull
+        stack.popAll()
+    }
+
+    override fun popUntil(predicate: (Screen) -> Boolean): Boolean {
+        lastEventTrigger = lastItemOrNull
+        return stack.popUntil(predicate)
+    }
+
+    override fun plusAssign(item: Screen) {
+        lastEventTrigger = lastItemOrNull
+        stack.plusAssign(item)
+    }
+
+    override fun plusAssign(items: List<Screen>) {
+        lastEventTrigger = lastItemOrNull
+        stack.plusAssign(items)
+    }
 }
 
 public data class NavigatorDisposeBehavior(

Can you validate this solution? You can copy this git diff, create a file transitions.patch and call git apply transitions.patch

@DevSrSouza
Copy link
Collaborator

Actually.... testing here, the AnimatedContentTransitionScope has initialState and targetState, by replacing navigator.lastItem by targetState and the invoker to initialState, the final results of the sample is actually the same

@Velord
Copy link
Author

Velord commented Feb 6, 2024

Should be closed. We can't break Core APIs.
Further discussion in #326

@DevSrSouza DevSrSouza closed this Feb 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants