diff --git a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/model/ScreenModelStore.kt b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/model/ScreenModelStore.kt index 7ed8c8d5..da4d6216 100644 --- a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/model/ScreenModelStore.kt +++ b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/model/ScreenModelStore.kt @@ -1,6 +1,7 @@ package cafe.adriel.voyager.core.model import androidx.compose.runtime.DisallowComposableCalls +import cafe.adriel.voyager.core.annotation.InternalVoyagerApi import cafe.adriel.voyager.core.concurrent.ThreadSafeMap import cafe.adriel.voyager.core.lifecycle.ScreenDisposable import cafe.adriel.voyager.core.platform.multiplatformName @@ -27,7 +28,12 @@ public object ScreenModelStore : ScreenDisposable { @PublishedApi internal inline fun getKey(screen: Screen, tag: String?): ScreenModelKey = - "${screen.key}:${T::class.multiplatformName}:${tag ?: "default"}" + getKey(screen.key, tag) + + // Public: used in Navigator Scoped ScreenModels + @InternalVoyagerApi + public inline fun getKey(holderKey: String, tag: String?): ScreenModelKey = + "${holderKey}:${T::class.multiplatformName}:${tag ?: "default"}" @PublishedApi internal fun getDependencyKey(screenModel: ScreenModel, name: String): DependencyKey = @@ -48,8 +54,16 @@ public object ScreenModelStore : ScreenDisposable { screen: Screen, tag: String?, factory: @DisallowComposableCalls () -> T + ): T = getOrPut(screen.key, tag, factory) + + // Public: used in Navigator Scoped ScreenModels + @InternalVoyagerApi + public inline fun getOrPut( + holderKey: String, + tag: String?, + factory: @DisallowComposableCalls () -> T ): T { - val key = getKey(screen, tag) + val key = getKey(holderKey, tag) lastScreenModelKey.value = key return screenModels.getOrPut(key, factory) as T } @@ -68,15 +82,13 @@ public object ScreenModelStore : ScreenDisposable { } override fun onDispose(screen: Screen) { - screenModels.onEach(screen) { key -> - screenModels[key]?.onDispose() - screenModels -= key - } + disposeHolder(screen.key) + } - dependencies.onEach(screen) { key -> - dependencies[key]?.let { (instance, onDispose) -> onDispose(instance) } - dependencies -= key - } + // Public: used in Navigator Scoped ScreenModels + @InternalVoyagerApi + public fun onDisposeNavigator(navigatorKey: String) { + disposeHolder(navigatorKey) } @Deprecated( @@ -87,9 +99,22 @@ public object ScreenModelStore : ScreenDisposable { onDispose(screen) } - private fun Map.onEach(screen: Screen, block: (String) -> Unit) = + private fun disposeHolder(holderKey: String) { + screenModels.onEachHolder(holderKey) { key -> + screenModels[key]?.onDispose() + screenModels -= key + } + + dependencies.onEachHolder(holderKey) { key -> + dependencies[key]?.let { (instance, onDispose) -> onDispose(instance) } + dependencies -= key + } + } + + + private fun Map.onEachHolder(holderKey: String, block: (String) -> Unit) = asSequence() - .filter { it.key.startsWith(screen.key) } + .filter { it.key.startsWith(holderKey) } .map { it.key } .forEach(block) } diff --git a/voyager-hilt/build.gradle.kts b/voyager-hilt/build.gradle.kts index 61d99376..e7232e16 100644 --- a/voyager-hilt/build.gradle.kts +++ b/voyager-hilt/build.gradle.kts @@ -20,6 +20,7 @@ kapt { dependencies { api(projects.voyagerAndroidx) + api(projects.voyagerNavigator) implementation(libs.compose.runtime) implementation(libs.compose.ui) diff --git a/voyager-hilt/src/main/java/cafe/adriel/voyager/hilt/ScreenModel.kt b/voyager-hilt/src/main/java/cafe/adriel/voyager/hilt/ScreenModel.kt index e08461e5..26f5841b 100644 --- a/voyager-hilt/src/main/java/cafe/adriel/voyager/hilt/ScreenModel.kt +++ b/voyager-hilt/src/main/java/cafe/adriel/voyager/hilt/ScreenModel.kt @@ -2,10 +2,13 @@ package cafe.adriel.voyager.hilt import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi 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.hilt.internal.componentActivity +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.screenModel.rememberNavigatorScreenModel import dagger.hilt.android.EntryPointAccessors /** @@ -57,3 +60,56 @@ public inline fun Scre factory.invoke(screenFactory as F) } } + +/** + * Provide a [ScreenModel] getting from Hilt graph, lifecycle bounded to the Navigator. + * + * @return A new instance of [ScreenModel] or the same instance remembered by the composition + */ +@ExperimentalVoyagerApi +@Composable +public inline fun Navigator.getNavigatorScreenModel( + tag: String? = null +): T { + val context = LocalContext.current + return rememberNavigatorScreenModel(tag) { + val screenModels = EntryPointAccessors + .fromActivity(context.componentActivity, ScreenModelEntryPoint::class.java) + .screenModels() + val model = screenModels[T::class.java]?.get() + ?: error( + "${T::class.java} not found in hilt graph.\nPlease, check if you have a Multibinding " + + "declaration to your ScreenModel using @IntoMap and " + + "@ScreenModelKey(${T::class.qualifiedName}::class)" + ) + model as T + } +} + +/** + * Provide a [ScreenModel] using a custom [ScreenModelFactory], lifecycle bounded to the Navigator. + * The [ScreenModelFactory] is provided by Hilt graph. + * + * @param factory A function that receives a [ScreenModelFactory] and returns a [ScreenModel] created by the custom factory + * @return A new instance of [ScreenModel] or the same instance remembered by the composition + */ +@ExperimentalVoyagerApi +@Composable +public inline fun Navigator.getNavigatorScreenModel( + tag: String? = null, + noinline factory: (F) -> T +): T { + val context = LocalContext.current + return rememberNavigatorScreenModel(tag) { + val screenFactories = EntryPointAccessors + .fromActivity(context.componentActivity, ScreenModelEntryPoint::class.java) + .screenModelFactories() + val screenFactory = screenFactories[F::class.java]?.get() + ?: error( + "${F::class.java} not found in hilt graph.\nPlease, check if you have a Multibinding " + + "declaration to your ScreenModelFactory using @IntoMap and " + + "@ScreenModelFactoryKey(${F::class.qualifiedName}::class)" + ) + factory.invoke(screenFactory as F) + } +} diff --git a/voyager-kodein/build.gradle.kts b/voyager-kodein/build.gradle.kts index 422ceebe..d6bea9d9 100644 --- a/voyager-kodein/build.gradle.kts +++ b/voyager-kodein/build.gradle.kts @@ -16,7 +16,7 @@ kotlin { val commonMain by getting { dependencies { api(projects.voyagerCore) - implementation(projects.voyagerNavigator) + api(projects.voyagerNavigator) compileOnly(compose.runtime) compileOnly(compose.runtimeSaveable) implementation(libs.kodein) diff --git a/voyager-kodein/src/commonMain/kotlin/cafe/adriel/voyager/kodein/ScreenModel.kt b/voyager-kodein/src/commonMain/kotlin/cafe/adriel/voyager/kodein/ScreenModel.kt index 17095f73..70c3d332 100644 --- a/voyager-kodein/src/commonMain/kotlin/cafe/adriel/voyager/kodein/ScreenModel.kt +++ b/voyager-kodein/src/commonMain/kotlin/cafe/adriel/voyager/kodein/ScreenModel.kt @@ -1,9 +1,12 @@ package cafe.adriel.voyager.kodein import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi 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.navigator.Navigator +import cafe.adriel.voyager.navigator.screenModel.rememberNavigatorScreenModel import org.kodein.di.compose.localDI import org.kodein.di.direct import org.kodein.di.provider @@ -22,3 +25,20 @@ public inline fun Screen.rememberScre ): T = with(localDI()) { rememberScreenModel(tag = tag?.toString()) { direct.provider(tag, arg)() } } + +@ExperimentalVoyagerApi +@Composable +public inline fun Navigator.rememberNavigatorScreenModel( + tag: Any? = null +): T = with(localDI()) { + rememberNavigatorScreenModel(tag = tag?.toString()) { direct.provider(tag)() } +} + +@ExperimentalVoyagerApi +@Composable +public inline fun Navigator.rememberNavigatorScreenModel( + tag: Any? = null, + arg: A +): T = with(localDI()) { + rememberNavigatorScreenModel(tag = tag?.toString()) { direct.provider(tag, arg)() } +} diff --git a/voyager-koin/build.gradle.kts b/voyager-koin/build.gradle.kts index dd8b16b0..81e81707 100644 --- a/voyager-koin/build.gradle.kts +++ b/voyager-koin/build.gradle.kts @@ -19,6 +19,7 @@ kotlin { val commonMain by getting { dependencies { api(projects.voyagerCore) + api(projects.voyagerNavigator) compileOnly(compose.runtime) compileOnly(compose.runtimeSaveable) diff --git a/voyager-koin/src/commonMain/kotlin/cafe/adriel/voyager/koin/ScreenModel.kt b/voyager-koin/src/commonMain/kotlin/cafe/adriel/voyager/koin/ScreenModel.kt index e4a8bb0d..38539bb5 100644 --- a/voyager-koin/src/commonMain/kotlin/cafe/adriel/voyager/koin/ScreenModel.kt +++ b/voyager-koin/src/commonMain/kotlin/cafe/adriel/voyager/koin/ScreenModel.kt @@ -1,9 +1,12 @@ package cafe.adriel.voyager.koin import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi 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.navigator.Navigator +import cafe.adriel.voyager.navigator.screenModel.rememberNavigatorScreenModel import org.koin.compose.getKoin import org.koin.core.parameter.ParametersDefinition import org.koin.core.qualifier.Qualifier @@ -16,3 +19,13 @@ public inline fun Screen.getScreenModel( val koin = getKoin() return rememberScreenModel(tag = qualifier?.value) { koin.get(qualifier, parameters) } } + +@ExperimentalVoyagerApi +@Composable +public inline fun Navigator.getNavigatorScreenModel( + qualifier: Qualifier? = null, + noinline parameters: ParametersDefinition? = null +): T { + val koin = getKoin() + return rememberNavigatorScreenModel(tag = qualifier?.value) { koin.get(qualifier, parameters) } +} diff --git a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/screenModel/NavigatorScreenModel.kt b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/screenModel/NavigatorScreenModel.kt new file mode 100644 index 00000000..ea656231 --- /dev/null +++ b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/screenModel/NavigatorScreenModel.kt @@ -0,0 +1,35 @@ +package cafe.adriel.voyager.navigator.screenModel + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisallowComposableCalls +import androidx.compose.runtime.remember +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.core.annotation.InternalVoyagerApi +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.ScreenModelStore +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.lifecycle.NavigatorDisposable +import cafe.adriel.voyager.navigator.lifecycle.NavigatorLifecycleStore + +@ExperimentalVoyagerApi +@Composable +public inline fun Navigator.rememberNavigatorScreenModel( + tag: String? = null, + crossinline factory: @DisallowComposableCalls () -> T +): T { + // register the navigator lifecycle listener if is not already registered + remember(this) { + NavigatorLifecycleStore.register(this) { NavigatorScreenModelDisposer } + } + + return remember(ScreenModelStore.getKey(this.key, tag)) { + ScreenModelStore.getOrPut(this.key, tag, factory) + } +} + +@InternalVoyagerApi +public object NavigatorScreenModelDisposer : NavigatorDisposable { + override fun onDispose(navigator: Navigator) { + ScreenModelStore.onDisposeNavigator(navigator.key) + } +}