diff --git a/samples/android/src/main/AndroidManifest.xml b/samples/android/src/main/AndroidManifest.xml
index 96782b71..156c3bac 100644
--- a/samples/android/src/main/AndroidManifest.xml
+++ b/samples/android/src/main/AndroidManifest.xml
@@ -20,7 +20,8 @@
-
+
+
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..a56999b4 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
@@ -22,6 +22,8 @@ import cafe.adriel.voyager.sample.androidLegacy.LegacyActivity
import cafe.adriel.voyager.sample.androidViewModel.AndroidViewModelActivity
import cafe.adriel.voyager.sample.basicNavigation.BasicNavigationActivity
import cafe.adriel.voyager.sample.bottomSheetNavigation.BottomSheetNavigationActivity
+import cafe.adriel.voyager.sample.disposeSample.DisposeWhenStackChangedSampleActivity
+import cafe.adriel.voyager.sample.disposeSample.DisposeWhenTransitionFinishedSampleActivity
import cafe.adriel.voyager.sample.hiltIntegration.HiltMainActivity
import cafe.adriel.voyager.sample.kodeinIntegration.KodeinIntegrationActivity
import cafe.adriel.voyager.sample.koinIntegration.KoinIntegrationActivity
@@ -52,6 +54,8 @@ class SampleActivity : ComponentActivity() {
contentPadding = PaddingValues(24.dp)
) {
item {
+ StartSampleButton("DisposeWhenTransitionFinished")
+ StartSampleButton("DisposeWhenStackChanged")
StartSampleButton("SnapshotStateStack")
StartSampleButton("Basic Navigation")
StartSampleButton("Basic Navigation with Parcelable")
@@ -76,7 +80,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/disposeSample/DisposeSampleActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/disposeSample/DisposeSampleActivity.kt
new file mode 100644
index 00000000..cce30fde
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/disposeSample/DisposeSampleActivity.kt
@@ -0,0 +1,39 @@
+package cafe.adriel.voyager.sample.disposeSample
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.animation.core.tween
+import androidx.compose.runtime.Composable
+import cafe.adriel.voyager.navigator.DisposeStepsBehavior
+import cafe.adriel.voyager.navigator.Navigator
+import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
+import cafe.adriel.voyager.transitions.SlideTransition
+
+open class DisposeSampleActivity(
+ private val disposeStepsBehavior: DisposeStepsBehavior
+) : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ Content()
+ }
+ }
+
+ @Composable
+ fun Content() {
+ Navigator(
+ screen = SampleScreen(0),
+ disposeBehavior = NavigatorDisposeBehavior(
+ disposeStepsBehavior = disposeStepsBehavior
+ )
+ ) {
+ SlideTransition(
+ navigator = it,
+ animationSpec = tween(1000)
+ )
+ }
+ }
+}
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/disposeSample/DisposeWhenStackChangedSampleActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/disposeSample/DisposeWhenStackChangedSampleActivity.kt
new file mode 100644
index 00000000..2d99ecda
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/disposeSample/DisposeWhenStackChangedSampleActivity.kt
@@ -0,0 +1,5 @@
+package cafe.adriel.voyager.sample.disposeSample
+
+import cafe.adriel.voyager.navigator.DisposeStepsBehavior
+
+class DisposeWhenStackChangedSampleActivity : DisposeSampleActivity(DisposeStepsBehavior.DisposeWhenStackChanged)
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/disposeSample/DisposeWhenTransitionFinishedSampleActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/disposeSample/DisposeWhenTransitionFinishedSampleActivity.kt
new file mode 100644
index 00000000..e4091f5e
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/disposeSample/DisposeWhenTransitionFinishedSampleActivity.kt
@@ -0,0 +1,7 @@
+package cafe.adriel.voyager.sample.disposeSample
+
+import cafe.adriel.voyager.navigator.DisposeStepsBehavior
+
+class DisposeWhenTransitionFinishedSampleActivity : DisposeSampleActivity(
+ DisposeStepsBehavior.DisposeWhenTransitionFinished
+)
diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/disposeSample/SampleScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/disposeSample/SampleScreen.kt
new file mode 100644
index 00000000..79c12a86
--- /dev/null
+++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/disposeSample/SampleScreen.kt
@@ -0,0 +1,84 @@
+package cafe.adriel.voyager.sample.disposeSample
+
+import android.util.Log
+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.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+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 androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.compose.viewModel
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.screen.ScreenKey
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+class SampleScreenModel : ViewModel() {
+
+ init {
+ Log.d("SampleScreenModel", "start doing job")
+ viewModelScope.launch {
+ while (true) {
+ Log.d("SampleScreenModel", "Doing job")
+ delay(1000)
+ }
+ }
+ }
+
+ override fun onCleared() {
+ Log.d("SampleScreenModel", "Disposed")
+ }
+}
+
+data class SampleScreen(
+ private val index: Int
+) : Screen {
+
+ override val key: ScreenKey = "SampleScreen$index"
+
+ @Composable
+ override fun Content() {
+ if (index == 1) {
+ viewModel(key = key) { SampleScreenModel() }
+ }
+
+ val navigator = LocalNavigator.currentOrThrow
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(40.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(text = "Screen $index")
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { navigator.push(SampleScreen(index = index + 1)) }
+ ) {
+ Text(text = "Next")
+ }
+
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { navigator.pop() }
+ ) {
+ Text(text = "Back")
+ }
+ }
+ }
+}
diff --git a/voyager-navigator/build.gradle.kts b/voyager-navigator/build.gradle.kts
index 961ecd71..0ba3a960 100644
--- a/voyager-navigator/build.gradle.kts
+++ b/voyager-navigator/build.gradle.kts
@@ -15,6 +15,7 @@ kotlin {
sourceSets {
commonMain.dependencies {
api(projects.voyagerCore)
+ implementation(libs.coroutines.core)
compileOnly(compose.runtime)
compileOnly(compose.runtimeSaveable)
}
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 f9239a71..9d6e0399 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
@@ -23,15 +23,21 @@ import cafe.adriel.voyager.navigator.internal.ChildrenNavigationDisposableEffect
import cafe.adriel.voyager.navigator.internal.LocalNavigatorStateHolder
import cafe.adriel.voyager.navigator.internal.NavigatorBackHandler
import cafe.adriel.voyager.navigator.internal.NavigatorDisposableEffect
+import cafe.adriel.voyager.navigator.internal.StepDisposableAfterTransitionEffect
import cafe.adriel.voyager.navigator.internal.StepDisposableEffect
import cafe.adriel.voyager.navigator.internal.getNavigatorScreenLifecycleProvider
import cafe.adriel.voyager.navigator.internal.rememberNavigator
import cafe.adriel.voyager.navigator.lifecycle.NavigatorKey
+import kotlinx.coroutines.channels.Channel
public typealias NavigatorContent = @Composable (navigator: Navigator) -> Unit
public typealias OnBackPressed = ((currentScreen: Screen) -> Boolean)?
+@InternalVoyagerApi
+public val LocalTransitionFinishedEvents: ProvidableCompositionLocal> =
+ staticCompositionLocalOf { error("No LocalTransitionFinishedEvents provided") }
+
public val LocalNavigator: ProvidableCompositionLocal =
staticCompositionLocalOf { null }
@@ -86,11 +92,20 @@ public fun Navigator(
NavigatorDisposableEffect(navigator)
}
+ val transitionFinishedEvents = remember { Channel() }
+
CompositionLocalProvider(
- LocalNavigator provides navigator
+ LocalNavigator provides navigator,
+ LocalTransitionFinishedEvents provides transitionFinishedEvents
) {
- if (disposeBehavior.disposeSteps) {
- StepDisposableEffect(navigator)
+ when (disposeBehavior.disposeStepsBehavior) {
+ DisposeStepsBehavior.DisposeWhenTransitionFinished -> StepDisposableAfterTransitionEffect(
+ transitionFinishedEvents = transitionFinishedEvents,
+ navigator = navigator
+ )
+
+ DisposeStepsBehavior.DisposeWhenStackChanged -> StepDisposableEffect(navigator)
+ DisposeStepsBehavior.None -> Unit
}
NavigatorBackHandler(navigator, onBackPressed)
@@ -180,10 +195,27 @@ public class Navigator @InternalVoyagerApi constructor(
}
}
+public enum class DisposeStepsBehavior {
+ DisposeWhenTransitionFinished,
+ DisposeWhenStackChanged,
+ None
+}
+
public data class NavigatorDisposeBehavior(
val disposeNestedNavigators: Boolean = true,
- val disposeSteps: Boolean = true
-)
+ val disposeStepsBehavior: DisposeStepsBehavior
+) {
+ public constructor(
+ disposeNestedNavigators: Boolean = true,
+ disposeSteps: Boolean = true
+ ) : this(
+ disposeNestedNavigators = disposeNestedNavigators,
+ disposeStepsBehavior = when {
+ disposeSteps -> DisposeStepsBehavior.DisposeWhenStackChanged
+ else -> DisposeStepsBehavior.None
+ }
+ )
+}
@InternalVoyagerApi
@Composable
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..1421d3a0 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
@@ -2,14 +2,27 @@ package cafe.adriel.voyager.navigator.internal
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
import cafe.adriel.voyager.core.lifecycle.DisposableEffectIgnoringConfiguration
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.stack.StackEvent
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.lifecycle.NavigatorLifecycleStore
+import kotlinx.coroutines.channels.Channel
private val disposableEvents: Set =
setOf(StackEvent.Pop, StackEvent.Replace)
+private data class ScreenData(
+ val key: ScreenKey,
+ val screen: Screen
+)
+
@Composable
internal fun NavigatorDisposableEffect(
navigator: Navigator
@@ -40,6 +53,38 @@ internal fun StepDisposableEffect(
}
}
+@Composable
+internal fun StepDisposableAfterTransitionEffect(
+ transitionFinishedEvents: Channel,
+ navigator: Navigator
+) {
+ val screenCandidatesToDispose = rememberSaveable(saver = screenCandidatesToDisposeSaver()) {
+ mutableStateOf(emptySet())
+ }
+
+ val currentScreens = navigator.items
+
+ DisposableEffect(currentScreens) {
+ onDispose {
+ val newScreenKeys = navigator.items.map { it.key }
+ screenCandidatesToDispose.value += currentScreens.filter { it.key !in newScreenKeys }
+ .map { ScreenData(it.key, it) }
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ for (event in transitionFinishedEvents) {
+ val newScreens = navigator.items.map { it.key }
+ val screensToDispose = screenCandidatesToDispose.value.filterNot { it.key in newScreens }
+ if (screensToDispose.isNotEmpty()) {
+ screensToDispose.forEach { navigator.dispose(it.screen) }
+ navigator.clearEvent()
+ }
+ screenCandidatesToDispose.value = emptySet()
+ }
+ }
+}
+
@Composable
internal fun ChildrenNavigationDisposableEffect(
navigator: Navigator
@@ -80,3 +125,10 @@ internal fun disposeNavigator(navigator: Navigator) {
NavigatorLifecycleStore.remove(navigator)
navigator.clearEvent()
}
+
+private fun screenCandidatesToDisposeSaver(): Saver>, List> {
+ return Saver(
+ save = { it.value.toList() },
+ restore = { mutableStateOf(it.toSet()) }
+ )
+}
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 e0b1fb3d..ec84a6ae 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
@@ -6,12 +6,15 @@ import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.stack.StackEvent
+import cafe.adriel.voyager.navigator.LocalTransitionFinishedEvents
import cafe.adriel.voyager.navigator.Navigator
@ExperimentalVoyagerApi
@@ -55,6 +58,7 @@ public fun ScreenTransition(
)
}
+@OptIn(ExperimentalAnimationApi::class)
@Composable
public fun ScreenTransition(
navigator: Navigator,
@@ -90,6 +94,10 @@ public fun ScreenTransition(
},
modifier = modifier
) { screen ->
+ if (this.transition.targetState == this.transition.currentState) {
+ val transitionFinishedEvents = LocalTransitionFinishedEvents.current
+ LaunchedEffect(Unit) { transitionFinishedEvents.trySend(Unit) }
+ }
navigator.saveableState("transition", screen) {
content(screen)
}