Skip to content

Commit 626e4cb

Browse files
authored
Navigator scoped ScreenModel (#233)
* feat: Navigator scoped ScreenModel * feat: adding extension for all di remember module
1 parent 2105dcf commit 626e4cb

File tree

8 files changed

+164
-13
lines changed

8 files changed

+164
-13
lines changed

voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/model/ScreenModelStore.kt

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cafe.adriel.voyager.core.model
22

33
import androidx.compose.runtime.DisallowComposableCalls
4+
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
45
import cafe.adriel.voyager.core.concurrent.ThreadSafeMap
56
import cafe.adriel.voyager.core.lifecycle.ScreenDisposable
67
import cafe.adriel.voyager.core.platform.multiplatformName
@@ -27,7 +28,12 @@ public object ScreenModelStore : ScreenDisposable {
2728

2829
@PublishedApi
2930
internal inline fun <reified T : ScreenModel> getKey(screen: Screen, tag: String?): ScreenModelKey =
30-
"${screen.key}:${T::class.multiplatformName}:${tag ?: "default"}"
31+
getKey<T>(screen.key, tag)
32+
33+
// Public: used in Navigator Scoped ScreenModels
34+
@InternalVoyagerApi
35+
public inline fun <reified T : ScreenModel> getKey(holderKey: String, tag: String?): ScreenModelKey =
36+
"${holderKey}:${T::class.multiplatformName}:${tag ?: "default"}"
3137

3238
@PublishedApi
3339
internal fun getDependencyKey(screenModel: ScreenModel, name: String): DependencyKey =
@@ -48,8 +54,16 @@ public object ScreenModelStore : ScreenDisposable {
4854
screen: Screen,
4955
tag: String?,
5056
factory: @DisallowComposableCalls () -> T
57+
): T = getOrPut(screen.key, tag, factory)
58+
59+
// Public: used in Navigator Scoped ScreenModels
60+
@InternalVoyagerApi
61+
public inline fun <reified T : ScreenModel> getOrPut(
62+
holderKey: String,
63+
tag: String?,
64+
factory: @DisallowComposableCalls () -> T
5165
): T {
52-
val key = getKey<T>(screen, tag)
66+
val key = getKey<T>(holderKey, tag)
5367
lastScreenModelKey.value = key
5468
return screenModels.getOrPut(key, factory) as T
5569
}
@@ -68,15 +82,13 @@ public object ScreenModelStore : ScreenDisposable {
6882
}
6983

7084
override fun onDispose(screen: Screen) {
71-
screenModels.onEach(screen) { key ->
72-
screenModels[key]?.onDispose()
73-
screenModels -= key
74-
}
85+
disposeHolder(screen.key)
86+
}
7587

76-
dependencies.onEach(screen) { key ->
77-
dependencies[key]?.let { (instance, onDispose) -> onDispose(instance) }
78-
dependencies -= key
79-
}
88+
// Public: used in Navigator Scoped ScreenModels
89+
@InternalVoyagerApi
90+
public fun onDisposeNavigator(navigatorKey: String) {
91+
disposeHolder(navigatorKey)
8092
}
8193

8294
@Deprecated(
@@ -87,9 +99,22 @@ public object ScreenModelStore : ScreenDisposable {
8799
onDispose(screen)
88100
}
89101

90-
private fun Map<String, *>.onEach(screen: Screen, block: (String) -> Unit) =
102+
private fun disposeHolder(holderKey: String) {
103+
screenModels.onEachHolder(holderKey) { key ->
104+
screenModels[key]?.onDispose()
105+
screenModels -= key
106+
}
107+
108+
dependencies.onEachHolder(holderKey) { key ->
109+
dependencies[key]?.let { (instance, onDispose) -> onDispose(instance) }
110+
dependencies -= key
111+
}
112+
}
113+
114+
115+
private fun Map<String, *>.onEachHolder(holderKey: String, block: (String) -> Unit) =
91116
asSequence()
92-
.filter { it.key.startsWith(screen.key) }
117+
.filter { it.key.startsWith(holderKey) }
93118
.map { it.key }
94119
.forEach(block)
95120
}

voyager-hilt/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ kapt {
2020

2121
dependencies {
2222
api(projects.voyagerAndroidx)
23+
api(projects.voyagerNavigator)
2324

2425
implementation(libs.compose.runtime)
2526
implementation(libs.compose.ui)

voyager-hilt/src/main/java/cafe/adriel/voyager/hilt/ScreenModel.kt

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ package cafe.adriel.voyager.hilt
22

33
import androidx.compose.runtime.Composable
44
import androidx.compose.ui.platform.LocalContext
5+
import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi
56
import cafe.adriel.voyager.core.model.ScreenModel
67
import cafe.adriel.voyager.core.model.rememberScreenModel
78
import cafe.adriel.voyager.core.screen.Screen
89
import cafe.adriel.voyager.hilt.internal.componentActivity
10+
import cafe.adriel.voyager.navigator.Navigator
11+
import cafe.adriel.voyager.navigator.screenModel.rememberNavigatorScreenModel
912
import dagger.hilt.android.EntryPointAccessors
1013

1114
/**
@@ -57,3 +60,56 @@ public inline fun <reified T : ScreenModel, reified F : ScreenModelFactory> Scre
5760
factory.invoke(screenFactory as F)
5861
}
5962
}
63+
64+
/**
65+
* Provide a [ScreenModel] getting from Hilt graph, lifecycle bounded to the Navigator.
66+
*
67+
* @return A new instance of [ScreenModel] or the same instance remembered by the composition
68+
*/
69+
@ExperimentalVoyagerApi
70+
@Composable
71+
public inline fun <reified T : ScreenModel> Navigator.getNavigatorScreenModel(
72+
tag: String? = null
73+
): T {
74+
val context = LocalContext.current
75+
return rememberNavigatorScreenModel(tag) {
76+
val screenModels = EntryPointAccessors
77+
.fromActivity(context.componentActivity, ScreenModelEntryPoint::class.java)
78+
.screenModels()
79+
val model = screenModels[T::class.java]?.get()
80+
?: error(
81+
"${T::class.java} not found in hilt graph.\nPlease, check if you have a Multibinding " +
82+
"declaration to your ScreenModel using @IntoMap and " +
83+
"@ScreenModelKey(${T::class.qualifiedName}::class)"
84+
)
85+
model as T
86+
}
87+
}
88+
89+
/**
90+
* Provide a [ScreenModel] using a custom [ScreenModelFactory], lifecycle bounded to the Navigator.
91+
* The [ScreenModelFactory] is provided by Hilt graph.
92+
*
93+
* @param factory A function that receives a [ScreenModelFactory] and returns a [ScreenModel] created by the custom factory
94+
* @return A new instance of [ScreenModel] or the same instance remembered by the composition
95+
*/
96+
@ExperimentalVoyagerApi
97+
@Composable
98+
public inline fun <reified T : ScreenModel, reified F : ScreenModelFactory> Navigator.getNavigatorScreenModel(
99+
tag: String? = null,
100+
noinline factory: (F) -> T
101+
): T {
102+
val context = LocalContext.current
103+
return rememberNavigatorScreenModel(tag) {
104+
val screenFactories = EntryPointAccessors
105+
.fromActivity(context.componentActivity, ScreenModelEntryPoint::class.java)
106+
.screenModelFactories()
107+
val screenFactory = screenFactories[F::class.java]?.get()
108+
?: error(
109+
"${F::class.java} not found in hilt graph.\nPlease, check if you have a Multibinding " +
110+
"declaration to your ScreenModelFactory using @IntoMap and " +
111+
"@ScreenModelFactoryKey(${F::class.qualifiedName}::class)"
112+
)
113+
factory.invoke(screenFactory as F)
114+
}
115+
}

voyager-kodein/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ kotlin {
1616
val commonMain by getting {
1717
dependencies {
1818
api(projects.voyagerCore)
19-
implementation(projects.voyagerNavigator)
19+
api(projects.voyagerNavigator)
2020
compileOnly(compose.runtime)
2121
compileOnly(compose.runtimeSaveable)
2222
implementation(libs.kodein)

voyager-kodein/src/commonMain/kotlin/cafe/adriel/voyager/kodein/ScreenModel.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package cafe.adriel.voyager.kodein
22

33
import androidx.compose.runtime.Composable
4+
import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi
45
import cafe.adriel.voyager.core.model.ScreenModel
56
import cafe.adriel.voyager.core.model.rememberScreenModel
67
import cafe.adriel.voyager.core.screen.Screen
8+
import cafe.adriel.voyager.navigator.Navigator
9+
import cafe.adriel.voyager.navigator.screenModel.rememberNavigatorScreenModel
710
import org.kodein.di.compose.localDI
811
import org.kodein.di.direct
912
import org.kodein.di.provider
@@ -22,3 +25,20 @@ public inline fun <reified A : Any, reified T : ScreenModel> Screen.rememberScre
2225
): T = with(localDI()) {
2326
rememberScreenModel(tag = tag?.toString()) { direct.provider<A, T>(tag, arg)() }
2427
}
28+
29+
@ExperimentalVoyagerApi
30+
@Composable
31+
public inline fun <reified T : ScreenModel> Navigator.rememberNavigatorScreenModel(
32+
tag: Any? = null
33+
): T = with(localDI()) {
34+
rememberNavigatorScreenModel(tag = tag?.toString()) { direct.provider<T>(tag)() }
35+
}
36+
37+
@ExperimentalVoyagerApi
38+
@Composable
39+
public inline fun <reified A : Any, reified T : ScreenModel> Navigator.rememberNavigatorScreenModel(
40+
tag: Any? = null,
41+
arg: A
42+
): T = with(localDI()) {
43+
rememberNavigatorScreenModel(tag = tag?.toString()) { direct.provider<A, T>(tag, arg)() }
44+
}

voyager-koin/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ kotlin {
1919
val commonMain by getting {
2020
dependencies {
2121
api(projects.voyagerCore)
22+
api(projects.voyagerNavigator)
2223

2324
compileOnly(compose.runtime)
2425
compileOnly(compose.runtimeSaveable)

voyager-koin/src/commonMain/kotlin/cafe/adriel/voyager/koin/ScreenModel.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package cafe.adriel.voyager.koin
22

33
import androidx.compose.runtime.Composable
4+
import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi
45
import cafe.adriel.voyager.core.model.ScreenModel
56
import cafe.adriel.voyager.core.model.rememberScreenModel
67
import cafe.adriel.voyager.core.screen.Screen
8+
import cafe.adriel.voyager.navigator.Navigator
9+
import cafe.adriel.voyager.navigator.screenModel.rememberNavigatorScreenModel
710
import org.koin.compose.getKoin
811
import org.koin.core.parameter.ParametersDefinition
912
import org.koin.core.qualifier.Qualifier
@@ -16,3 +19,13 @@ public inline fun <reified T : ScreenModel> Screen.getScreenModel(
1619
val koin = getKoin()
1720
return rememberScreenModel(tag = qualifier?.value) { koin.get(qualifier, parameters) }
1821
}
22+
23+
@ExperimentalVoyagerApi
24+
@Composable
25+
public inline fun <reified T : ScreenModel> Navigator.getNavigatorScreenModel(
26+
qualifier: Qualifier? = null,
27+
noinline parameters: ParametersDefinition? = null
28+
): T {
29+
val koin = getKoin()
30+
return rememberNavigatorScreenModel(tag = qualifier?.value) { koin.get(qualifier, parameters) }
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package cafe.adriel.voyager.navigator.screenModel
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.DisallowComposableCalls
5+
import androidx.compose.runtime.remember
6+
import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi
7+
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
8+
import cafe.adriel.voyager.core.model.ScreenModel
9+
import cafe.adriel.voyager.core.model.ScreenModelStore
10+
import cafe.adriel.voyager.navigator.Navigator
11+
import cafe.adriel.voyager.navigator.lifecycle.NavigatorDisposable
12+
import cafe.adriel.voyager.navigator.lifecycle.NavigatorLifecycleStore
13+
14+
@ExperimentalVoyagerApi
15+
@Composable
16+
public inline fun <reified T : ScreenModel> Navigator.rememberNavigatorScreenModel(
17+
tag: String? = null,
18+
crossinline factory: @DisallowComposableCalls () -> T
19+
): T {
20+
// register the navigator lifecycle listener if is not already registered
21+
remember(this) {
22+
NavigatorLifecycleStore.register(this) { NavigatorScreenModelDisposer }
23+
}
24+
25+
return remember(ScreenModelStore.getKey<T>(this.key, tag)) {
26+
ScreenModelStore.getOrPut(this.key, tag, factory)
27+
}
28+
}
29+
30+
@InternalVoyagerApi
31+
public object NavigatorScreenModelDisposer : NavigatorDisposable {
32+
override fun onDispose(navigator: Navigator) {
33+
ScreenModelStore.onDisposeNavigator(navigator.key)
34+
}
35+
}

0 commit comments

Comments
 (0)