diff --git a/samples/android/build.gradle.kts b/samples/android/build.gradle.kts
index 3dcaa778..a1c228ea 100644
--- a/samples/android/build.gradle.kts
+++ b/samples/android/build.gradle.kts
@@ -51,3 +51,7 @@ dependencies {
debugImplementation(libs.leakCanary)
}
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class).all {
+ kotlinOptions.freeCompilerArgs = listOf("-Xcontext-receivers")
+}
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..243fa533 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")
@@ -76,7 +78,9 @@ class SampleActivity : ComponentActivity() {
Button(
onClick = { context.startActivity(Intent(this, T::class.java)) },
- modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp)
) {
Text(text = text)
}
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenOne.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenOne.kt
index 63bff5e0..4781f603 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenOne.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenOne.kt
@@ -18,6 +18,7 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.hilt.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.navigator.push
class LegacyScreenOne : Screen {
override val key: ScreenKey = uniqueScreenKey
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenTwo.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenTwo.kt
index deceb287..bd637c4c 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenTwo.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenTwo.kt
@@ -18,6 +18,7 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.hilt.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.navigator.pop
class LegacyScreenTwo : Screen {
override val key: ScreenKey = uniqueScreenKey
@@ -47,7 +48,7 @@ class LegacyScreenTwo : Screen {
Spacer(modifier = Modifier.height(16.dp))
Button(
- onClick = navigator::pop,
+ onClick = { navigator.pop() },
content = { Text(text = "Go to One") }
)
}
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsScreen.kt
index 259a10d5..ced20387 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsScreen.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsScreen.kt
@@ -5,7 +5,9 @@ import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.navigator.pop
import cafe.adriel.voyager.sample.DetailsContent
import org.koin.androidx.compose.getViewModel
import org.koin.core.parameter.parametersOf
@@ -20,6 +22,8 @@ data class AndroidDetailsScreen(
val navigator = LocalNavigator.currentOrThrow
val viewModel = getViewModel { parametersOf(index) }
- DetailsContent(viewModel, "Item #${viewModel.index}", navigator::pop)
+ DetailsContent(viewModel, "Item #${viewModel.index}") {
+ navigator.pop()
+ }
}
}
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListScreen.kt
index fb2093c4..3634b8e3 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListScreen.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListScreen.kt
@@ -7,6 +7,7 @@ import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.navigator.push
import cafe.adriel.voyager.sample.ListContent
class AndroidListScreen : Screen {
@@ -17,6 +18,8 @@ class AndroidListScreen : Screen {
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel()
- ListContent(viewModel.items, onClick = { index -> navigator.push(AndroidDetailsScreen(index)) })
+ ListContent(viewModel.items, onClick = {
+ index -> navigator.push(AndroidDetailsScreen(index))
+ })
}
}
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/basicNavigation/BasicNavigationScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/basicNavigation/BasicNavigationScreen.kt
index 306cdec5..c1a54918 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/basicNavigation/BasicNavigationScreen.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/basicNavigation/BasicNavigationScreen.kt
@@ -22,6 +22,9 @@ 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
+import cafe.adriel.voyager.navigator.pop
+import cafe.adriel.voyager.navigator.push
+import cafe.adriel.voyager.navigator.replace
data class BasicNavigationScreen(
val index: Int,
@@ -62,7 +65,7 @@ data class BasicNavigationScreen(
) {
Button(
enabled = navigator.canPop,
- onClick = navigator::pop,
+ onClick = { navigator.pop() },
modifier = Modifier.weight(.5f)
) {
Text(text = "Pop")
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltDetailsScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltDetailsScreen.kt
index 83f2559c..0eff6d5f 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltDetailsScreen.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltDetailsScreen.kt
@@ -7,6 +7,7 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.hilt.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.navigator.pop
import cafe.adriel.voyager.sample.DetailsContent
data class HiltDetailsScreen(
@@ -28,6 +29,8 @@ data class HiltDetailsScreen(
factory.create(index)
}
- DetailsContent(viewModel, "Item #${viewModel.index}", navigator::pop)
+ DetailsContent(viewModel, "Item #${viewModel.index}") {
+ navigator.pop()
+ }
}
}
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltListScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltListScreen.kt
index af0a7214..9b330c75 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltListScreen.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltListScreen.kt
@@ -5,6 +5,7 @@ import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.hilt.getViewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.navigator.push
import cafe.adriel.voyager.sample.ListContent
class HiltListScreen : Screen {
@@ -17,6 +18,6 @@ class HiltListScreen : Screen {
// Uncomment version below if you want to use ScreenModel
// val viewModel: HiltListScreenModel = getScreenModel()
- ListContent(viewModel.items, onClick = { index -> navigator.push(HiltDetailsScreen(index)) })
+ ListContent(viewModel.items, onClick = { index -> navigator.push(HiltDetailsScreen(index)) } )
}
}
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/nestedNavigation/NestedNavigationActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/nestedNavigation/NestedNavigationActivity.kt
index d8f22a22..7f921aa3 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/nestedNavigation/NestedNavigationActivity.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/nestedNavigation/NestedNavigationActivity.kt
@@ -42,7 +42,7 @@ class NestedNavigationActivity : ComponentActivity() {
NestedNavigation(backgroundColor = Color.White) { navigator ->
CurrentScreen()
Button(
- onClick = { navigator.popUntilRoot() },
+ onClick = { navigator.popUntilRoot(navigator.lastItem) },
modifier = Modifier.padding(bottom = 16.dp)
) {
Text(text = "Pop Until Root")
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/parcelableScreen/SampleParcelableScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/parcelableScreen/SampleParcelableScreen.kt
index 3e94016e..0ef8ce55 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/parcelableScreen/SampleParcelableScreen.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/parcelableScreen/SampleParcelableScreen.kt
@@ -23,6 +23,9 @@ 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
+import cafe.adriel.voyager.navigator.pop
+import cafe.adriel.voyager.navigator.push
+import cafe.adriel.voyager.navigator.replace
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -70,7 +73,7 @@ data class SampleParcelableScreen(
) {
Button(
enabled = navigator.canPop,
- onClick = navigator::pop,
+ onClick = { navigator.pop() },
modifier = Modifier.weight(.5f)
) {
Text(text = "Pop")
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreen.kt
index 1529a679..010e3910 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreen.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreen.kt
@@ -11,6 +11,7 @@ import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.navigator.pop
import cafe.adriel.voyager.sample.DetailsContent
import cafe.adriel.voyager.sample.LoadingContent
@@ -28,7 +29,9 @@ data class DetailsScreen(
when (val result = state) {
is DetailsScreenModel.State.Loading -> LoadingContent()
- is DetailsScreenModel.State.Result -> DetailsContent(screenModel, result.item, navigator::pop)
+ is DetailsScreenModel.State.Result -> DetailsContent(screenModel, result.item) {
+ navigator.pop()
+ }
}
LaunchedEffect(currentCompositeKeyHash) {
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreen.kt
index d52789bb..b1008f6e 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreen.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreen.kt
@@ -7,6 +7,7 @@ import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.navigator.push
import cafe.adriel.voyager.sample.ListContent
class ListScreen : Screen {
@@ -18,6 +19,8 @@ class ListScreen : Screen {
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { ListScreenModel() }
- ListContent(screenModel.items, onClick = { index -> navigator.push(DetailsScreen(index)) })
+ ListContent(screenModel.items, onClick = {
+ index -> navigator.push(DetailsScreen(index)) }
+ )
}
}
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/stateStack/StateStackActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/stateStack/StateStackActivity.kt
index ee4e355b..c3774493 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/stateStack/StateStackActivity.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/stateStack/StateStackActivity.kt
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.Button
import androidx.compose.material.Text
@@ -83,7 +82,7 @@ class StateStackActivity : ComponentActivity() {
if (stateStack.lastItemOrNull == selectedItem) {
selectItem("")
}
- stateStack.pop()
+ stateStack.pop("Pop")
}
ActionButton(text = "Pop Until", enabled = stateStack.canPop) {
if (selectedItem.isBlank()) {
@@ -91,27 +90,27 @@ class StateStackActivity : ComponentActivity() {
return@ActionButton
}
selectItem("")
- stateStack.popUntil { it == selectedItem }
+ stateStack.popUntil("Pop Until") { it == selectedItem }
}
ActionButton(text = "Push") {
- stateStack.push(randomValue)
+ stateStack.push("Push", randomValue)
}
}
Row(
modifier = Modifier.weight(.1f)
) {
ActionButton(text = "Replace") {
- stateStack.replace(randomValue)
+ stateStack.replace("Replace", randomValue)
}
ActionButton(text = "Replace All") {
- stateStack.replaceAll(randomValue)
+ stateStack.replaceAll("Replace All", randomValue)
}
}
}
}
@Composable
- private fun LazyItemScope.ListItem(
+ private fun ListItem(
index: Int,
item: String,
isLast: Boolean,
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/TabNavigationActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/TabNavigationActivity.kt
index 26e73ad6..016b4a7b 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/TabNavigationActivity.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/TabNavigationActivity.kt
@@ -67,7 +67,7 @@ class TabNavigationActivity : ComponentActivity() {
BottomNavigationItem(
selected = tabNavigator.current.key == tab.key,
- onClick = { tabNavigator.current = tab },
+ onClick = { tabNavigator.setCurrent(invoker = tabNavigator.current, newTab = tab) },
icon = { Icon(painter = tab.options.icon!!, contentDescription = tab.options.title) }
)
}
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/tabs/TabContent.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/tabs/TabContent.kt
index aab8f17b..64165a85 100644
--- a/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/tabs/TabContent.kt
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/tabs/TabContent.kt
@@ -34,7 +34,7 @@ fun Tab.TabContent() {
Column {
InnerTabNavigation()
screen.Content()
- Log.d("Navigator", "Last Event: ${navigator.lastEvent}")
+ Log.d("Navigator", "Last Event: ${navigator.lastAction.event}")
}
}
}
@@ -65,7 +65,7 @@ private fun RowScope.TabNavigationButton(
Button(
enabled = tabNavigator.current.key != tab.key,
- onClick = { tabNavigator.current = tab },
+ onClick = { tabNavigator.setCurrent(invoker = tabNavigator.current, newTab = tab) },
modifier = Modifier.weight(1f)
) {
Text(text = tab.options.title)
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/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..ff77f6a2
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt
@@ -0,0 +1,164 @@
+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.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.() -> 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 = 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..5cad0df5
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt
@@ -0,0 +1,60 @@
+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
+import cafe.adriel.voyager.navigator.push
+
+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-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/BottomSheetNavigator.kt b/voyager-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/BottomSheetNavigator.kt
index 91384caf..590ac4d5 100644
--- a/voyager-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/BottomSheetNavigator.kt
+++ b/voyager-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/BottomSheetNavigator.kt
@@ -26,8 +26,10 @@ import androidx.compose.ui.unit.dp
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.WithLastActionStack
import cafe.adriel.voyager.navigator.CurrentScreen
import cafe.adriel.voyager.navigator.Navigator
+import cafe.adriel.voyager.navigator.NavigatorOld
import cafe.adriel.voyager.navigator.bottomSheet.internal.BottomSheetNavigatorBackHandler
import cafe.adriel.voyager.navigator.compositionUniqueId
import kotlinx.coroutines.CoroutineScope
@@ -98,19 +100,57 @@ public fun BottomSheetNavigator(
}
}
+@OptIn(ExperimentalMaterialApi::class)
+public class BottomSheetNavigatorSimple @InternalVoyagerApi constructor(
+ private val navigator: NavigatorOld,
+ private val sheetState: ModalBottomSheetState,
+ private val coroutineScope: CoroutineScope
+) : Stack by navigator {
+
+ public val isVisible: Boolean
+ get() = sheetState.isVisible
+
+ public fun show(screen: Screen) {
+ coroutineScope.launch {
+ navigator.replaceAll(screen)
+ sheetState.show()
+ }
+ }
+
+ public fun hide() {
+ coroutineScope.launch {
+ if (isVisible) {
+ sheetState.hide()
+ navigator.replaceAll(HiddenBottomSheetScreen)
+ } else if (sheetState.targetValue == ModalBottomSheetValue.Hidden) {
+ // Swipe down - sheetState is already hidden here so `isVisible` is false
+ navigator.replaceAll(HiddenBottomSheetScreen)
+ }
+ }
+ }
+
+ @Composable
+ public fun saveableState(
+ key: String,
+ content: @Composable () -> Unit
+ ) {
+ navigator.saveableState(key, content = content)
+ }
+}
+
@OptIn(ExperimentalMaterialApi::class)
public class BottomSheetNavigator @InternalVoyagerApi constructor(
private val navigator: Navigator,
private val sheetState: ModalBottomSheetState,
private val coroutineScope: CoroutineScope
-) : Stack by navigator {
+) : WithLastActionStack by navigator {
public val isVisible: Boolean
get() = sheetState.isVisible
public fun show(screen: Screen) {
coroutineScope.launch {
- replaceAll(screen)
+ navigator.replaceAll(navigator.lastItem, screen)
sheetState.show()
}
}
@@ -119,10 +159,10 @@ public class BottomSheetNavigator @InternalVoyagerApi constructor(
coroutineScope.launch {
if (isVisible) {
sheetState.hide()
- replaceAll(HiddenBottomSheetScreen)
+ navigator.replaceAll(navigator.lastItem, HiddenBottomSheetScreen)
} else if (sheetState.targetValue == ModalBottomSheetValue.Hidden) {
// Swipe down - sheetState is already hidden here so `isVisible` is false
- replaceAll(HiddenBottomSheetScreen)
+ navigator.replaceAll(navigator.lastItem, HiddenBottomSheetScreen)
}
}
}
diff --git a/voyager-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/internal/BottomSheetNavigatorBackHandler.kt b/voyager-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/internal/BottomSheetNavigatorBackHandler.kt
index bdb13b89..7858a944 100644
--- a/voyager-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/internal/BottomSheetNavigatorBackHandler.kt
+++ b/voyager-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/internal/BottomSheetNavigatorBackHandler.kt
@@ -16,8 +16,10 @@ internal fun BottomSheetNavigatorBackHandler(
hideOnBackPress: Boolean
) {
BackHandler(enabled = sheetState.isVisible) {
- if (navigator.pop().not() && hideOnBackPress) {
- navigator.hide()
+ navigator.lastItemOrNull?.let {
+ if (navigator.pop(it).not() && hideOnBackPress) {
+ navigator.hide()
+ }
}
}
}
diff --git a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/SnapshotStatePropertyHolderStack.kt b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/SnapshotStatePropertyHolderStack.kt
new file mode 100644
index 00000000..52f13df3
--- /dev/null
+++ b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/SnapshotStatePropertyHolderStack.kt
@@ -0,0 +1,44 @@
+package cafe.adriel.voyager.core.stack
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.runtime.toMutableStateList
+
+public open class SnapshotStatePropertyHolderStack- (
+ items: List
- ,
+ minSize: Int = 0
+) : PropertyHolderStack
- {
+
+ init {
+ require(minSize >= 0) { "Min size $minSize is less than zero" }
+ require(items.size >= minSize) { "Stack size ${items.size} is less than the min size $minSize" }
+ }
+
+ @PublishedApi
+ internal val stateStack: SnapshotStateList
- = items.toMutableStateList()
+
+ public override val items: List
- by derivedStateOf {
+ stateStack.toList()
+ }
+
+ public override val lastItemOrNull: Item? by derivedStateOf {
+ stateStack.lastOrNull()
+ }
+
+ public override val size: Int by derivedStateOf {
+ stateStack.size
+ }
+
+ public override val isEmpty: Boolean by derivedStateOf {
+ stateStack.isEmpty()
+ }
+
+ public override val canPop: Boolean by derivedStateOf {
+ stateStack.size > minSize
+ }
+}
diff --git a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/SnapshotStateStack.kt b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/SnapshotStateStack.kt
index d6e7adb8..d6d9330c 100644
--- a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/SnapshotStateStack.kt
+++ b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/SnapshotStateStack.kt
@@ -1,7 +1,6 @@
package cafe.adriel.voyager.core.stack
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.neverEqualPolicy
@@ -9,48 +8,35 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.SnapshotStateList
-import androidx.compose.runtime.toMutableStateList
-public fun
- List
- .toMutableStateStack(
+public fun
- List
- .toMutableSnapshotStateStack(
minSize: Int = 0
-): SnapshotStateStack
- =
- SnapshotStateStack(this, minSize)
+): SnapshotStateStack
- = SnapshotStateStack(this, minSize)
-public fun
- mutableStateStackOf(
+public fun
- mutableSnapshotStateStackOf(
vararg items: Item,
minSize: Int = 0
-): SnapshotStateStack
- =
- SnapshotStateStack(*items, minSize = minSize)
+): SnapshotStateStack
- = SnapshotStateStack((items as List
- ), minSize = minSize)
@Composable
-public fun
- rememberStateStack(
- vararg items: Item,
- minSize: Int = 0
-): SnapshotStateStack
- =
- rememberStateStack(items.toList(), minSize)
-
-@Composable
-public fun
- rememberStateStack(
+public fun
- rememberSnapshotStateStack(
items: List
- ,
minSize: Int = 0
-): SnapshotStateStack
- =
- rememberSaveable(saver = stackSaver(minSize)) {
- SnapshotStateStack(items, minSize)
- }
+): SnapshotStateStack
- = rememberSaveable(saver = stackSaver(minSize)) {
+ SnapshotStateStack(items, minSize)
+}
private fun
- stackSaver(
minSize: Int
-): Saver, Any> =
- listSaver(
- save = { stack -> stack.items },
- restore = { items -> SnapshotStateStack(items, minSize) }
- )
+): Saver, Any> = listSaver(
+ save = { stack -> stack.items },
+ restore = { items -> SnapshotStateStack(items, minSize) }
+)
public class SnapshotStateStack
- (
items: List
- ,
minSize: Int = 0
-) : Stack
- {
+) : SnapshotStatePropertyHolderStack
- (items, minSize), Stack
- {
public constructor(
vararg items: Item,
@@ -60,37 +46,9 @@ public class SnapshotStateStack
- (
minSize = minSize
)
- init {
- require(minSize >= 0) { "Min size $minSize is less than zero" }
- require(items.size >= minSize) { "Stack size ${items.size} is less than the min size $minSize" }
- }
-
- @PublishedApi
- internal val stateStack: SnapshotStateList
- = items.toMutableStateList()
-
public override var lastEvent: StackEvent by mutableStateOf(StackEvent.Idle, neverEqualPolicy())
private set
- public override val items: List
- by derivedStateOf {
- stateStack.toList()
- }
-
- public override val lastItemOrNull: Item? by derivedStateOf {
- stateStack.lastOrNull()
- }
-
- public override val size: Int by derivedStateOf {
- stateStack.size
- }
-
- public override val isEmpty: Boolean by derivedStateOf {
- stateStack.isEmpty()
- }
-
- public override val canPop: Boolean by derivedStateOf {
- stateStack.size > minSize
- }
-
public override infix fun push(item: Item) {
stateStack += item
lastEvent = StackEvent.Push
@@ -131,10 +89,6 @@ public class SnapshotStateStack
- (
false
}
- public override fun popAll() {
- popUntil { false }
- }
-
public override infix fun popUntil(predicate: (Item) -> Boolean): Boolean {
var success = false
val shouldPop = {
@@ -154,6 +108,10 @@ public class SnapshotStateStack
- (
return success
}
+ public override fun popAll() {
+ popUntil { false }
+ }
+
public override operator fun plusAssign(item: Item) {
push(item)
}
diff --git a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/Stack.kt b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/Stack.kt
index bf65e8fd..0dcd2cfb 100644
--- a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/Stack.kt
+++ b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/Stack.kt
@@ -10,12 +10,10 @@ public enum class StackEvent {
Idle
}
-public interface Stack
- {
+public interface PropertyHolderStack
- {
public val items: List
-
- public val lastEvent: StackEvent
-
public val lastItemOrNull: Item?
public val size: Int
@@ -23,6 +21,11 @@ public interface Stack
- {
public val isEmpty: Boolean
public val canPop: Boolean
+}
+
+public interface Stack
- : PropertyHolderStack
- {
+
+ public val lastEvent: StackEvent
public infix fun push(item: Item)
@@ -34,15 +37,47 @@ public interface Stack
- {
public infix fun replaceAll(items: List
- )
+ public infix fun popUntil(predicate: (Item) -> Boolean): Boolean
+
public fun pop(): Boolean
public fun popAll()
- public infix fun popUntil(predicate: (Item) -> Boolean): Boolean
-
public operator fun plusAssign(item: Item)
public operator fun plusAssign(items: List
- )
public fun clearEvent()
}
+
+public data class StackLastAction
- (
+ val invoker: Item?,
+ val event: StackEvent,
+)
+
+/**
+ * A [PropertyHolderStack] a stack that keeps track of the last action performed in it.
+ * Crucial API difference from [Stack] is that this interface can't perform infix or operator functions.
+ */
+public interface WithLastActionStack
- : PropertyHolderStack
- {
+
+ public val lastAction: StackLastAction
-
+
+ public fun push(invoker: Item, item: Item)
+
+ public fun push(invoker: Item, items: List
- )
+
+ public fun replace(invoker: Item, item: Item)
+
+ public fun replaceAll(invoker: Item, item: Item)
+
+ public fun replaceAll(invoker: Item, items: List
- )
+
+ public fun pop(invoker: Item): Boolean
+
+ public fun popUntil(invoker: Item, predicate: (Item) -> Boolean): Boolean
+
+ public fun popAll(invoker: Item)
+
+ public fun clearEvent(invoker: Item)
+}
diff --git a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/WithLastActionSnapshotStackState.kt b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/WithLastActionSnapshotStackState.kt
new file mode 100644
index 00000000..353c33e9
--- /dev/null
+++ b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/WithLastActionSnapshotStackState.kt
@@ -0,0 +1,127 @@
+package cafe.adriel.voyager.core.stack
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.neverEqualPolicy
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+
+public fun
- List
- .toMutableStateStack(
+ minSize: Int = 0
+): WithLastActionSnapshotStackState
- = WithLastActionSnapshotStackState(this, minSize)
+
+public fun
- mutableStateStackOf(
+ vararg items: Item,
+ minSize: Int = 0
+): WithLastActionSnapshotStackState
- = WithLastActionSnapshotStackState((items as List
- ), minSize = minSize)
+
+@Composable
+public fun
- rememberStateStack(
+ vararg items: Item,
+ minSize: Int = 0
+): WithLastActionSnapshotStackState
- = rememberStateStack(items.toList(), minSize)
+
+@Composable
+public fun
- rememberStateStack(
+ items: List
- ,
+ minSize: Int = 0
+): WithLastActionSnapshotStackState
- = rememberSaveable(saver = stackSaver(minSize)) {
+ WithLastActionSnapshotStackState(items, minSize)
+}
+
+private fun
- stackSaver(
+ minSize: Int
+): Saver, Any> = listSaver(
+ save = { stack -> stack.items },
+ restore = { items -> WithLastActionSnapshotStackState(items, minSize) }
+)
+
+public class WithLastActionSnapshotStackState
- (
+ items: List
- ,
+ minSize: Int = 1
+) : SnapshotStatePropertyHolderStack
- (items, minSize), WithLastActionStack
- {
+
+ public constructor(
+ vararg items: Item,
+ minSize: Int = 0
+ ) : this(
+ items = items.toList(),
+ minSize = minSize
+ )
+
+ public override var lastAction: StackLastAction
- by mutableStateOf(
+ value = StackLastAction(null, StackEvent.Idle), policy = neverEqualPolicy()
+ )
+ private set
+
+ override fun push(invoker: Item, item: Item) {
+ stateStack += item
+ lastAction = StackLastAction(invoker, StackEvent.Push)
+ }
+
+ override fun push(invoker: Item, items: List
- ) {
+ stateStack += items
+ lastAction = StackLastAction(invoker, StackEvent.Push)
+ }
+
+ override fun replace(invoker: Item, item: Item) {
+ if (stateStack.isEmpty()) {
+ push(invoker, item)
+ } else {
+ stateStack[stateStack.lastIndex] = item
+ }
+ lastAction = StackLastAction(invoker, StackEvent.Replace)
+ }
+
+ override fun replaceAll(invoker: Item, item: Item) {
+ stateStack.clear()
+ stateStack += item
+ lastAction = StackLastAction(invoker, StackEvent.Replace)
+ }
+
+ override fun replaceAll(invoker: Item, items: List
- ) {
+ stateStack.clear()
+ stateStack += items
+ lastAction = StackLastAction(invoker, StackEvent.Replace)
+ }
+
+ override fun pop(invoker: Item): Boolean {
+ return if (canPop) {
+ stateStack.removeLast()
+ lastAction = StackLastAction(invoker, StackEvent.Pop)
+ true
+ } else {
+ false
+ }
+ }
+
+ override fun popUntil(invoker: Item, predicate: (Item) -> Boolean): Boolean {
+ var success = false
+ val shouldPop = {
+ lastItemOrNull
+ ?.let(predicate)
+ ?.also { success = it }
+ ?.not()
+ ?: false
+ }
+
+ while (canPop && shouldPop()) {
+ stateStack.removeLast()
+ }
+
+ lastAction = StackLastAction(invoker, StackEvent.Pop)
+
+ return success
+ }
+
+ override fun popAll(invoker: Item) {
+ popUntil(invoker) { false }
+ }
+
+ override fun clearEvent(invoker: Item) {
+ lastAction = StackLastAction(invoker, StackEvent.Idle)
+ }
+}
diff --git a/voyager-navigator/build.gradle.kts b/voyager-navigator/build.gradle.kts
index b717c6eb..1b1a8be8 100644
--- a/voyager-navigator/build.gradle.kts
+++ b/voyager-navigator/build.gradle.kts
@@ -35,3 +35,7 @@ kotlin {
}
}
}
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class).all {
+ kotlinOptions.freeCompilerArgs = listOf("-Xcontext-receivers")
+}
diff --git a/voyager-navigator/src/androidMain/kotlin/cafe/adriel/voyager/navigator/NavigatorComposeUtils.kt b/voyager-navigator/src/androidMain/kotlin/cafe/adriel/voyager/navigator/NavigatorComposeUtils.kt
new file mode 100644
index 00000000..15dccbcd
--- /dev/null
+++ b/voyager-navigator/src/androidMain/kotlin/cafe/adriel/voyager/navigator/NavigatorComposeUtils.kt
@@ -0,0 +1,50 @@
+package cafe.adriel.voyager.navigator
+
+import cafe.adriel.voyager.core.screen.Screen
+
+public fun Screen.self() : Screen = this
+
+context(Screen)
+public fun Navigator.push(screen: Screen) {
+ push(self(), screen)
+}
+
+context(Screen)
+public fun Navigator.push(screens: List) {
+ push(self(), screens)
+}
+
+context(Screen)
+public fun Navigator.replace(screen: Screen) {
+ replace(self(), screen)
+}
+
+context(Screen)
+public fun Navigator.replaceAll(screen: Screen) {
+ replaceAll(self(), screen)
+}
+
+context(Screen)
+public fun Navigator.replaceAll(screens: List) {
+ replaceAll(self(), screens)
+}
+
+context(Screen)
+public fun Navigator.pop() {
+ pop(self())
+}
+
+context(Screen)
+public fun Navigator.popUntil(predicate: (Screen) -> Boolean) {
+ popUntil(self(), predicate)
+}
+
+context(Screen)
+public fun Navigator.popAll() {
+ popAll(self())
+}
+
+context(Screen)
+public fun Navigator.clearEvent() {
+ clearEvent(self())
+}
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..61141f7e 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
@@ -3,6 +3,7 @@ package cafe.adriel.voyager.navigator
import android.os.Parcelable
import androidx.compose.runtime.saveable.listSaver
import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.stack.WithLastActionStack
/**
* Navigator Saver that forces all Screens be [Parcelable], if not, it will throw a exception while trying to save
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..2c0c7ec5 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
@@ -18,8 +18,10 @@ import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleStore
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.SnapshotStateStack
import cafe.adriel.voyager.core.stack.Stack
-import cafe.adriel.voyager.core.stack.toMutableStateStack
+import cafe.adriel.voyager.core.stack.WithLastActionSnapshotStackState
+import cafe.adriel.voyager.core.stack.WithLastActionStack
import cafe.adriel.voyager.navigator.internal.ChildrenNavigationDisposableEffect
import cafe.adriel.voyager.navigator.internal.LocalNavigatorStateHolder
import cafe.adriel.voyager.navigator.internal.NavigatorBackHandler
@@ -108,7 +110,7 @@ public class Navigator @InternalVoyagerApi constructor(
private val stateHolder: SaveableStateHolder,
public val disposeBehavior: NavigatorDisposeBehavior,
public val parent: Navigator? = null
-) : Stack by screens.toMutableStateStack(minSize = 1) {
+) : WithLastActionStack by WithLastActionSnapshotStackState(screens, 1) {
public val level: Int =
parent?.level?.inc() ?: 0
@@ -152,15 +154,15 @@ public class Navigator @InternalVoyagerApi constructor(
)
}
- public fun popUntilRoot() {
- popUntilRoot(this)
+ public fun popUntilRoot(invoker: Screen) {
+ popUntilRoot(invoker, this)
}
- private tailrec fun popUntilRoot(navigator: Navigator) {
- navigator.popAll()
+ private tailrec fun popUntilRoot(invoker: Screen, navigator: Navigator) {
+ navigator.popAll(invoker)
if (navigator.parent != null) {
- popUntilRoot(navigator.parent)
+ popUntilRoot(invoker, navigator.parent)
}
}
diff --git a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/NavigatorOld.kt b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/NavigatorOld.kt
new file mode 100644
index 00000000..e24e0364
--- /dev/null
+++ b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/NavigatorOld.kt
@@ -0,0 +1,96 @@
+package cafe.adriel.voyager.navigator
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.SaveableStateHolder
+import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
+import cafe.adriel.voyager.core.concurrent.ThreadSafeMap
+import cafe.adriel.voyager.core.concurrent.ThreadSafeSet
+import cafe.adriel.voyager.core.lifecycle.MultipleProvideBeforeScreenContent
+import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleStore
+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.SnapshotStateStack
+import cafe.adriel.voyager.core.stack.Stack
+import cafe.adriel.voyager.navigator.lifecycle.NavigatorKey
+
+public class NavigatorOld @InternalVoyagerApi constructor(
+ screens: List,
+ @InternalVoyagerApi public val key: String,
+ private val stateHolder: SaveableStateHolder,
+ public val disposeBehavior: NavigatorDisposeBehavior,
+ public val parent: NavigatorOld? = null
+) : Stack by SnapshotStateStack(screens, 0) {
+
+ public val level: Int =
+ parent?.level?.inc() ?: 0
+
+ public val lastItem: Screen by derivedStateOf {
+ lastItemOrNull ?: error("Navigator has no screen")
+ }
+
+ private val stateKeys = ThreadSafeSet()
+
+ internal val children = ThreadSafeMap()
+
+ @Composable
+ public fun saveableState(
+ key: String,
+ screen: Screen = lastItem,
+ content: @Composable () -> Unit
+ ) {
+ val stateKey = "${screen.key}:$key"
+ stateKeys += stateKey
+
+ @Composable
+ fun provideSaveableState(suffixKey: String, content: @Composable () -> Unit) {
+ val providedStateKey = "$stateKey:$suffixKey"
+ stateKeys += providedStateKey
+ stateHolder.SaveableStateProvider(providedStateKey, content)
+ }
+
+ val lifecycleOwner = rememberScreenLifecycleOwner(screen)
+ val navigatorScreenLifecycleOwners = getNavigatorScreenLifecycleProvider(screen)
+
+ val composed = remember(lifecycleOwner, navigatorScreenLifecycleOwners) {
+ listOf(lifecycleOwner) + navigatorScreenLifecycleOwners
+ }
+ MultipleProvideBeforeScreenContent(
+ screenLifecycleContentProviders = composed,
+ provideSaveableState = { suffix, content -> provideSaveableState(suffix, content) },
+ content = {
+ stateHolder.SaveableStateProvider(stateKey, content)
+ }
+ )
+ }
+
+ public fun popUntilRoot() {
+ popUntilRoot(this)
+ }
+
+ private tailrec fun popUntilRoot(navigator: NavigatorOld) {
+ navigator.popAll()
+
+ if (navigator.parent != null) {
+ popUntilRoot(navigator.parent)
+ }
+ }
+
+ @InternalVoyagerApi
+ public fun dispose(
+ screen: Screen
+ ) {
+ ScreenLifecycleStore.remove(screen)
+ stateKeys
+ .toSet() // Copy
+ .asSequence()
+ .filter { it.startsWith(screen.key) }
+ .forEach { key ->
+ stateHolder.removeState(key)
+ stateKeys -= key
+ }
+ }
+}
diff --git a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorBackHandler.kt b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorBackHandler.kt
index 6eca94e9..ad9205ce 100644
--- a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorBackHandler.kt
+++ b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorBackHandler.kt
@@ -16,9 +16,11 @@ internal fun NavigatorBackHandler(
BackHandler(
enabled = navigator.canPop || navigator.parent?.canPop ?: false,
onBack = {
- if (onBackPressed(navigator.lastItem)) {
- if (navigator.pop().not()) {
- navigator.parent?.pop()
+ with(navigator) {
+ if (onBackPressed(lastItem)) {
+ if (pop(lastItem).not()) {
+ parent?.pop(lastItem)
+ }
}
}
}
diff --git a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorDisposable.kt b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorDisposable.kt
index 81408884..12844b46 100644
--- a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorDisposable.kt
+++ b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorDisposable.kt
@@ -30,11 +30,11 @@ internal fun StepDisposableEffect(
DisposableEffect(currentScreens) {
onDispose {
val newScreenKeys = navigator.items.map { it.key }
- if (navigator.lastEvent in disposableEvents) {
+ if (navigator.lastAction.event in disposableEvents) {
currentScreens.filter { it.key !in newScreenKeys }.forEach {
navigator.dispose(it)
}
- navigator.clearEvent()
+ navigator.clearEvent(navigator.lastItem)
}
}
}
@@ -78,5 +78,5 @@ internal fun disposeNavigator(navigator: Navigator) {
navigator.dispose(screen)
}
NavigatorLifecycleStore.remove(navigator)
- navigator.clearEvent()
+ navigator.clearEvent(navigator.lastItem)
}
diff --git a/voyager-tab-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/tab/TabNavigator.kt b/voyager-tab-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/tab/TabNavigator.kt
index 7c93a515..07cf37e7 100644
--- a/voyager-tab-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/tab/TabNavigator.kt
+++ b/voyager-tab-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/tab/TabNavigator.kt
@@ -61,7 +61,12 @@ public class TabNavigator internal constructor(
public var current: Tab
get() = navigator.lastItem as Tab
- set(tab) = navigator.replaceAll(tab)
+ private set(value) {}
+
+ public fun setCurrent(invoker: Tab, newTab: Tab) {
+ navigator.replaceAll(invoker, newTab)
+ current = newTab
+ }
@Composable
public fun saveableState(
diff --git a/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScaleTransition.kt b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScaleTransition.kt
index ee957af1..bc920f6d 100644
--- a/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScaleTransition.kt
+++ b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScaleTransition.kt
@@ -26,7 +26,7 @@ public fun ScaleTransition(
modifier = modifier,
content = content,
transition = {
- val (initialScale, targetScale) = when (navigator.lastEvent) {
+ val (initialScale, targetScale) = when (navigator.lastAction.event) {
StackEvent.Pop -> ExitScales
else -> EnterScales
}
diff --git a/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScreenTransition.kt b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScreenTransition.kt
index c09bb2d8..6eb9c7fa 100644
--- a/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScreenTransition.kt
+++ b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScreenTransition.kt
@@ -25,7 +25,7 @@ public fun ScreenTransition(
modifier = modifier,
content = content,
transition = {
- when (navigator.lastEvent) {
+ when (navigator.lastAction.event) {
StackEvent.Pop -> exitTransition()
else -> enterTransition()
}
diff --git a/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/SlideTransition.kt b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/SlideTransition.kt
index efb5311b..668dfe39 100644
--- a/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/SlideTransition.kt
+++ b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/SlideTransition.kt
@@ -12,6 +12,7 @@ import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
+import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.stack.StackEvent
import cafe.adriel.voyager.navigator.Navigator
@@ -31,7 +32,7 @@ public fun SlideTransition(
modifier = modifier,
content = content,
transition = {
- val (initialOffset, targetOffset) = when (navigator.lastEvent) {
+ val (initialOffset, targetOffset) = when (navigator.lastAction.event) {
StackEvent.Pop -> ({ size: Int -> -size }) to ({ size: Int -> size })
else -> ({ size: Int -> size }) to ({ size: Int -> -size })
}
@@ -39,10 +40,10 @@ public fun SlideTransition(
when (orientation) {
SlideOrientation.Horizontal ->
slideInHorizontally(animationSpec, initialOffset) togetherWith
- slideOutHorizontally(animationSpec, targetOffset)
+ slideOutHorizontally(animationSpec, targetOffset)
SlideOrientation.Vertical ->
slideInVertically(animationSpec, initialOffset) togetherWith
- slideOutVertically(animationSpec, targetOffset)
+ slideOutVertically(animationSpec, targetOffset)
}
}
)