Skip to content

Commit 498515c

Browse files
committed
Renderer bindings for Metro
Create a `Renderer` dependency graph for Metro and combine it with the `Renderer` component from kotlin-inject. This allows for an easier migration from kotlin-inject to Metro and eventually mixing both dependency injection frameworks if needed. See #119
1 parent 3819b9d commit 498515c

File tree

12 files changed

+227
-63
lines changed

12 files changed

+227
-63
lines changed

di-common/public/src/commonMain/kotlin/software/amazon/app/platform/inject/ContributesRenderer.kt

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import software.amazon.lastmile.kotlin.inject.anvil.extend.ContributingAnnotatio
1212
* class IncrementRenderer : Renderer<IncrementPresenter.Model>()
1313
* ```
1414
*
15-
* This annotation would generated following component interface:
15+
* This annotation would generated following component interface for kotlin-inject:
1616
* ```
1717
* @ContributesTo(RendererScope::class)
1818
* interface IncrementRendererComponent {
@@ -27,29 +27,33 @@ import software.amazon.lastmile.kotlin.inject.anvil.extend.ContributingAnnotatio
2727
* }
2828
* ```
2929
*
30+
* Or following graph for Metro:
31+
* ```
32+
* @ContributesTo(RendererScope::class)
33+
* interface IncrementRendererGraph {
34+
* @Provides
35+
* @IntoMap
36+
* @RendererKey(IncrementPresenter.Model::class)
37+
* fun provideIncrementRendererIncrementPresenterModel(
38+
* renderer: Provider<IncrementRenderer>,
39+
* ): Renderer<*> = renderer()
40+
*
41+
* @Provides
42+
* fun provideIncrementRenderer(): IncrementRenderer = IncrementRenderer()
43+
* }
44+
* ```
45+
*
3046
* Although strongly discouraged, your renderer is allowed to have an `@Inject constructor`. The
31-
* only valid use case is for injecting other renderers.
47+
* only valid use case is for injecting other renderers returned by the `RendererFactory`.
3248
*
3349
* ```
3450
* @Inject
3551
* @ContributesRenderer
3652
* class IncrementRenderer(
37-
* private val otherRenderer: OtherRenderer
53+
* private val rendererFactory: RendererFactory
3854
* ) : Renderer<IncrementPresenter.Model>() {
3955
* ```
4056
*
41-
* In this case following module would be generated:
42-
* ```
43-
* @ContributesTo(RendererScope::class)
44-
* abstract class IncrementRendererModule {
45-
* @Provides
46-
* @IntoMap
47-
* fun provideIncrementRendererIntoMap(
48-
* renderer: () -> IncrementRenderer,
49-
* ): Pair<KClass<out BaseModel>, () -> Renderer<*>> = IncrementRenderer.Model::class to renderer
50-
* }
51-
* ```
52-
*
5357
* If the model type is a sealed hierarchy, then for each explicit type a binding method will be
5458
* generated.
5559
*/

metro-extensions/contribute/impl-code-generators/src/test/kotlin/software/amazon/app/platform/inject/metro/CommonSourceCode.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ package software.amazon.app.platform.inject.metro
44

55
import com.tschuchort.compiletesting.JvmCompilationResult
66
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
7-
import software.amazon.test.TestRendererComponent
7+
import software.amazon.test.TestRendererGraph
88

99
internal val JvmCompilationResult.graphInterface: Class<*>
1010
get() = classLoader.loadClass("software.amazon.test.GraphInterface")
1111

12-
internal fun Class<*>.newTestRendererComponent(): TestRendererComponent {
12+
internal fun Class<*>.newTestRendererGraph(): TestRendererGraph {
1313
val companionObject = fields.single().get(null)
1414
return classes
1515
.single { it.simpleName == "Companion" }
1616
.declaredMethods
1717
.single { it.name == "create" }
18-
.invoke(companionObject) as TestRendererComponent
18+
.invoke(companionObject) as TestRendererGraph
1919
}

metro-extensions/contribute/impl-code-generators/src/test/kotlin/software/amazon/app/platform/inject/metro/processor/ContributesRendererProcessorTest.kt

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
2323
import org.junit.jupiter.api.Test
2424
import software.amazon.app.platform.inject.metro.compile
2525
import software.amazon.app.platform.inject.metro.graphInterface
26-
import software.amazon.app.platform.inject.metro.newTestRendererComponent
26+
import software.amazon.app.platform.inject.metro.newTestRendererGraph
2727
import software.amazon.app.platform.ksp.inner
2828
import software.amazon.app.platform.ksp.isAnnotatedWith
2929
import software.amazon.app.platform.metro.METRO_LOOKUP_PACKAGE
@@ -90,12 +90,12 @@ class ContributesRendererProcessorTest {
9090
assertThat(getAnnotation(RendererKey::class.java).value).isEqualTo(model)
9191
}
9292

93-
assertThat(graphInterface.newTestRendererComponent().renderers.keys).containsOnly(model)
93+
assertThat(graphInterface.newTestRendererGraph().renderers.keys).containsOnly(model)
9494

95-
assertThat(graphInterface.newTestRendererComponent().modelToRendererMapping.keys)
95+
assertThat(graphInterface.newTestRendererGraph().modelToRendererMapping.keys)
9696
.containsOnly(model)
9797

98-
assertThat(graphInterface.newTestRendererComponent().modelToRendererMapping.values)
98+
assertThat(graphInterface.newTestRendererGraph().modelToRendererMapping.values)
9999
.containsOnly(testRenderer.kotlin)
100100
}
101101
}
@@ -148,7 +148,7 @@ class ContributesRendererProcessorTest {
148148
assertThat(getAnnotation(RendererKey::class.java).value).isEqualTo(model)
149149
}
150150

151-
assertThat(graphInterface.newTestRendererComponent().renderers.keys).containsOnly(model)
151+
assertThat(graphInterface.newTestRendererGraph().renderers.keys).containsOnly(model)
152152
}
153153
}
154154

@@ -197,7 +197,7 @@ class ContributesRendererProcessorTest {
197197
assertThat(this).isAnnotatedWith(IntoMap::class)
198198
}
199199

200-
assertThat(graphInterface.newTestRendererComponent().renderers.keys)
200+
assertThat(graphInterface.newTestRendererGraph().renderers.keys)
201201
.containsOnly(presenter.model.kotlin)
202202
}
203203
}
@@ -222,7 +222,7 @@ class ContributesRendererProcessorTest {
222222
""",
223223
graphInterfaceSource,
224224
) {
225-
assertThat(graphInterface.newTestRendererComponent().renderers.keys).containsOnly(model)
225+
assertThat(graphInterface.newTestRendererGraph().renderers.keys).containsOnly(model)
226226
}
227227
}
228228

@@ -371,7 +371,7 @@ class ContributesRendererProcessorTest {
371371
assertThat(it).isAnnotatedWith(RendererKey::class)
372372
}
373373

374-
assertThat(graphInterface.newTestRendererComponent().renderers.keys)
374+
assertThat(graphInterface.newTestRendererGraph().renderers.keys)
375375
.containsExactlyInAnyOrder(
376376
presenter.model.kotlin,
377377
presenter.model.inner.kotlin,
@@ -402,7 +402,7 @@ class ContributesRendererProcessorTest {
402402
assertThat(it).isAnnotatedWith(ForScope::class)
403403
}
404404

405-
assertThat(graphInterface.newTestRendererComponent().modelToRendererMapping.keys)
405+
assertThat(graphInterface.newTestRendererGraph().modelToRendererMapping.keys)
406406
.containsExactlyInAnyOrder(
407407
presenter.model.kotlin,
408408
presenter.model.inner.kotlin,
@@ -411,7 +411,7 @@ class ContributesRendererProcessorTest {
411411
presenter.model.model2.kotlin,
412412
)
413413

414-
assertThat(graphInterface.newTestRendererComponent().modelToRendererMapping.values.distinct())
414+
assertThat(graphInterface.newTestRendererGraph().modelToRendererMapping.values.distinct())
415415
.containsOnly(testRenderer.kotlin)
416416
}
417417
}
@@ -459,13 +459,13 @@ class ContributesRendererProcessorTest {
459459
)
460460
.containsOnly("provideSoftwareAmazonTestTestRendererPresenterModelKey")
461461

462-
assertThat(graphInterface.newTestRendererComponent().renderers.keys)
462+
assertThat(graphInterface.newTestRendererGraph().renderers.keys)
463463
.containsOnly(presenter.model.kotlin)
464464

465-
assertThat(graphInterface.newTestRendererComponent().modelToRendererMapping.keys)
465+
assertThat(graphInterface.newTestRendererGraph().modelToRendererMapping.keys)
466466
.containsOnly(presenter.model.kotlin)
467467

468-
assertThat(graphInterface.newTestRendererComponent().modelToRendererMapping.values)
468+
assertThat(graphInterface.newTestRendererGraph().modelToRendererMapping.values)
469469
.containsOnly(testRenderer.kotlin)
470470
}
471471
}
@@ -501,7 +501,7 @@ class ContributesRendererProcessorTest {
501501
"provideSoftwareAmazonTestTestRendererModelKey",
502502
)
503503

504-
assertThat(graphInterface.newTestRendererComponent().renderers.keys).containsOnly(model)
504+
assertThat(graphInterface.newTestRendererGraph().renderers.keys).containsOnly(model)
505505
}
506506
}
507507

@@ -613,11 +613,11 @@ class ContributesRendererProcessorTest {
613613
import dev.zacsweers.metro.Provider
614614
import dev.zacsweers.metro.Provides
615615
import dev.zacsweers.metro.SingleIn
616-
import software.amazon.test.TestRendererComponent
616+
import software.amazon.test.TestRendererGraph
617617
618618
@DependencyGraph(RendererScope::class)
619619
@SingleIn(RendererScope::class)
620-
interface GraphInterface : TestRendererComponent {
620+
interface GraphInterface : TestRendererGraph {
621621
@Provides fun provideString(): String = "abc"
622622
623623
companion object {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import software.amazon.app.platform.presenter.BaseModel
77
import software.amazon.app.platform.renderer.Renderer
88
import software.amazon.app.platform.renderer.RendererScope
99

10-
interface TestRendererComponent {
10+
interface TestRendererGraph {
1111
val renderers: Map<KClass<out BaseModel>, Provider<Renderer<*>>>
1212

1313
@ForScope(RendererScope::class)

renderer/public/api/android/public.api

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
public final class metro/hints/SoftwareAmazonAppPlatformRendererRendererGraphFactoryAppScopeKt {
2+
public static final fun AppScope (Lsoftware/amazon/app/platform/renderer/RendererGraph$Factory;)V
3+
}
4+
15
public class software/amazon/app/platform/renderer/BaseRendererFactory : software/amazon/app/platform/renderer/RendererFactory {
26
public fun <init> (Lsoftware/amazon/app/platform/scope/RootScopeProvider;)V
37
public fun createRenderer (Lkotlin/reflect/KClass;)Lsoftware/amazon/app/platform/renderer/Renderer;
@@ -33,6 +37,18 @@ public final class software/amazon/app/platform/renderer/RendererFactoryKt {
3337
public static synthetic fun getRenderer$default (Lsoftware/amazon/app/platform/renderer/RendererFactory;Lsoftware/amazon/app/platform/presenter/BaseModel;IILjava/lang/Object;)Lsoftware/amazon/app/platform/renderer/Renderer;
3438
}
3539

40+
public abstract interface class software/amazon/app/platform/renderer/RendererGraph {
41+
public abstract fun getModelToRendererMapping ()Ljava/util/Map;
42+
public abstract fun getRenderers ()Ljava/util/Map;
43+
}
44+
45+
public abstract interface class software/amazon/app/platform/renderer/RendererGraph$Factory {
46+
public abstract fun createRendererGraph (Lsoftware/amazon/app/platform/renderer/RendererFactory;)Lsoftware/amazon/app/platform/renderer/RendererGraph;
47+
}
48+
49+
public abstract interface class software/amazon/app/platform/renderer/RendererGraph$Factory$$$MetroContributionToAppScope : software/amazon/app/platform/renderer/RendererGraph$Factory {
50+
}
51+
3652
public abstract class software/amazon/app/platform/renderer/RendererScope {
3753
}
3854

renderer/public/api/desktop/public.api

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
public final class metro/hints/SoftwareAmazonAppPlatformRendererRendererGraphFactoryAppScopeKt {
2+
public static final fun AppScope (Lsoftware/amazon/app/platform/renderer/RendererGraph$Factory;)V
3+
}
4+
15
public class software/amazon/app/platform/renderer/BaseRendererFactory : software/amazon/app/platform/renderer/RendererFactory {
26
public fun <init> (Lsoftware/amazon/app/platform/scope/RootScopeProvider;)V
37
public fun createRenderer (Lkotlin/reflect/KClass;)Lsoftware/amazon/app/platform/renderer/Renderer;
@@ -33,6 +37,18 @@ public final class software/amazon/app/platform/renderer/RendererFactoryKt {
3337
public static synthetic fun getRenderer$default (Lsoftware/amazon/app/platform/renderer/RendererFactory;Lsoftware/amazon/app/platform/presenter/BaseModel;IILjava/lang/Object;)Lsoftware/amazon/app/platform/renderer/Renderer;
3438
}
3539

40+
public abstract interface class software/amazon/app/platform/renderer/RendererGraph {
41+
public abstract fun getModelToRendererMapping ()Ljava/util/Map;
42+
public abstract fun getRenderers ()Ljava/util/Map;
43+
}
44+
45+
public abstract interface class software/amazon/app/platform/renderer/RendererGraph$Factory {
46+
public abstract fun createRendererGraph (Lsoftware/amazon/app/platform/renderer/RendererFactory;)Lsoftware/amazon/app/platform/renderer/RendererGraph;
47+
}
48+
49+
public abstract interface class software/amazon/app/platform/renderer/RendererGraph$Factory$$$MetroContributionToAppScope : software/amazon/app/platform/renderer/RendererGraph$Factory {
50+
}
51+
3652
public abstract class software/amazon/app/platform/renderer/RendererScope {
3753
}
3854

renderer/public/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ plugins {
44

55
appPlatformBuildSrc {
66
enableKotlinInject true
7+
enableMetro true
78
enablePublishing true
89
}
910

renderer/public/src/commonMain/kotlin/software/amazon/app/platform/renderer/BaseRendererFactory.kt

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import kotlinx.atomicfu.locks.SynchronizedObject
55
import kotlinx.atomicfu.locks.synchronized
66
import software.amazon.app.platform.presenter.BaseModel
77
import software.amazon.app.platform.scope.RootScopeProvider
8+
import software.amazon.app.platform.scope.Scope
89
import software.amazon.app.platform.scope.di.kotlinInjectComponent
10+
import software.amazon.app.platform.scope.di.metro.metroDependencyGraph
911

1012
/**
1113
* Default implementation for [RendererFactory]. Implementations usually override [createRenderer]
@@ -15,13 +17,21 @@ public open class BaseRendererFactory(rootScopeProvider: RootScopeProvider) : Re
1517

1618
private val rendererComponent =
1719
rootScopeProvider.rootScope
18-
.kotlinInjectComponent<RendererComponent.Parent>()
19-
.rendererComponent(this)
20+
.kotlinInjectComponentOrNull<RendererComponent.Parent>()
21+
?.rendererComponent(this)
2022

21-
private val renderers: Map<KClass<out BaseModel>, () -> Renderer<*>> = rendererComponent.renderers
23+
private val rendererGraph =
24+
rootScopeProvider.rootScope
25+
.metroDependencyGraphOrNull<RendererGraph.Factory>()
26+
?.createRendererGraph(this)
27+
28+
private val renderers: Map<KClass<out BaseModel>, () -> Renderer<*>> =
29+
rendererComponent?.renderers.orEmpty() +
30+
rendererGraph?.renderers.orEmpty().mapValues { { it.value() } }
2231

2332
private val cacheKeys: Map<KClass<out BaseModel>, KClass<out Renderer<*>>> =
24-
rendererComponent.modelToRendererMapping
33+
rendererComponent?.modelToRendererMapping.orEmpty() +
34+
rendererGraph?.modelToRendererMapping.orEmpty()
2535

2636
private val rendererCache = mutableMapOf<CacheKey, Renderer<*>>()
2737
private val lock = SynchronizedObject()
@@ -52,4 +62,22 @@ public open class BaseRendererFactory(rootScopeProvider: RootScopeProvider) : Re
5262
}
5363

5464
private data class CacheKey(val clazz: KClass<out Renderer<*>>, val rendererId: Int)
65+
66+
private companion object {
67+
inline fun <reified T : Any> Scope.metroDependencyGraphOrNull(): T? {
68+
return try {
69+
metroDependencyGraph<T>()
70+
} catch (_: NoSuchElementException) {
71+
null
72+
}
73+
}
74+
75+
inline fun <reified T : Any> Scope.kotlinInjectComponentOrNull(): T? {
76+
return try {
77+
kotlinInjectComponent<T>()
78+
} catch (_: NoSuchElementException) {
79+
null
80+
}
81+
}
82+
}
5583
}

renderer/public/src/commonMain/kotlin/software/amazon/app/platform/renderer/RendererComponent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent
77
import software.amazon.lastmile.kotlin.inject.anvil.ForScope
88
import software.amazon.lastmile.kotlin.inject.anvil.SingleIn
99

10-
/** Component that provides all [Renderer] instance from the dependency graph. */
10+
/** Component that provides all [Renderer] instance from the kotlin-inject dependency graph. */
1111
@ContributesSubcomponent(RendererScope::class)
1212
@SingleIn(RendererScope::class)
1313
public interface RendererComponent {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package software.amazon.app.platform.renderer
2+
3+
import dev.zacsweers.metro.AppScope
4+
import dev.zacsweers.metro.ContributesTo
5+
import dev.zacsweers.metro.ForScope
6+
import dev.zacsweers.metro.GraphExtension
7+
import dev.zacsweers.metro.Provider
8+
import dev.zacsweers.metro.Provides
9+
import dev.zacsweers.metro.SingleIn
10+
import kotlin.reflect.KClass
11+
import software.amazon.app.platform.presenter.BaseModel
12+
13+
/** Graph that provides all [Renderer] instance from the Metro dependency graph. */
14+
@GraphExtension(RendererScope::class)
15+
@SingleIn(RendererScope::class)
16+
public interface RendererGraph {
17+
/** All [Renderer]s provided in the dependency graph. */
18+
public val renderers: Map<KClass<out BaseModel>, Provider<Renderer<*>>>
19+
20+
/**
21+
* [RendererFactory]s cache renderers based on the model type. This works well, when there's a one
22+
* to one relationship between a model type and a renderer. However, for sealed hierarchies there
23+
* are multiple model types pointing to the same renderer.
24+
*
25+
* This map, given any model type as key, returns the type that should be used as key for caching.
26+
* For non-sealed hierarchies (aka a single model type per renderer) there is only single mapping
27+
* between the model and renderer class. For sealed hierarchies there are multiple entries with
28+
* all keys pointing to the same renderer class.
29+
*/
30+
@ForScope(RendererScope::class)
31+
public val modelToRendererMapping: Map<KClass<out BaseModel>, KClass<out Renderer<*>>>
32+
33+
/** The parent interface to create a [RendererGraph]. */
34+
@ContributesTo(AppScope::class)
35+
@GraphExtension.Factory
36+
public interface Factory {
37+
/** Creates a new [RendererGraph]. */
38+
public fun createRendererGraph(@Provides factory: RendererFactory): RendererGraph
39+
}
40+
}

0 commit comments

Comments
 (0)