diff --git a/samples/android/src/main/AndroidManifest.xml b/samples/android/src/main/AndroidManifest.xml
index 96782b71..dfa169a5 100644
--- a/samples/android/src/main/AndroidManifest.xml
+++ b/samples/android/src/main/AndroidManifest.xml
@@ -22,6 +22,7 @@
+
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 8d9be558..25e5f342 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.transition.TransitionActivity
class SampleActivity : ComponentActivity() {
@@ -58,6 +59,7 @@ class SampleActivity : ComponentActivity() {
StartSampleButton("Tab Navigation")
StartSampleButton("BottomSheet Navigation")
StartSampleButton("Nested Navigation")
+ StartSampleButton("Transition")
StartSampleButton("Android ViewModel")
StartSampleButton("ScreenModel")
StartSampleButton("Koin Integration")
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/FadeScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/FadeScreen.kt
new file mode 100644
index 00000000..3c16b45e
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/FadeScreen.kt
@@ -0,0 +1,28 @@
+package cafe.adriel.voyager.sample.transition
+
+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/transition/ScaleScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/ScaleScreen.kt
new file mode 100644
index 00000000..e5fabe0b
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/ScaleScreen.kt
@@ -0,0 +1,28 @@
+package cafe.adriel.voyager.sample.transition
+
+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/transition/SelfResetNavigator.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/SelfResetNavigator.kt
new file mode 100644
index 00000000..f618811e
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/SelfResetNavigator.kt
@@ -0,0 +1,122 @@
+package cafe.adriel.voyager.sample.transition
+
+import android.util.Log
+import androidx.compose.runtime.saveable.SaveableStateHolder
+import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.stack.Stack
+import cafe.adriel.voyager.core.stack.toMutableStateStack
+import cafe.adriel.voyager.navigator.Navigator
+import cafe.adriel.voyager.navigator.NavigatorCreator
+import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+fun SelfResetNavigatorCreator(resetAfter: Long): NavigatorCreator = {
+ screenCollection: List,
+ s: String,
+ saveableStateHolder: SaveableStateHolder,
+ navigatorDisposeBehavior: NavigatorDisposeBehavior,
+ navigator: Navigator?, ->
+ SelfResetNavigator(resetAfter, screenCollection, s, saveableStateHolder, navigatorDisposeBehavior, navigator)
+}
+
+private class SelfResetNavigator @InternalVoyagerApi constructor(
+ val resetAfter: Long,
+ screens: List,
+ key: String,
+ stateHolder: SaveableStateHolder,
+ disposeBehavior: NavigatorDisposeBehavior,
+ parent: Navigator? = null,
+ private val stack: Stack = screens.toMutableStateStack(minSize = 1)
+) : Navigator(screens, key, stateHolder, disposeBehavior, parent, stack) {
+
+ private val timeStamp = MutableSharedFlow(
+ extraBufferCapacity = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+
+ private val scope = CoroutineScope(Job() + Dispatchers.Default)
+
+ init {
+ Log.d("@@@", "Init")
+ observeTimeStamp()
+ }
+
+ private fun observeTimeStamp() {
+ scope.launch {
+ timeStamp.collectLatest {
+ Log.d("@@@", "Before delay")
+ delay(resetAfter)
+ Log.d("@@@", "After delay")
+ if (canPop) {
+ Log.d("@@@", "Reset")
+ popAll()
+ }
+ }
+ }
+ }
+
+ override fun push(item: Screen) {
+ timeStamp.tryEmit(Unit)
+ stack.push(item)
+ }
+
+ override fun push(items: List) {
+ timeStamp.tryEmit(Unit)
+ stack.push(items)
+ }
+
+ override fun replace(item: Screen) {
+ timeStamp.tryEmit(Unit)
+ stack.replace(item)
+ }
+
+ override fun replaceAll(item: Screen) {
+ timeStamp.tryEmit(Unit)
+ stack.replaceAll(item)
+ }
+
+ override fun replaceAll(items: List) {
+ timeStamp.tryEmit(Unit)
+ stack.replaceAll(items)
+ }
+
+ override fun pop(): Boolean {
+ if (canPop) {
+ timeStamp.tryEmit(Unit)
+ }
+ return stack.pop()
+ }
+
+ override fun popAll() {
+ timeStamp.tryEmit(Unit)
+ stack.popAll()
+ }
+
+ override fun popUntil(predicate: (Screen) -> Boolean): Boolean {
+ timeStamp.tryEmit(Unit)
+ return stack.popUntil(predicate)
+ }
+
+ override fun plusAssign(item: Screen) {
+ timeStamp.tryEmit(Unit)
+ stack.plusAssign(item)
+ }
+
+ override fun plusAssign(items: List) {
+ timeStamp.tryEmit(Unit)
+ stack.plusAssign(items)
+ }
+
+ override fun clearEvent() {
+ timeStamp.tryEmit(Unit)
+ stack.clearEvent()
+ }
+}
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/ShrinkScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/ShrinkScreen.kt
new file mode 100644
index 00000000..4e069268
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/ShrinkScreen.kt
@@ -0,0 +1,28 @@
+package cafe.adriel.voyager.sample.transition
+
+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/transition/TransitionActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt
new file mode 100644
index 00000000..22c7ea02
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt
@@ -0,0 +1,175 @@
+package cafe.adriel.voyager.sample.transition
+
+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.navigator.DefaultNavigatorCreator
+import cafe.adriel.voyager.navigator.Navigator
+import cafe.adriel.voyager.navigator.isPopLastEvent
+import cafe.adriel.voyager.navigator.isPushLastEvent
+import cafe.adriel.voyager.navigator.summonNavigatorCreator
+import cafe.adriel.voyager.transitions.ScreenTransition
+import cafe.adriel.voyager.transitions.ScreenTransitionContent
+
+class TransitionActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ summonNavigatorCreator = SelfResetNavigatorCreator(5000L)
+ Navigator(TransitionScreen) {
+ TransitionDemo(it)
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ summonNavigatorCreator = DefaultNavigatorCreator()
+ }
+}
+
+@Composable
+fun TransitionDemo(
+ navigator: Navigator,
+ modifier: Modifier = Modifier,
+ content: ScreenTransitionContent = { it.Content() }
+) {
+ val transition: AnimatedContentTransitionScope.() -> ContentTransform = {
+ // Define any StackEvent you want transition to be
+ val isPush = navigator.isPushLastEvent()
+ val isPop = navigator.isPopLastEvent()
+ // Define any Screen you want transition must be from
+ val invoker = this.initialState
+ val target = this.targetState
+ val isInvokerTransitionScreen = invoker == TransitionScreen
+ val isInvokerFadeScreen = invoker == FadeScreen
+ val isInvokerShrinkScreen = invoker == ShrinkScreen
+ val isInvokerScaleScreen = invoker == ScaleScreen
+ // Define any Screen you want transition must be to
+ val isTargetTransitionScreen = target == TransitionScreen
+ val isTargetFadeScreen = target == FadeScreen
+ val isTargetShrinkScreen = target == ShrinkScreen
+ val isTargetScaleScreen = target == ScaleScreen
+
+ val tweenOffset: FiniteAnimationSpec = tween(
+ durationMillis = 2000,
+ delayMillis = 100,
+ easing = LinearEasing
+ )
+ val tweenSize: FiniteAnimationSpec = 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 = 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/transition/TransitionScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt
new file mode 100644
index 00000000..919cc78d
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt
@@ -0,0 +1,59 @@
+package cafe.adriel.voyager.sample.transition
+
+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/androidMain/kotlin/cafe/adriel/voyager/navigator/NavigatorSaver.android.kt b/voyager-navigator/src/androidMain/kotlin/cafe/adriel/voyager/navigator/NavigatorSaver.android.kt
index 4c2f7b18..8b0e15ca 100644
--- a/voyager-navigator/src/androidMain/kotlin/cafe/adriel/voyager/navigator/NavigatorSaver.android.kt
+++ b/voyager-navigator/src/androidMain/kotlin/cafe/adriel/voyager/navigator/NavigatorSaver.android.kt
@@ -8,7 +8,9 @@ import cafe.adriel.voyager.core.screen.Screen
* Navigator Saver that forces all Screens be [Parcelable], if not, it will throw a exception while trying to save
* the navigator state.
*/
-public fun parcelableNavigatorSaver(): NavigatorSaver =
+public fun parcelableNavigatorSaver(
+ navigatorCreator: NavigatorCreator = summonNavigatorCreator
+): NavigatorSaver =
NavigatorSaver { _, key, stateHolder, disposeBehavior, parent ->
listSaver(
save = { navigator ->
@@ -28,7 +30,7 @@ public fun parcelableNavigatorSaver(): NavigatorSaver =
screenAsParcelables
},
- restore = { items -> Navigator(items as List, key, stateHolder, disposeBehavior, parent) }
+ restore = { items -> navigatorCreator(items as List, key, stateHolder, disposeBehavior, parent) }
)
}
diff --git a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/DefaultNavigator.kt b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/DefaultNavigator.kt
new file mode 100644
index 00000000..efc69af8
--- /dev/null
+++ b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/DefaultNavigator.kt
@@ -0,0 +1,16 @@
+package cafe.adriel.voyager.navigator
+
+import androidx.compose.runtime.saveable.SaveableStateHolder
+import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.stack.Stack
+import cafe.adriel.voyager.core.stack.toMutableStateStack
+
+public class DefaultNavigator @InternalVoyagerApi constructor(
+ screens: List,
+ key: String,
+ stateHolder: SaveableStateHolder,
+ disposeBehavior: NavigatorDisposeBehavior,
+ parent: Navigator? = null,
+ stack: Stack = screens.toMutableStateStack(minSize = 1)
+) : Navigator(screens, key, stateHolder, disposeBehavior, parent, stack)
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 0aa0e187..36286380 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
@@ -19,6 +19,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
@@ -102,13 +103,14 @@ public fun Navigator(
}
}
-public class Navigator @InternalVoyagerApi constructor(
+public abstract class Navigator @InternalVoyagerApi constructor(
screens: List,
@InternalVoyagerApi public val key: String,
private val stateHolder: SaveableStateHolder,
public val disposeBehavior: NavigatorDisposeBehavior,
- public val parent: Navigator? = null
-) : Stack by screens.toMutableStateStack(minSize = 1) {
+ public val parent: Navigator? = null,
+ private val stack: Stack = screens.toMutableStateStack(minSize = 1)
+) : Stack by stack {
public val level: Int =
parent?.level?.inc() ?: 0
@@ -190,3 +192,8 @@ public data class NavigatorDisposeBehavior(
public fun compositionUniqueId(): String = currentCompositeKeyHash.toString(MaxSupportedRadix)
private val MaxSupportedRadix = 36
+
+public fun Navigator?.isPushLastEvent(): Boolean = this?.lastEvent == StackEvent.Push
+public fun Navigator?.isPopLastEvent(): Boolean = this?.lastEvent == StackEvent.Pop
+public fun Navigator?.isReplaceLastEvent(): Boolean = this?.lastEvent == StackEvent.Replace
+public fun Navigator?.isIdleLastEvent(): Boolean = this?.lastEvent == StackEvent.Idle
diff --git a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/NavigatorCreator.kt b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/NavigatorCreator.kt
new file mode 100644
index 00000000..5712b1c3
--- /dev/null
+++ b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/NavigatorCreator.kt
@@ -0,0 +1,24 @@
+package cafe.adriel.voyager.navigator
+
+import androidx.compose.runtime.saveable.SaveableStateHolder
+import cafe.adriel.voyager.core.screen.Screen
+
+public typealias NavigatorCreator = (
+ screens: List,
+ key: String,
+ stateHolder: SaveableStateHolder,
+ disposeBehavior: NavigatorDisposeBehavior,
+ parent: Navigator?
+) -> Navigator
+
+// Choose your Navigator
+public var summonNavigatorCreator: NavigatorCreator = DefaultNavigatorCreator()
+
+public fun DefaultNavigatorCreator(): NavigatorCreator = {
+ screenCollection: List,
+ s: String,
+ saveableStateHolder: SaveableStateHolder,
+ navigatorDisposeBehavior: NavigatorDisposeBehavior,
+ navigator: Navigator?, ->
+ DefaultNavigator(screenCollection, s, saveableStateHolder, navigatorDisposeBehavior, navigator)
+}
diff --git a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/NavigatorSaver.kt b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/NavigatorSaver.kt
index 86c1e41c..496fbd9e 100644
--- a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/NavigatorSaver.kt
+++ b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/NavigatorSaver.kt
@@ -33,10 +33,12 @@ public fun interface NavigatorSaver {
*
* If you want to use only Parcelable and want a NavigatorSaver that forces the use Parcelable, you can use [parcelableNavigatorSaver].
*/
-public fun defaultNavigatorSaver(): NavigatorSaver =
+public fun defaultNavigatorSaver(
+ navigatorCreator: NavigatorCreator = summonNavigatorCreator
+): NavigatorSaver =
NavigatorSaver { _, key, stateHolder, disposeBehavior, parent ->
listSaver(
save = { navigator -> navigator.items },
- restore = { items -> Navigator(items, key, stateHolder, disposeBehavior, parent) }
+ restore = { items -> navigatorCreator(items, key, stateHolder, disposeBehavior, parent) }
)
}
diff --git a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorSaverInternal.kt b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorSaverInternal.kt
index 4411f0d2..293f11cc 100644
--- a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorSaverInternal.kt
+++ b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorSaverInternal.kt
@@ -9,7 +9,9 @@ import androidx.compose.runtime.staticCompositionLocalOf
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigatorSaver
import cafe.adriel.voyager.navigator.Navigator
+import cafe.adriel.voyager.navigator.NavigatorCreator
import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
+import cafe.adriel.voyager.navigator.summonNavigatorCreator
internal val LocalNavigatorStateHolder: ProvidableCompositionLocal =
staticCompositionLocalOf { error("LocalNavigatorStateHolder not initialized") }
@@ -19,7 +21,8 @@ internal fun rememberNavigator(
screens: List,
key: String,
disposeBehavior: NavigatorDisposeBehavior,
- parent: Navigator?
+ parent: Navigator?,
+ navigatorCreator: NavigatorCreator = summonNavigatorCreator
): Navigator {
val stateHolder = LocalNavigatorStateHolder.current
val navigatorSaver = LocalNavigatorSaver.current
@@ -28,6 +31,6 @@ internal fun rememberNavigator(
}
return rememberSaveable(saver = saver, key = key) {
- Navigator(screens, key, stateHolder, disposeBehavior, parent)
+ navigatorCreator(screens, key, stateHolder, disposeBehavior, parent)
}
}