-
-
Notifications
You must be signed in to change notification settings - Fork 130
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
Conversation
Rework: Navigator API Rework: API of Stack and SnapshotStackState
5eedcf7
to
568d6ac
Compare
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. |
Indeed it does. Without rewriting the core you still will be adding only small extensions. |
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 |
"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:
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. |
Oh course there is option to not break backward compatibility. |
Looking into your code, the way I think it could work and would not add any breaking changes is to provide this In the case of the TabNavigator, it personally can have the |
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 |
Actually.... testing here, the |
Should be closed. We can't break Core APIs. |
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 ofNavigator
looks like this.From ->
navigator.pop()
To ->
navigator.pop()
If you inside aScreen
.Added sample "Transition" that shows all the advantages of this approach.
Check simple custom transition based on target and invoker.
Voyager.Transition.Sample.mp4