diff --git a/.github/workflows/docs-publish.yaml b/.github/workflows/docs-publish.yaml index 16e36c78..8e22d4b6 100644 --- a/.github/workflows/docs-publish.yaml +++ b/.github/workflows/docs-publish.yaml @@ -1,8 +1,6 @@ name: Docs Publish on: push: - branches: [main, mkdocs] - pull_request: branches: [main] jobs: diff --git a/docs/transitions-api.md b/docs/transitions-api.md index c8428e9d..9e05530b 100644 --- a/docs/transitions-api.md +++ b/docs/transitions-api.md @@ -13,6 +13,25 @@ setContent { } ``` +!!! error + There is a know bug using any Transition APIs can leaky ScreenModels or ViewModels, this happens because Voyager by default + dispose Screens in the next Composition tick after a `pop` or `replace` is called, but the transition only finish later, so + the ScreenModel or ViewModel is re created or cleared to early. For this purpose since Voyager `1.1.0-beta02` we have introduce + a new API to fix this issue. For more details on the issue see [#106](https://github.com/adrielcafe/voyager/issues/106). + + ```kotlin + Navigator( + screen = ..., + disposeBehavior = NavigatorDisposeBehavior(disposeSteps = false), + ) { navigator -> + SlideTransition( + navigator = navigator, + ... + disposeScreenAfterTransitionEnd = true + ) + } + ``` + !!! warning Have encounter `Screen was used multiple times` crash? Provide a `uniqueScreenKey` for your Screens diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/screenTransition/SampleScreens.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/screenTransition/SampleScreens.kt index a19b0da0..ed862748 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/screenTransition/SampleScreens.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/screenTransition/SampleScreens.kt @@ -9,12 +9,15 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.transitions.ScreenTransition @@ -27,6 +30,18 @@ private val colors = listOf( Color.Black ) +class BaseSampleScreenModel( + val index: Int +) : ScreenModel { + + init { + println("Init BaseSampleScreenModel $index") + } + override fun onDispose() { + println("Disposing BaseSampleScreenModel $index") + } +} + abstract class BaseSampleScreen( private val transitionType: String ) : Screen { @@ -37,10 +52,16 @@ abstract class BaseSampleScreen( @Composable override fun Content() { + val model = rememberScreenModel { + BaseSampleScreenModel(index) + } + val color = remember { + colors.getOrNull(index % colors.size) ?: colors.random() + } Column( modifier = Modifier .fillMaxSize() - .background(colors[index % colors.size].copy(alpha = 0.3f)) + .background(color.copy(alpha = 0.3f)) .padding(40.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/screenTransition/ScreenTransitionActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/screenTransition/ScreenTransitionActivity.kt index 18b3439b..e1da2ff6 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/screenTransition/ScreenTransitionActivity.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/screenTransition/ScreenTransitionActivity.kt @@ -5,6 +5,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -16,7 +17,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior import cafe.adriel.voyager.transitions.ScreenTransition +import kotlin.random.Random class ScreenTransitionActivity : ComponentActivity() { @@ -31,40 +34,67 @@ class ScreenTransitionActivity : ComponentActivity() { @Composable fun Content() { Navigator( - screen = NoCustomAnimationSampleScreen(0) + screen = NoCustomAnimationSampleScreen(0), + disposeBehavior = NavigatorDisposeBehavior(disposeSteps = false) ) { navigator -> Box(modifier = Modifier.fillMaxSize()) { ScreenTransition( navigator = navigator, - defaultTransition = SlideTransition() + defaultTransition = SlideTransition(), + disposeScreenAfterTransitionEnd = true ) - Row( + Column( modifier = Modifier .align(Alignment.BottomCenter) .fillMaxWidth() - .padding(40.dp), - horizontalArrangement = Arrangement.spacedBy(20.dp) ) { - Button( - onClick = { navigator.push(FadeAnimationSampleScreen(navigator.items.size)) }, - modifier = Modifier.weight(1f) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + horizontalArrangement = Arrangement.spacedBy(20.dp) ) { - Text(text = "Fade") - } + Button( + onClick = { navigator.push(FadeAnimationSampleScreen(navigator.items.size)) }, + modifier = Modifier.weight(1f) + ) { + Text(text = "Fade") + } - Button( - onClick = { navigator.push(NoCustomAnimationSampleScreen(navigator.items.size)) }, - modifier = Modifier.weight(1f) - ) { - Text(text = "Default") - } + Button( + onClick = { navigator.push(NoCustomAnimationSampleScreen(navigator.items.size)) }, + modifier = Modifier.weight(1f) + ) { + Text(text = "Default") + } - Button( - onClick = { navigator.pop() }, - modifier = Modifier.weight(1f) + Button( + onClick = { navigator.pop() }, + modifier = Modifier.weight(1f) + ) { + Text(text = "Pop") + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + horizontalArrangement = Arrangement.spacedBy(20.dp) ) { - Text(text = "Pop") + Button( + onClick = { navigator.replace(NoCustomAnimationSampleScreen(Random.nextInt())) }, + modifier = Modifier.weight(1f) + ) { + Text(text = "Replace") + } + + Button( + onClick = { navigator.replaceAll(NoCustomAnimationSampleScreen(Random.nextInt())) }, + modifier = Modifier.weight(1f) + ) { + Text(text = "ReplaceAll") + } } } } diff --git a/voyager-transitions/api/android/voyager-transitions.api b/voyager-transitions/api/android/voyager-transitions.api index 0fd293e7..9c8fe82d 100644 --- a/voyager-transitions/api/android/voyager-transitions.api +++ b/voyager-transitions/api/android/voyager-transitions.api @@ -8,15 +8,19 @@ public final class cafe/adriel/voyager/transitions/ComposableSingletons$Crossfad public final class cafe/adriel/voyager/transitions/ComposableSingletons$FadeTransitionKt { public static final field INSTANCE Lcafe/adriel/voyager/transitions/ComposableSingletons$FadeTransitionKt; public static field lambda-1 Lkotlin/jvm/functions/Function4; + public static field lambda-2 Lkotlin/jvm/functions/Function4; public fun ()V public final fun getLambda-1$voyager_transitions_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-2$voyager_transitions_release ()Lkotlin/jvm/functions/Function4; } public final class cafe/adriel/voyager/transitions/ComposableSingletons$ScaleTransitionKt { public static final field INSTANCE Lcafe/adriel/voyager/transitions/ComposableSingletons$ScaleTransitionKt; public static field lambda-1 Lkotlin/jvm/functions/Function4; + public static field lambda-2 Lkotlin/jvm/functions/Function4; public fun ()V public final fun getLambda-1$voyager_transitions_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-2$voyager_transitions_release ()Lkotlin/jvm/functions/Function4; } public final class cafe/adriel/voyager/transitions/ComposableSingletons$ScreenTransitionKt { @@ -24,17 +28,23 @@ public final class cafe/adriel/voyager/transitions/ComposableSingletons$ScreenTr public static field lambda-1 Lkotlin/jvm/functions/Function4; public static field lambda-2 Lkotlin/jvm/functions/Function4; public static field lambda-3 Lkotlin/jvm/functions/Function4; + public static field lambda-4 Lkotlin/jvm/functions/Function4; + public static field lambda-5 Lkotlin/jvm/functions/Function4; public fun ()V public final fun getLambda-1$voyager_transitions_release ()Lkotlin/jvm/functions/Function4; public final fun getLambda-2$voyager_transitions_release ()Lkotlin/jvm/functions/Function4; public final fun getLambda-3$voyager_transitions_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-4$voyager_transitions_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-5$voyager_transitions_release ()Lkotlin/jvm/functions/Function4; } public final class cafe/adriel/voyager/transitions/ComposableSingletons$SlideTransitionKt { public static final field INSTANCE Lcafe/adriel/voyager/transitions/ComposableSingletons$SlideTransitionKt; public static field lambda-1 Lkotlin/jvm/functions/Function4; + public static field lambda-2 Lkotlin/jvm/functions/Function4; public fun ()V public final fun getLambda-1$voyager_transitions_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-2$voyager_transitions_release ()Lkotlin/jvm/functions/Function4; } public final class cafe/adriel/voyager/transitions/CrossfadeTransitionKt { diff --git a/voyager-transitions/api/desktop/voyager-transitions.api b/voyager-transitions/api/desktop/voyager-transitions.api index 1aacfd2e..f4ecf2b1 100644 --- a/voyager-transitions/api/desktop/voyager-transitions.api +++ b/voyager-transitions/api/desktop/voyager-transitions.api @@ -8,15 +8,19 @@ public final class cafe/adriel/voyager/transitions/ComposableSingletons$Crossfad public final class cafe/adriel/voyager/transitions/ComposableSingletons$FadeTransitionKt { public static final field INSTANCE Lcafe/adriel/voyager/transitions/ComposableSingletons$FadeTransitionKt; public static field lambda-1 Lkotlin/jvm/functions/Function4; + public static field lambda-2 Lkotlin/jvm/functions/Function4; public fun ()V public final fun getLambda-1$voyager_transitions ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-2$voyager_transitions ()Lkotlin/jvm/functions/Function4; } public final class cafe/adriel/voyager/transitions/ComposableSingletons$ScaleTransitionKt { public static final field INSTANCE Lcafe/adriel/voyager/transitions/ComposableSingletons$ScaleTransitionKt; public static field lambda-1 Lkotlin/jvm/functions/Function4; + public static field lambda-2 Lkotlin/jvm/functions/Function4; public fun ()V public final fun getLambda-1$voyager_transitions ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-2$voyager_transitions ()Lkotlin/jvm/functions/Function4; } public final class cafe/adriel/voyager/transitions/ComposableSingletons$ScreenTransitionKt { @@ -24,17 +28,23 @@ public final class cafe/adriel/voyager/transitions/ComposableSingletons$ScreenTr public static field lambda-1 Lkotlin/jvm/functions/Function4; public static field lambda-2 Lkotlin/jvm/functions/Function4; public static field lambda-3 Lkotlin/jvm/functions/Function4; + public static field lambda-4 Lkotlin/jvm/functions/Function4; + public static field lambda-5 Lkotlin/jvm/functions/Function4; public fun ()V public final fun getLambda-1$voyager_transitions ()Lkotlin/jvm/functions/Function4; public final fun getLambda-2$voyager_transitions ()Lkotlin/jvm/functions/Function4; public final fun getLambda-3$voyager_transitions ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-4$voyager_transitions ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-5$voyager_transitions ()Lkotlin/jvm/functions/Function4; } public final class cafe/adriel/voyager/transitions/ComposableSingletons$SlideTransitionKt { public static final field INSTANCE Lcafe/adriel/voyager/transitions/ComposableSingletons$SlideTransitionKt; public static field lambda-1 Lkotlin/jvm/functions/Function4; + public static field lambda-2 Lkotlin/jvm/functions/Function4; public fun ()V public final fun getLambda-1$voyager_transitions ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-2$voyager_transitions ()Lkotlin/jvm/functions/Function4; } public final class cafe/adriel/voyager/transitions/CrossfadeTransitionKt { diff --git a/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/FadeTransition.kt b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/FadeTransition.kt index 428880c3..2e61c0b7 100644 --- a/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/FadeTransition.kt +++ b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/FadeTransition.kt @@ -8,6 +8,7 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.navigator.Navigator @Composable @@ -16,10 +17,29 @@ public fun FadeTransition( modifier: Modifier = Modifier, animationSpec: FiniteAnimationSpec = spring(stiffness = Spring.StiffnessMediumLow), content: ScreenTransitionContent = { it.Content() } +) { + FadeTransition( + navigator = navigator, + modifier = modifier, + disposeScreenAfterTransitionEnd = false, + animationSpec = animationSpec, + content = content + ) +} + +@ExperimentalVoyagerApi +@Composable +public fun FadeTransition( + navigator: Navigator, + modifier: Modifier = Modifier, + animationSpec: FiniteAnimationSpec = spring(stiffness = Spring.StiffnessMediumLow), + disposeScreenAfterTransitionEnd: Boolean = false, + content: ScreenTransitionContent = { it.Content() } ) { ScreenTransition( navigator = navigator, modifier = modifier, + disposeScreenAfterTransitionEnd = disposeScreenAfterTransitionEnd, content = content, transition = { fadeIn(animationSpec = animationSpec) togetherWith fadeOut(animationSpec = animationSpec) } ) 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..81f8a9cb 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 @@ -8,6 +8,7 @@ import androidx.compose.animation.scaleOut import androidx.compose.animation.togetherWith import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.stack.StackEvent import cafe.adriel.voyager.navigator.Navigator @@ -20,10 +21,29 @@ public fun ScaleTransition( modifier: Modifier = Modifier, animationSpec: FiniteAnimationSpec = spring(stiffness = Spring.StiffnessMediumLow), content: ScreenTransitionContent = { it.Content() } +) { + ScaleTransition( + navigator = navigator, + modifier = modifier, + animationSpec = animationSpec, + disposeScreenAfterTransitionEnd = false, + content = content + ) +} + +@ExperimentalVoyagerApi +@Composable +public fun ScaleTransition( + navigator: Navigator, + modifier: Modifier = Modifier, + animationSpec: FiniteAnimationSpec = spring(stiffness = Spring.StiffnessMediumLow), + disposeScreenAfterTransitionEnd: Boolean = false, + content: ScreenTransitionContent = { it.Content() } ) { ScreenTransition( navigator = navigator, modifier = modifier, + disposeScreenAfterTransitionEnd = disposeScreenAfterTransitionEnd, content = content, transition = { val (initialScale, targetScale) = when (navigator.lastEvent) { 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 bdf169e9..62af9d53 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,8 +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.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 androidx.compose.ui.Modifier import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.screen.Screen @@ -36,17 +43,20 @@ public interface ScreenTransition { public typealias ScreenTransitionContent = @Composable AnimatedVisibilityScope.(Screen) -> Unit +@ExperimentalVoyagerApi @Composable public fun ScreenTransition( navigator: Navigator, enterTransition: AnimatedContentTransitionScope.() -> ContentTransform, exitTransition: AnimatedContentTransitionScope.() -> ContentTransform, modifier: Modifier = Modifier, + disposeScreenAfterTransitionEnd: Boolean = false, content: ScreenTransitionContent = { it.Content() } ) { ScreenTransition( navigator = navigator, modifier = modifier, + disposeScreenAfterTransitionEnd = disposeScreenAfterTransitionEnd, content = content, transition = { when (navigator.lastEvent) { @@ -57,12 +67,30 @@ public fun ScreenTransition( ) } +@Composable +public fun ScreenTransition( + navigator: Navigator, + enterTransition: AnimatedContentTransitionScope.() -> ContentTransform, + exitTransition: AnimatedContentTransitionScope.() -> ContentTransform, + modifier: Modifier = Modifier, + content: ScreenTransitionContent = { it.Content() } +) { + ScreenTransition( + navigator = navigator, + enterTransition = enterTransition, + exitTransition = exitTransition, + modifier = modifier, + content = content + ) +} + @ExperimentalVoyagerApi @Composable public fun ScreenTransition( navigator: Navigator, defaultTransition: ScreenTransition, modifier: Modifier = Modifier, + disposeScreenAfterTransitionEnd: Boolean = false, content: ScreenTransitionContent = { it.Content() } ) { ScreenTransition( @@ -73,17 +101,52 @@ public fun ScreenTransition( enter togetherWith exit }, modifier = modifier, + disposeScreenAfterTransitionEnd = disposeScreenAfterTransitionEnd, + content = content + ) +} + +@Composable +public fun ScreenTransition( + navigator: Navigator, + transition: AnimatedContentTransitionScope.() -> ContentTransform, + modifier: Modifier = Modifier, + content: ScreenTransitionContent = { it.Content() } +) { + ScreenTransition( + navigator = navigator, + transition = transition, + modifier = modifier, + disposeScreenAfterTransitionEnd = false, content = content ) } +@ExperimentalVoyagerApi +@OptIn(ExperimentalAnimationApi::class) @Composable public fun ScreenTransition( navigator: Navigator, transition: AnimatedContentTransitionScope.() -> ContentTransform, modifier: Modifier = Modifier, + disposeScreenAfterTransitionEnd: Boolean = false, content: ScreenTransitionContent = { it.Content() } ) { + val screenCandidatesToDispose = rememberSaveable(saver = screenCandidatesToDisposeSaver()) { + mutableStateOf(emptySet()) + } + + val currentScreens = navigator.items + + if (disposeScreenAfterTransitionEnd) { + DisposableEffect(currentScreens) { + onDispose { + val newScreenKeys = navigator.items.map { it.key } + screenCandidatesToDispose.value += currentScreens.filter { it.key !in newScreenKeys } + } + } + } + AnimatedContent( targetState = navigator.lastItem, transitionSpec = { @@ -104,8 +167,27 @@ public fun ScreenTransition( }, modifier = modifier ) { screen -> + if (this.transition.targetState == this.transition.currentState && disposeScreenAfterTransitionEnd) { + LaunchedEffect(Unit) { + val newScreens = navigator.items.map { it.key } + val screensToDispose = screenCandidatesToDispose.value.filterNot { it.key in newScreens } + if (screensToDispose.isNotEmpty()) { + screensToDispose.forEach { navigator.dispose(it) } + navigator.clearEvent() + } + screenCandidatesToDispose.value = emptySet() + } + } + navigator.saveableState("transition", screen) { content(screen) } } } + +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/SlideTransition.kt b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/SlideTransition.kt index efb5311b..13e1d7c8 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.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.stack.StackEvent import cafe.adriel.voyager.navigator.Navigator @@ -25,10 +26,34 @@ public fun SlideTransition( visibilityThreshold = IntOffset.VisibilityThreshold ), content: ScreenTransitionContent = { it.Content() } +) { + SlideTransition( + navigator = navigator, + modifier = modifier, + orientation = orientation, + animationSpec = animationSpec, + disposeScreenAfterTransitionEnd = false, + content = content + ) +} + +@ExperimentalVoyagerApi +@Composable +public fun SlideTransition( + navigator: Navigator, + modifier: Modifier = Modifier, + orientation: SlideOrientation = SlideOrientation.Horizontal, + animationSpec: FiniteAnimationSpec = spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ), + disposeScreenAfterTransitionEnd: Boolean = false, + content: ScreenTransitionContent = { it.Content() } ) { ScreenTransition( navigator = navigator, modifier = modifier, + disposeScreenAfterTransitionEnd = disposeScreenAfterTransitionEnd, content = content, transition = { val (initialOffset, targetOffset) = when (navigator.lastEvent) {