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..722b2132 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 @@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.lifecycle.LifecycleEffect +import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.navigator.LocalNavigator @@ -36,6 +37,9 @@ data class BasicNavigationScreen( onStarted = { Log.d("Navigator", "Start screen #$index") }, onDisposed = { Log.d("Navigator", "Dispose screen #$index") } ) + LifecycleEffectOnce { + Log.d("Navigator", "On screen first appear #$index") + } val navigator = LocalNavigator.currentOrThrow diff --git a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/lifecycle/LifecycleEffectStore.kt b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/lifecycle/LifecycleEffectStore.kt new file mode 100644 index 00000000..def1a68e --- /dev/null +++ b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/lifecycle/LifecycleEffectStore.kt @@ -0,0 +1,22 @@ +package cafe.adriel.voyager.core.lifecycle + +import cafe.adriel.voyager.core.concurrent.ThreadSafeMap +import cafe.adriel.voyager.core.concurrent.ThreadSafeSet +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey + +internal object LifecycleEffectStore : ScreenDisposable { + private val executedLifecycles = ThreadSafeMap>() + + fun store(screen: Screen, effectKey: String) { + val set = executedLifecycles.getOrPut(screen.key) { ThreadSafeSet() } + set.add(effectKey) + } + + fun hasExecuted(screen: Screen, effectKey: String): Boolean = + executedLifecycles.get(screen.key)?.contains(effectKey) == true + + override fun onDispose(screen: Screen) { + executedLifecycles.remove(screen.key) + } +} diff --git a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/lifecycle/ScreenLifecycle.kt b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/lifecycle/ScreenLifecycle.kt index 4dd7cb8b..6c29ab10 100644 --- a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/lifecycle/ScreenLifecycle.kt +++ b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/lifecycle/ScreenLifecycle.kt @@ -2,9 +2,16 @@ package cafe.adriel.voyager.core.lifecycle import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.randomUuid +@Deprecated( + message = "This API is a wrap on top on DisposableEffect, will be removed in 1.1.0, replace with DisposableEffect" +) @Composable public fun Screen.LifecycleEffect( onStarted: () -> Unit = {}, @@ -16,6 +23,23 @@ public fun Screen.LifecycleEffect( } } +@ExperimentalVoyagerApi +@Composable +public fun Screen.LifecycleEffectOnce(onFirstAppear: () -> Unit) { + val uniqueCompositionKey = rememberSaveable { randomUuid() } + + val lifecycleEffectStore = remember { + ScreenLifecycleStore.get(this) { LifecycleEffectStore } + } + + LaunchedEffect(Unit) { + if (lifecycleEffectStore.hasExecuted(this@LifecycleEffectOnce, uniqueCompositionKey).not()) { + lifecycleEffectStore.store(this@LifecycleEffectOnce, uniqueCompositionKey) + onFirstAppear() + } + } +} + @Composable public fun rememberScreenLifecycleOwner( screen: Screen