Skip to content

Commit

Permalink
Navigator scoped ScreenModel (#233)
Browse files Browse the repository at this point in the history
* feat: Navigator scoped ScreenModel

* feat: adding extension for all di remember module
  • Loading branch information
DevSrSouza authored Oct 22, 2023
1 parent 2105dcf commit 626e4cb
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 13 deletions.
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)
}
}

0 comments on commit 626e4cb

Please sign in to comment.