From 2fd7081fccd15bc22de822333cf4a3b579516eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adriel=20Caf=C3=A9?= Date: Fri, 27 Aug 2021 18:57:49 -0300 Subject: [PATCH] fix: viewmodel lifecycle --- README.md | 14 ++-- gradle.properties | 2 +- .../java/cafe/adriel/voyager/sample/App.kt | 4 + .../sample/androidNavigation/ListScreen.kt | 8 +- .../sample/androidNavigation/ListViewModel.kt | 15 ++++ .../basicNavigation/BasicNavigationScreen.kt | 2 +- .../sample/tabNavigation/tabs/TabContent.kt | 2 +- .../adriel/voyager/androidx/AndroidScreen.kt | 9 ++- .../voyager/androidx/ScreenLifecycleHolder.kt | 77 +++++++++++++++++++ .../voyager/androidx/ScreenLifecycleOwner.kt | 44 ----------- .../cafe/adriel/voyager/core/hook/Hookable.kt | 20 ----- .../adriel/voyager/core/hook/ScreenHook.kt | 51 ------------ .../voyager/core/lifecycle/ScreenHooks.kt | 13 ++++ .../voyager/core/lifecycle/ScreenLifecycle.kt | 41 ++++++++++ .../cafe/adriel/voyager/core/screen/Screen.kt | 16 +--- .../adriel/voyager/core/screen/ScreenKey.kt | 4 +- .../voyager/core/stack/SnapshotStateStack.kt | 4 + .../cafe/adriel/voyager/core/stack/Stack.kt | 2 + .../adriel/voyager/navigator/Navigator.kt | 13 ++-- .../voyager/navigator/tab/TabNavigator.kt | 11 ++- 20 files changed, 196 insertions(+), 156 deletions(-) create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListViewModel.kt create mode 100644 voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/ScreenLifecycleHolder.kt delete mode 100644 voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/ScreenLifecycleOwner.kt delete mode 100644 voyager-core/src/main/java/cafe/adriel/voyager/core/hook/Hookable.kt delete mode 100644 voyager-core/src/main/java/cafe/adriel/voyager/core/hook/ScreenHook.kt create mode 100644 voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenHooks.kt create mode 100644 voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycle.kt diff --git a/README.md b/README.md index 0b609369..5f0237c9 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ Voyager: Compose on Warp Speed -A lightweight and pragmatic navigation library built for, and seamlessly integrated with, [Jetpack Compose](https://developer.android.com/jetpack/compose). +Voyager is a lightweight and complete navigation library built for, and seamlessly integrated with, [Jetpack Compose](https://developer.android.com/jetpack/compose). -Turn on the Warp Drive and enjoy the trek 🖖 +Create scalable Single-Activity apps powered by a [pragmatic API](https://voyager.adriel.cafe/navigation/fundamentals). ```kotlin class HomeScreen : Screen { @@ -35,11 +35,12 @@ class SingleActivity : ComponentActivity() { } ``` +Turn on the Warp Drive and enjoy the trek 🖖 + ### Documentation See the [project website](https://voyager.adriel.cafe) for documentation and APIs. ### Features -- Create scalable Single-Activity apps powered by a [pragmatic API](https://voyager.adriel.cafe/navigation/fundamentals) - [BottomSheet navigation](https://voyager.adriel.cafe/navigation/bottomsheet-navigation) - [Tab navigation](https://voyager.adriel.cafe/navigation/tab-navigation) like [Youtube app](https://play.google.com/store/apps/details?id=com.google.android.youtube) - [Nested navigation](https://voyager.adriel.cafe/navigation/nested-navigation) if you need to manage multiple stacks @@ -47,7 +48,6 @@ See the [project website](https://voyager.adriel.cafe) for documentation and API - Type-safe [multi-module navigation](https://voyager.adriel.cafe/navigation/multi-module-navigation) - State-aware [Stack API](https://voyager.adriel.cafe/stack-api) - Built-in [transitions](https://voyager.adriel.cafe/transitions) -- Pluggable [hooks](https://voyager.adriel.cafe/hooks) - [Lifecycle](https://voyager.adriel.cafe/lifecycle) callbacks - [Back press](https://voyager.adriel.cafe/back-press) handling - [Deep linking](https://voyager.adriel.cafe/deep-links) support @@ -60,6 +60,6 @@ See the [project website](https://voyager.adriel.cafe) for documentation and API |------------|----------|-------------| | ![navigation-stack](https://user-images.githubusercontent.com/2512298/126323192-9b6349fe-7b96-4acf-b62e-c75165d909e1.gif) | ![navigation-androidx](https://user-images.githubusercontent.com/2512298/130377801-c350b4f5-bcca-4d28-9403-0d9d4c1e99f7.gif) | ![navigation-basic](https://user-images.githubusercontent.com/2512298/126323165-47760eec-2ba2-48ee-8e3a-841d50098d33.gif) | -| [Tab nav.](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/tabNavigation) | [Multi-module nav.](https://github.com/adrielcafe/voyager/tree/main/sample-multi-module) | [Nested nav.](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/nestedNavigation) | -|------------|----------|-------------| -| ![navigation-tab](https://user-images.githubusercontent.com/2512298/126323588-2f970953-0adb-47f8-b2fb-91c5854656bd.gif) | ![navigation-multi-module](https://user-images.githubusercontent.com/2512298/130662717-c15caf88-350e-42a0-837c-3453805b68f2.gif) | ![navigation-nested](https://user-images.githubusercontent.com/2512298/126323027-a2633aef-9402-4df8-9384-45935d7986cf.gif) | +| [BottomSheet nav.](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/bottomSheetNavigation) | [Tab nav.](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/tabNavigation) | [Multi-module nav.](https://github.com/adrielcafe/voyager/tree/main/sample-multi-module) | [Nested nav.](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/nestedNavigation) | +|------------|------------|----------|-------------| +| ![navigation-bottom-sheet](https://user-images.githubusercontent.com/2512298/131191122-18025192-ce4d-4659-9afa-aacfdb488796.gif) | ![navigation-tab](https://user-images.githubusercontent.com/2512298/126323588-2f970953-0adb-47f8-b2fb-91c5854656bd.gif) | ![navigation-multi-module](https://user-images.githubusercontent.com/2512298/130662717-c15caf88-350e-42a0-837c-3453805b68f2.gif) | ![navigation-nested](https://user-images.githubusercontent.com/2512298/126323027-a2633aef-9402-4df8-9384-45935d7986cf.gif) | diff --git a/gradle.properties b/gradle.properties index cee740c3..574f7a16 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ kotlin.code.style=official # Maven GROUP=cafe.adriel.voyager -VERSION_NAME=1.0.0-beta08 +VERSION_NAME=1.0.0-beta09 POM_DESCRIPTION=A pragmatic navigation library for Jetpack Compose POM_INCEPTION_YEAR=2021 diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/App.kt b/sample/src/main/java/cafe/adriel/voyager/sample/App.kt index a40aadbe..2c936c13 100644 --- a/sample/src/main/java/cafe/adriel/voyager/sample/App.kt +++ b/sample/src/main/java/cafe/adriel/voyager/sample/App.kt @@ -2,6 +2,7 @@ package cafe.adriel.voyager.sample import android.app.Application import cafe.adriel.voyager.sample.androidNavigation.DetailsViewModel +import cafe.adriel.voyager.sample.androidNavigation.ListViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.context.startKoin import org.koin.dsl.module @@ -13,6 +14,9 @@ class App : Application() { startKoin { modules( module { + viewModel { + ListViewModel(handle = get()) + } viewModel { parameters -> DetailsViewModel(index = parameters.get()) } diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListScreen.kt b/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListScreen.kt index 5a718c48..7487f8f3 100644 --- a/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListScreen.kt +++ b/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListScreen.kt @@ -2,6 +2,8 @@ package cafe.adriel.voyager.sample.androidNavigation import androidx.compose.foundation.clickable import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ListItem import androidx.compose.material.Text @@ -10,6 +12,7 @@ import androidx.compose.ui.Modifier import cafe.adriel.voyager.androidx.AndroidScreen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow +import org.koin.androidx.compose.getStateViewModel class ListScreen : AndroidScreen() { @@ -17,11 +20,12 @@ class ListScreen : AndroidScreen() { @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow + val viewModel = getStateViewModel() LazyColumn { - items(100) { index -> + itemsIndexed(viewModel.items) { index, item -> ListItem( - text = { Text(text = "Item $index") }, + text = { Text(text = item) }, modifier = Modifier.clickable { navigator.push(DetailsScreen(index)) } ) } diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListViewModel.kt b/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListViewModel.kt new file mode 100644 index 00000000..8891682e --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListViewModel.kt @@ -0,0 +1,15 @@ +package cafe.adriel.voyager.sample.androidNavigation + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import java.util.UUID + +class ListViewModel(private val handle: SavedStateHandle) : ViewModel() { + + init { + handle["items"] = (0..100).map { "Item #$it | ${UUID.randomUUID().toString().substringBefore('-')}" } + } + + val items: List + get() = handle["items"] ?: error("Items not found") +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/basicNavigation/BasicNavigationScreen.kt b/sample/src/main/java/cafe/adriel/voyager/sample/basicNavigation/BasicNavigationScreen.kt index 075c3469..93908f50 100644 --- a/sample/src/main/java/cafe/adriel/voyager/sample/basicNavigation/BasicNavigationScreen.kt +++ b/sample/src/main/java/cafe/adriel/voyager/sample/basicNavigation/BasicNavigationScreen.kt @@ -17,7 +17,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.LifecycleEffect +import cafe.adriel.voyager.core.lifecycle.LifecycleEffect import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.navigator.LocalNavigator diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/tabNavigation/tabs/TabContent.kt b/sample/src/main/java/cafe/adriel/voyager/sample/tabNavigation/tabs/TabContent.kt index 3ce573b2..654f7dde 100644 --- a/sample/src/main/java/cafe/adriel/voyager/sample/tabNavigation/tabs/TabContent.kt +++ b/sample/src/main/java/cafe/adriel/voyager/sample/tabNavigation/tabs/TabContent.kt @@ -12,7 +12,7 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.LifecycleEffect +import cafe.adriel.voyager.core.lifecycle.LifecycleEffect import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.Tab diff --git a/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/AndroidScreen.kt b/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/AndroidScreen.kt index 4f2106b6..1979a9ff 100644 --- a/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/AndroidScreen.kt +++ b/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/AndroidScreen.kt @@ -1,9 +1,14 @@ package cafe.adriel.voyager.androidx +import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleOwner +import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleProvider import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey -public abstract class AndroidScreen : Screen, ScreenLifecycleOwner by ScreenLifecycleHolder() { +public abstract class AndroidScreen : Screen, ScreenLifecycleProvider { - override val key: String = uniqueScreenKey + override val key: ScreenKey = uniqueScreenKey + + override fun getLifecycleOwner(): ScreenLifecycleOwner = ScreenLifecycleHolder.get(key) } diff --git a/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/ScreenLifecycleHolder.kt b/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/ScreenLifecycleHolder.kt new file mode 100644 index 00000000..f1c88f14 --- /dev/null +++ b/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/ScreenLifecycleHolder.kt @@ -0,0 +1,77 @@ +package cafe.adriel.voyager.androidx + +import android.app.Activity +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSavedStateRegistryOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner +import cafe.adriel.voyager.core.lifecycle.ScreenHooks +import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleOwner +import cafe.adriel.voyager.core.screen.ScreenKey +import java.util.concurrent.ConcurrentHashMap + +public class ScreenLifecycleHolder private constructor( + private val key: ScreenKey +) : ScreenLifecycleOwner, + LifecycleOwner, + ViewModelStoreOwner, + SavedStateRegistryOwner { + + private val registry = LifecycleRegistry(this) + + private val store = ViewModelStore() + + private val controller = SavedStateRegistryController.create(this) + + private val Context.canDispose: Boolean + get() = (this as? Activity)?.isChangingConfigurations?.not() ?: true + + init { + controller.performRestore(null) + } + + @Composable + override fun getHooks(): ScreenHooks { + val context = LocalContext.current + + return ScreenHooks( + providers = listOf( + LocalViewModelStoreOwner provides this, + LocalSavedStateRegistryOwner provides this, + ), + disposer = { + if (context.canDispose) { + viewModelStore.clear() + remove(key) + } + } + ) + } + + override fun getLifecycle(): Lifecycle = registry + + override fun getViewModelStore(): ViewModelStore = store + + override fun getSavedStateRegistry(): SavedStateRegistry = controller.savedStateRegistry + + internal companion object { + + private val holders = ConcurrentHashMap() + + internal fun get(key: ScreenKey) = + holders.getOrPut(key) { ScreenLifecycleHolder(key) } + + private fun remove(key: ScreenKey) { + holders -= key + } + } +} diff --git a/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/ScreenLifecycleOwner.kt b/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/ScreenLifecycleOwner.kt deleted file mode 100644 index 9d2c9115..00000000 --- a/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/ScreenLifecycleOwner.kt +++ /dev/null @@ -1,44 +0,0 @@ -package cafe.adriel.voyager.androidx - -import androidx.compose.ui.platform.LocalSavedStateRegistryOwner -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LifecycleRegistry -import androidx.lifecycle.ViewModelStore -import androidx.lifecycle.ViewModelStoreOwner -import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner -import androidx.savedstate.SavedStateRegistry -import androidx.savedstate.SavedStateRegistryController -import androidx.savedstate.SavedStateRegistryOwner -import cafe.adriel.voyager.core.hook.HookableScreen -import cafe.adriel.voyager.core.hook.ScreenHook -import cafe.adriel.voyager.core.hook.ScreenHookHandler - -public val ScreenLifecycleOwner.screenLifecycleHooks: List - get() = listOf( - ScreenHook.OnProvide { LocalViewModelStoreOwner provides this }, - ScreenHook.OnProvide { LocalSavedStateRegistryOwner provides this }, - ScreenHook.OnDispose { viewModelStore.clear() } - ) - -public interface ScreenLifecycleOwner : HookableScreen, LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner - -public class ScreenLifecycleHolder : ScreenLifecycleOwner, HookableScreen by ScreenHookHandler() { - - private val store = ViewModelStore() - - private val registry = LifecycleRegistry(this) - - private val controller = SavedStateRegistryController.create(this) - - init { - controller.performRestore(null) - addHooks(screenLifecycleHooks) - } - - override fun getViewModelStore(): ViewModelStore = store - - override fun getLifecycle(): Lifecycle = registry - - override fun getSavedStateRegistry(): SavedStateRegistry = controller.savedStateRegistry -} diff --git a/voyager-core/src/main/java/cafe/adriel/voyager/core/hook/Hookable.kt b/voyager-core/src/main/java/cafe/adriel/voyager/core/hook/Hookable.kt deleted file mode 100644 index e648d25a..00000000 --- a/voyager-core/src/main/java/cafe/adriel/voyager/core/hook/Hookable.kt +++ /dev/null @@ -1,20 +0,0 @@ -package cafe.adriel.voyager.core.hook - -public inline fun Hookable.addHooks(vararg hooks: T) { - addHooks(hooks.toList()) -} - -public inline fun Hookable.removeHooks(vararg hooks: T) { - removeHooks(hooks.toList()) -} - -public interface Hookable { - - public val hooks: List - - public fun addHooks(hooks: List) - - public fun removeHooks(hooks: List) - - public fun clearHooks() -} diff --git a/voyager-core/src/main/java/cafe/adriel/voyager/core/hook/ScreenHook.kt b/voyager-core/src/main/java/cafe/adriel/voyager/core/hook/ScreenHook.kt deleted file mode 100644 index 761c5fbd..00000000 --- a/voyager-core/src/main/java/cafe/adriel/voyager/core/hook/ScreenHook.kt +++ /dev/null @@ -1,51 +0,0 @@ -package cafe.adriel.voyager.core.hook - -import androidx.compose.runtime.ProvidedValue -import cafe.adriel.voyager.core.screen.Screen - -public typealias HookableScreen = Hookable - -public val Screen.hooks: ScreenHooks - get() = when (this) { - is Hookable<*> -> ScreenHooks( - providers = hooks.filterIsInstance>(), - disposers = hooks.filterIsInstance(), - ) - else -> ScreenHooks() - } - -public fun Screen.clearHooks() { - if (this is Hookable<*>) { - clearHooks() - } -} - -public sealed class ScreenHook { - public data class OnProvide(val provide: () -> ProvidedValue) : ScreenHook() - public data class OnDispose(val dispose: () -> Unit) : ScreenHook() -} - -public data class ScreenHooks( - val providers: List> = emptyList(), - val disposers: List = emptyList() -) - -public class ScreenHookHandler : HookableScreen { - - private val mutableHooks = mutableListOf() - - override val hooks: List - get() = mutableHooks - - override fun addHooks(hooks: List) { - mutableHooks += hooks - } - - override fun removeHooks(hooks: List) { - mutableHooks -= hooks - } - - override fun clearHooks() { - mutableHooks.clear() - } -} diff --git a/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenHooks.kt b/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenHooks.kt new file mode 100644 index 00000000..c45a73c9 --- /dev/null +++ b/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenHooks.kt @@ -0,0 +1,13 @@ +package cafe.adriel.voyager.core.lifecycle + +import androidx.compose.runtime.ProvidedValue + +public data class ScreenHooks( + val providers: List> = emptyList(), + val disposer: () -> Unit = {} +) { + + internal companion object { + val Empty = ScreenHooks() + } +} diff --git a/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycle.kt b/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycle.kt new file mode 100644 index 00000000..b4311d7c --- /dev/null +++ b/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycle.kt @@ -0,0 +1,41 @@ +package cafe.adriel.voyager.core.lifecycle + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import cafe.adriel.voyager.core.screen.Screen + +@Composable +public fun Screen.LifecycleEffect( + onStarted: () -> Unit = {}, + onDisposed: () -> Unit = {} +) { + DisposableEffect(key) { + onStarted() + onDispose(onDisposed) + } +} + +@Composable +public fun rememberScreenLifecycleOwner( + screen: Screen +): ScreenLifecycleOwner = + remember(screen.key) { + when (screen) { + is ScreenLifecycleProvider -> screen.getLifecycleOwner() + else -> DefaultScreenLifecycleOwner + } + } + +public interface ScreenLifecycleProvider { + + public fun getLifecycleOwner(): ScreenLifecycleOwner +} + +public interface ScreenLifecycleOwner { + + @Composable + public fun getHooks(): ScreenHooks = ScreenHooks.Empty +} + +private object DefaultScreenLifecycleOwner : ScreenLifecycleOwner diff --git a/voyager-core/src/main/java/cafe/adriel/voyager/core/screen/Screen.kt b/voyager-core/src/main/java/cafe/adriel/voyager/core/screen/Screen.kt index 4a57e70b..8fe4dd0a 100644 --- a/voyager-core/src/main/java/cafe/adriel/voyager/core/screen/Screen.kt +++ b/voyager-core/src/main/java/cafe/adriel/voyager/core/screen/Screen.kt @@ -1,24 +1,12 @@ package cafe.adriel.voyager.core.screen import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import java.io.Serializable -@Composable -public fun Screen.LifecycleEffect( - onStarted: () -> Unit = {}, - onDisposed: () -> Unit = {} -) { - DisposableEffect(key) { - onStarted() - onDispose(onDisposed) - } -} - public interface Screen : Serializable { - public val key: String - get() = this::class.qualifiedName ?: error("Default key not found, please provide your own key") + public val key: ScreenKey + get() = this::class.qualifiedName ?: error("Default ScreenKey not found, please provide your own key") @Composable public fun Content() diff --git a/voyager-core/src/main/java/cafe/adriel/voyager/core/screen/ScreenKey.kt b/voyager-core/src/main/java/cafe/adriel/voyager/core/screen/ScreenKey.kt index cf7869bf..e1d5e9ac 100644 --- a/voyager-core/src/main/java/cafe/adriel/voyager/core/screen/ScreenKey.kt +++ b/voyager-core/src/main/java/cafe/adriel/voyager/core/screen/ScreenKey.kt @@ -2,7 +2,9 @@ package cafe.adriel.voyager.core.screen import java.util.concurrent.atomic.AtomicInteger +public typealias ScreenKey = String + private val nextScreenKey = AtomicInteger(0) -public val Screen.uniqueScreenKey: String +public val Screen.uniqueScreenKey: ScreenKey get() = "Screen#${nextScreenKey.getAndIncrement()}" diff --git a/voyager-core/src/main/java/cafe/adriel/voyager/core/stack/SnapshotStateStack.kt b/voyager-core/src/main/java/cafe/adriel/voyager/core/stack/SnapshotStateStack.kt index 837a424e..7956419c 100644 --- a/voyager-core/src/main/java/cafe/adriel/voyager/core/stack/SnapshotStateStack.kt +++ b/voyager-core/src/main/java/cafe/adriel/voyager/core/stack/SnapshotStateStack.kt @@ -156,4 +156,8 @@ public class SnapshotStateStack( public override operator fun plusAssign(items: List) { push(items) } + + override fun clearEvent() { + lastEvent = StackEvent.Idle + } } diff --git a/voyager-core/src/main/java/cafe/adriel/voyager/core/stack/Stack.kt b/voyager-core/src/main/java/cafe/adriel/voyager/core/stack/Stack.kt index 295c1a0b..58b249a3 100644 --- a/voyager-core/src/main/java/cafe/adriel/voyager/core/stack/Stack.kt +++ b/voyager-core/src/main/java/cafe/adriel/voyager/core/stack/Stack.kt @@ -47,4 +47,6 @@ public interface Stack { public operator fun plusAssign(item: Item) public operator fun plusAssign(items: List) + + public fun clearEvent() } diff --git a/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/Navigator.kt b/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/Navigator.kt index 9f27e52e..e9f8ae47 100644 --- a/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/Navigator.kt +++ b/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/Navigator.kt @@ -7,9 +7,9 @@ import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.saveable.SaveableStateHolder +import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf -import cafe.adriel.voyager.core.hook.clearHooks -import cafe.adriel.voyager.core.hook.hooks +import cafe.adriel.voyager.core.lifecycle.rememberScreenLifecycleOwner import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.stack.Stack import cafe.adriel.voyager.core.stack.StackEvent @@ -64,11 +64,12 @@ public fun Navigator( val navigator = rememberNavigator(screens, LocalNavigator.current) val currentScreen = navigator.lastItem - val hooks = currentScreen.hooks + val lifecycleOwner = rememberScreenLifecycleOwner(currentScreen) + val hooks = lifecycleOwner.getHooks() CompositionLocalProvider( LocalNavigator provides navigator, - *hooks.providers.map { it.provide() }.toTypedArray() + *hooks.providers.toTypedArray() ) { content(navigator) @@ -77,9 +78,9 @@ public fun Navigator( DisposableEffect(currentScreen.key) { onDispose { if (navigator.lastEvent in disposableEvents) { + hooks.disposer() navigator.stateHolder.removeState(currentScreen.key) - hooks.disposers.forEach { it.dispose() } - currentScreen.clearHooks() + navigator.clearEvent() } } } diff --git a/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/TabNavigator.kt b/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/TabNavigator.kt index 42b4738e..d74241a6 100644 --- a/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/TabNavigator.kt +++ b/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/TabNavigator.kt @@ -9,8 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.SaveableStateHolder import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf -import cafe.adriel.voyager.core.hook.clearHooks -import cafe.adriel.voyager.core.hook.hooks +import cafe.adriel.voyager.core.lifecycle.rememberScreenLifecycleOwner import cafe.adriel.voyager.navigator.tab.internal.rememberTabNavigator public typealias TabNavigatorContent = @Composable (tabNavigator: TabNavigator) -> Unit @@ -25,19 +24,19 @@ public fun TabNavigator( ) { val tabNavigator = rememberTabNavigator(tab) val currentTab = tabNavigator.current - val hooks = currentTab.hooks + val lifecycleOwner = rememberScreenLifecycleOwner(currentTab) + val hooks = lifecycleOwner.getHooks() CompositionLocalProvider( LocalTabNavigator provides tabNavigator, - *hooks.providers.map { it.provide() }.toTypedArray() + *hooks.providers.toTypedArray() ) { content(tabNavigator) } DisposableEffect(tabNavigator) { onDispose { - hooks.disposers.forEach { it.dispose() } - currentTab.clearHooks() + hooks.disposer() } } }