Skip to content

Commit

Permalink
dispose steps after transition
Browse files Browse the repository at this point in the history
  • Loading branch information
shpasha committed May 21, 2024
1 parent 6f8fdf0 commit 935682e
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 6 deletions.
2 changes: 1 addition & 1 deletion samples/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</activity>
<activity android:name=".stateStack.StateStackActivity"/>
<activity android:name=".basicNavigation.BasicNavigationActivity"/>
<activity android:name=".tabNavigation.TabNavigationActivity"/>
<activity android:name=".disposeAfterTransition.DisposeScreensSampleActivity"/>
<activity android:name=".nestedNavigation.NestedNavigationActivity"/>
<activity android:name=".parcelableScreen.ParcelableActivity"/>
<activity android:name=".screenModel.ScreenModelActivity"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import cafe.adriel.voyager.sample.parcelableScreen.ParcelableActivity
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.disposeAfterTransition.DisposeScreensSampleActivity
import cafe.adriel.voyager.sample.tabNavigation.TabNavigationActivity

class SampleActivity : ComponentActivity() {
Expand All @@ -52,6 +53,7 @@ class SampleActivity : ComponentActivity() {
contentPadding = PaddingValues(24.dp)
) {
item {
StartSampleButton<DisposeScreensSampleActivity>("DisposeScreens")
StartSampleButton<StateStackActivity>("SnapshotStateStack")
StartSampleButton<BasicNavigationActivity>("Basic Navigation")
StartSampleButton<ParcelableActivity>("Basic Navigation with Parcelable")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cafe.adriel.voyager.sample.disposeAfterTransition

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

class DisposeScreensSampleActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
Content()
}
}

@Composable
fun Content() {
Navigator(
screen = SampleScreen(0),
disposeBehavior = NavigatorDisposeBehavior(
disposeStepsBehavior = DisposeStepsBehavior.DisposeWhenTransitionFinished
)
) {
SlideTransition(
navigator = it,
animationSpec = tween(1000)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package cafe.adriel.voyager.sample.disposeAfterTransition

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")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package cafe.adriel.voyager.navigator

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.currentCompositeKeyHash
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.SaveableStateHolder
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
Expand All @@ -23,6 +25,7 @@ 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
Expand All @@ -32,6 +35,11 @@ public typealias NavigatorContent = @Composable (navigator: Navigator) -> Unit

public typealias OnBackPressed = ((currentScreen: Screen) -> Boolean)?

@InternalVoyagerApi
public val LocalIsTransitionRunning: ProvidableCompositionLocal<MutableState<Boolean>> = staticCompositionLocalOf {
error("No LocalIsTransitionRunning provided")
}

public val LocalNavigator: ProvidableCompositionLocal<Navigator?> =
staticCompositionLocalOf { null }

Expand Down Expand Up @@ -86,11 +94,20 @@ public fun Navigator(
NavigatorDisposableEffect(navigator)
}

val isTransitionRunning = remember { mutableStateOf(false) }

CompositionLocalProvider(
LocalNavigator provides navigator
LocalNavigator provides navigator,
LocalIsTransitionRunning provides isTransitionRunning
) {
if (disposeBehavior.disposeSteps) {
StepDisposableEffect(navigator)

when (disposeBehavior.disposeStepsBehavior) {
DisposeStepsBehavior.DisposeWhenTransitionFinished -> StepDisposableAfterTransitionEffect(
isTransitionRunning = isTransitionRunning,
navigator = navigator,
)
DisposeStepsBehavior.DisposeWhenStackChanged -> StepDisposableEffect(navigator)
DisposeStepsBehavior.None -> Unit
}

NavigatorBackHandler(navigator, onBackPressed)
Expand Down Expand Up @@ -180,10 +197,24 @@ 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 = if (disposeSteps) DisposeStepsBehavior.DisposeWhenStackChanged else DisposeStepsBehavior.None,
)
}

@InternalVoyagerApi
@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,26 @@ 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

private val disposableEvents: Set<StackEvent> =
setOf(StackEvent.Pop, StackEvent.Replace)

private data class ScreenData(
val key: ScreenKey,
val screen: Screen,
)

@Composable
internal fun NavigatorDisposableEffect(
navigator: Navigator
Expand Down Expand Up @@ -40,6 +52,42 @@ internal fun StepDisposableEffect(
}
}

@Composable
internal fun StepDisposableAfterTransitionEffect(
isTransitionRunning: MutableState<Boolean>,
navigator: Navigator,
) {

val currentScreens = navigator.items
val screenCandidatesToDispose = rememberSaveable(saver = screenCandidatesToDisposeSaver()) {
mutableStateOf(emptySet())
}

fun dispose() {
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()
}

DisposableEffect(currentScreens) {
onDispose {
val newScreenKeys = navigator.items.map { it.key }
screenCandidatesToDispose.value += currentScreens.filter { it.key !in newScreenKeys }
.map { ScreenData(it.key, it) }
}
}

LaunchedEffect(isTransitionRunning.value) {
if (!isTransitionRunning.value) {
dispose()
}
}
}

@Composable
internal fun ChildrenNavigationDisposableEffect(
navigator: Navigator
Expand Down Expand Up @@ -80,3 +128,10 @@ internal fun disposeNavigator(navigator: Navigator) {
NavigatorLifecycleStore.remove(navigator)
navigator.clearEvent()
}

private fun screenCandidatesToDisposeSaver(): Saver<MutableState<Set<ScreenData>>, List<ScreenData>> {
return Saver(
save = { it.value.toList() },
restore = { mutableStateOf(it.toSet()) },
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.togetherWith
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.Composable
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.LocalIsTransitionRunning
import cafe.adriel.voyager.navigator.Navigator

@ExperimentalVoyagerApi
Expand Down Expand Up @@ -55,6 +57,7 @@ public fun ScreenTransition(
)
}

@OptIn(ExperimentalAnimationApi::class)
@Composable
public fun ScreenTransition(
navigator: Navigator,
Expand Down Expand Up @@ -90,6 +93,8 @@ public fun ScreenTransition(
},
modifier = modifier
) { screen ->
val isTransitionRunning = LocalIsTransitionRunning.current
isTransitionRunning.value = this.transition.isRunning
navigator.saveableState("transition", screen) {
content(screen)
}
Expand Down

0 comments on commit 935682e

Please sign in to comment.