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 5878254
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 7 deletions.
3 changes: 2 additions & 1 deletion samples/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
</activity>
<activity android:name=".stateStack.StateStackActivity"/>
<activity android:name=".basicNavigation.BasicNavigationActivity"/>
<activity android:name=".tabNavigation.TabNavigationActivity"/>
<activity android:name=".disposeSample.DisposeWhenTransitionFinishedSampleActivity"/>
<activity android:name=".disposeSample.DisposeWhenStackChangedSampleActivity"/>
<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 @@ -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
Expand Down Expand Up @@ -52,6 +54,8 @@ class SampleActivity : ComponentActivity() {
contentPadding = PaddingValues(24.dp)
) {
item {
StartSampleButton<DisposeWhenTransitionFinishedSampleActivity>("DisposeWhenTransitionFinished")
StartSampleButton<DisposeWhenStackChangedSampleActivity>("DisposeWhenStackChanged")
StartSampleButton<StateStackActivity>("SnapshotStateStack")
StartSampleButton<BasicNavigationActivity>("Basic Navigation")
StartSampleButton<ParcelableActivity>("Basic Navigation with Parcelable")
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package cafe.adriel.voyager.sample.disposeSample

import cafe.adriel.voyager.navigator.DisposeStepsBehavior

class DisposeWhenStackChangedSampleActivity : DisposeSampleActivity(DisposeStepsBehavior.DisposeWhenStackChanged)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package cafe.adriel.voyager.sample.disposeSample

import cafe.adriel.voyager.navigator.DisposeStepsBehavior

class DisposeWhenTransitionFinishedSampleActivity : DisposeSampleActivity(
DisposeStepsBehavior.DisposeWhenTransitionFinished
)
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
}
1 change: 1 addition & 0 deletions voyager-navigator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ kotlin {
sourceSets {
commonMain.dependencies {
api(projects.voyagerCore)
implementation(libs.coroutines.core)
compileOnly(compose.runtime)
compileOnly(compose.runtimeSaveable)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Channel<Unit>> =
staticCompositionLocalOf { error("No LocalTransitionFinishedEvents provided") }

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

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

val transitionFinishedEvents = remember { Channel<Unit>() }

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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<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 +53,38 @@ internal fun StepDisposableEffect(
}
}

@Composable
internal fun StepDisposableAfterTransitionEffect(
transitionFinishedEvents: Channel<Unit>,
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
Expand Down Expand Up @@ -80,3 +125,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 @@ -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
Expand Down Expand Up @@ -55,6 +58,7 @@ public fun ScreenTransition(
)
}

@OptIn(ExperimentalAnimationApi::class)
@Composable
public fun ScreenTransition(
navigator: Navigator,
Expand Down Expand Up @@ -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)
}
Expand Down

0 comments on commit 5878254

Please sign in to comment.