Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Navigator scoped ScreenModel #233

Merged
merged 3 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -27,7 +28,12 @@ public object ScreenModelStore : ScreenDisposable {

@PublishedApi
internal inline fun <reified T : ScreenModel> getKey(screen: Screen, tag: String?): ScreenModelKey =
"${screen.key}:${T::class.multiplatformName}:${tag ?: "default"}"
getKey<T>(screen.key, tag)

// Public: used in Navigator Scoped ScreenModels
@InternalVoyagerApi
public inline fun <reified T : ScreenModel> getKey(holderKey: String, tag: String?): ScreenModelKey =
"${holderKey}:${T::class.multiplatformName}:${tag ?: "default"}"

@PublishedApi
internal fun getDependencyKey(screenModel: ScreenModel, name: String): DependencyKey =
Expand All @@ -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 <reified T : ScreenModel> getOrPut(
holderKey: String,
tag: String?,
factory: @DisallowComposableCalls () -> T
): T {
val key = getKey<T>(screen, tag)
val key = getKey<T>(holderKey, tag)
lastScreenModelKey.value = key
return screenModels.getOrPut(key, factory) as T
}
Expand All @@ -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(
Expand All @@ -87,9 +99,22 @@ public object ScreenModelStore : ScreenDisposable {
onDispose(screen)
}

private fun Map<String, *>.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<String, *>.onEachHolder(holderKey: String, block: (String) -> Unit) =
asSequence()
.filter { it.key.startsWith(screen.key) }
.filter { it.key.startsWith(holderKey) }
.map { it.key }
.forEach(block)
}
1 change: 1 addition & 0 deletions voyager-hilt/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ kapt {

dependencies {
api(projects.voyagerAndroidx)
api(projects.voyagerNavigator)

implementation(libs.compose.runtime)
implementation(libs.compose.ui)
Expand Down
56 changes: 56 additions & 0 deletions voyager-hilt/src/main/java/cafe/adriel/voyager/hilt/ScreenModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -57,3 +60,56 @@ public inline fun <reified T : ScreenModel, reified F : ScreenModelFactory> 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 <reified T : ScreenModel> 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 <reified T : ScreenModel, reified F : ScreenModelFactory> 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)
}
}
2 changes: 1 addition & 1 deletion voyager-kodein/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,3 +25,20 @@ public inline fun <reified A : Any, reified T : ScreenModel> Screen.rememberScre
): T = with(localDI()) {
rememberScreenModel(tag = tag?.toString()) { direct.provider<A, T>(tag, arg)() }
}

@ExperimentalVoyagerApi
@Composable
public inline fun <reified T : ScreenModel> Navigator.rememberNavigatorScreenModel(
tag: Any? = null
): T = with(localDI()) {
rememberNavigatorScreenModel(tag = tag?.toString()) { direct.provider<T>(tag)() }
}

@ExperimentalVoyagerApi
@Composable
public inline fun <reified A : Any, reified T : ScreenModel> Navigator.rememberNavigatorScreenModel(
tag: Any? = null,
arg: A
): T = with(localDI()) {
rememberNavigatorScreenModel(tag = tag?.toString()) { direct.provider<A, T>(tag, arg)() }
}
1 change: 1 addition & 0 deletions voyager-koin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ kotlin {
val commonMain by getting {
dependencies {
api(projects.voyagerCore)
api(projects.voyagerNavigator)

compileOnly(compose.runtime)
compileOnly(compose.runtimeSaveable)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,3 +19,13 @@ public inline fun <reified T : ScreenModel> Screen.getScreenModel(
val koin = getKoin()
return rememberScreenModel(tag = qualifier?.value) { koin.get(qualifier, parameters) }
}

@ExperimentalVoyagerApi
@Composable
public inline fun <reified T : ScreenModel> Navigator.getNavigatorScreenModel(
qualifier: Qualifier? = null,
noinline parameters: ParametersDefinition? = null
): T {
val koin = getKoin()
return rememberNavigatorScreenModel(tag = qualifier?.value) { koin.get(qualifier, parameters) }
}
Original file line number Diff line number Diff line change
@@ -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 <reified T : ScreenModel> 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<T>(this.key, tag)) {
ScreenModelStore.getOrPut(this.key, tag, factory)
}
}

@InternalVoyagerApi
public object NavigatorScreenModelDisposer : NavigatorDisposable {
override fun onDispose(navigator: Navigator) {
ScreenModelStore.onDisposeNavigator(navigator.key)
}
}