diff --git a/docs/di.md b/docs/di.md index 3fb98aae..ec60300f 100644 --- a/docs/di.md +++ b/docs/di.md @@ -2,99 +2,113 @@ !!! note - App Platform provides support for [kotlin-inject-anvil](https://github.com/amzn/kotlin-inject-anvil) and - [Metro](https://zacsweers.github.io/metro) as dependency injection framework. You can choose which one to use and - even mix them if needed. Both frameworks are compile-time injection frameworks and ready for Kotlin Multiplatform - (Metro still runs into issues). They verify correctness of the object graph at build time and avoid crashes at - runtime. + App Platform provides support for [Metro](https://zacsweers.github.io/metro) and + [kotlin-inject-anvil](https://github.com/amzn/kotlin-inject-anvil) as dependency injection + frameworks. Metro is the recommended default, while `kotlin-inject-anvil` remains available as + the alternative and for existing codebases. Both frameworks are compile-time injection + frameworks and ready for Kotlin Multiplatform. They verify correctness of the object graph at + build time and avoid crashes at runtime. Enabling dependency injection is an opt-in feature through the Gradle DSL. The default value is `false`. ```groovy appPlatform { - enableKotlinInject true enableMetro true + enableKotlinInject true } ``` !!! tip - Consider taking a look at the [kotlin-inject-anvil documentation](https://github.com/amzn/kotlin-inject-anvil) or - [Metro documentation](https://zacsweers.github.io/metro) first. App Platform makes heavy use the of - `@ContributesBinding` and `@ContributesTo` annotations to decompose and assemble components / object graphs. + Start with the [Metro documentation](https://zacsweers.github.io/metro). Reach for the + [kotlin-inject-anvil documentation](https://github.com/amzn/kotlin-inject-anvil) when you are + maintaining the alternative path or migrating older code. App Platform makes heavy use of + `@ContributesBinding` and `@ContributesTo` annotations to decompose and assemble components / + object graphs. -## kotlin-inject-anvil +## Metro !!! note - `kotlin-inject-anvil` is an opt-in feature through the Gradle DSL. The default value is `false`. + Metro is an opt-in feature through the Gradle DSL. The default value is `false`. ```groovy appPlatform { - enableKotlinInject true + enableMetro true } ``` -### Component +### Dependency graph -Components are added as a service to the `Scope` class and can be obtained using the `kotlinInjectComponent()` extension -function: +Dependency graphs are added as a service to the `Scope` class and can be obtained using the `metroDependencyGraph()` +extension function: ```kotlin -scope.kotlinInjectComponent() +scope.metroDependencyGraph() ``` -In modularized projects, final components are defined in the `:app` modules, because the object graph has to -know about all features of the app. It is strongly recommended to create a component in each platform specific +In modularized projects, final graphs are defined in the `:app` modules, because the object graph has to +know about all features of the app. It is strongly recommended to create an object graph in each platform specific folder to provide platform specific types. === "Android" ```kotlin title="androidMain" - @SingleIn(AppScope::class) - @MergeComponent(AppScope::class) - abstract class AndroidAppComponent( - @get:Provides val application: Application, - @get:Provides val rootScopeProvider: RootScopeProvider, - ) + @DependencyGraph(AppScope::class) + interface AndroidAppGraph { + @DependencyGraph.Factory + fun interface Factory { + fun create( + @Provides application: Application, + @Provides rootScopeProvider: RootScopeProvider, + ): AndroidAppGraph + } + } ``` === "iOS" ```kotlin title="iosMain" - @SingleIn(AppScope::class) - @MergeComponent(AppScope::class) - abstract class IosAppComponent( - @get:Provides val uiApplication: UIApplication, - @get:Provides val rootScopeProvider: RootScopeProvider, - ) + @DependencyGraph(AppScope::class) + interface IosAppGraph { + @DependencyGraph.Factory + fun interface Factory { + fun create( + @Provides uiApplication: UIApplication, + @Provides rootScopeProvider: RootScopeProvider, + ): IosAppGraph + } + } ``` === "Desktop" ```kotlin title="desktopMain" - @SingleIn(AppScope::class) - @MergeComponent(AppScope::class) - abstract class DesktopAppComponent( - @get:Provides val rootScopeProvider: RootScopeProvider - ) + @DependencyGraph(AppScope::class) + interface DesktopAppGraph { + @DependencyGraph.Factory + fun interface Factory { + fun create(@Provides rootScopeProvider: RootScopeProvider): DesktopAppGraph + } + } ``` === "WasmJs" ```kotlin title="wasmJsMain" - @MergeComponent(AppScope::class) - @SingleIn(AppScope::class) - abstract class WasmJsAppComponent( - @get:Provides val rootScopeProvider: RootScopeProvider - ) + @DependencyGraph(AppScope::class) + interface WasmJsAppGraph { + @DependencyGraph.Factory + fun interface Factory { + fun create(@Provides rootScopeProvider: RootScopeProvider): WasmJsAppGraph + } + } ``` - ### Platform implementations -`kotlin-inject-anvil` makes it simple to provide platform specific implementations for abstract APIs without needing -to use `expect / actual` declarations or any specific wiring. Since the final components live in the platform specific -source folders, all contributions for a platform are automatically picked up. Platform specific implementations can -use and inject types from the platform. +Metro makes it simple to provide platform specific implementations for abstract APIs without needing +to use `expect / actual` declarations or any specific wiring. Since the final object graphs live in the platform +specific source folders, all contributions for a platform are automatically picked up. Platform specific +implementations can use and inject types from the platform. ```kotlin title="commonMain" interface LocationProvider @@ -149,41 +163,41 @@ Other common code within `commonMain` can safely inject and use `LocationProvide ### Injecting dependencies It's recommended to rely on constructor injection as much as possible, because it removes boilerplate and makes -testing easier. But it some cases it's required to get a dependency from a component where constructor injection -is not possible, e.g. in a static context or types created by the platform. In this case a contributed component +testing easier. But it some cases it's required to get a dependency from an object graph where constructor injection +is not possible, e.g. in a static context or types created by the platform. In this case a contributed object graph interface with access to the `Scope` help: ```kotlin title="androidMain" class MainActivityViewModel(application: Application) : AndroidViewModel(application) { - private val component = (application as RootScopeProvider).rootScope.kotlinInjectComponent() - private val templateProvider = component.templateProviderFactory.createTemplateProvider() + private val graph = (application as RootScopeProvider).rootScope.metroDependencyGraph() + private val templateProvider = graph.templateProviderFactory.createTemplateProvider() @ContributesTo(AppScope::class) - interface Component { + interface Graph { val templateProviderFactory: TemplateProvider.Factory } } ``` This sample shows an Android `ViewModel` that doesn't use constructor injection. Instead, the `Scope` is retrieved -from the `Application` class and the `kotlin-inject-anvil` component is found through the `kotlinInjectComponent()` function. +from the `Application` class and the Metro object graph is found through the `metroDependencyGraph()` function. ??? example "Sample" The `ViewModel` example comes from the [sample app](https://github.com/amzn/app-platform/blob/main/sample/app/src/androidMain/kotlin/software/amazon/app/platform/sample/MainActivityViewModel.kt). - `ViewModels` can use constructor injection, but this requires more setup. This approach of using a component + `ViewModels` can use constructor injection, but this requires more setup. This approach of using a graph interface was simpler and faster. Another example where this approach is handy is in [`NavigationPresenterImpl`](https://github.com/amzn/app-platform/blob/main/sample/navigation/impl/src/commonMain/kotlin/software/amazon/app/platform/sample/navigation/NavigationPresenterImpl.kt). This class waits for the user scope to be available and then optionally retrieves the `Presenter` that is part - of the user component. Constructor injection cannot be used, because `NavigationPresenterImpl` is part of the app + of the user graph. Constructor injection cannot be used, because `NavigationPresenterImpl` is part of the app scope and cannot inject dependencies from the user scope, which is a child scope of app scope. This would violate dependency inversion rules. ```kotlin hl_lines="17" @ContributesTo(UserScope::class) - interface UserComponent { + interface UserGraph { val userPresenter: UserPagePresenter } @@ -196,9 +210,9 @@ from the `Application` class and the `kotlin-inject-anvil` component is found th return presenter.present(Unit) } - // A user is logged in. Use the user component to get an instance of UserPagePresenter, which is only + // A user is logged in. Use the user graph to get an instance of UserPagePresenter, which is only // part of the user scope. - val userPresenter = remember(scope) { scope.kotlinInjectComponent().userPresenter } + val userPresenter = remember(scope) { scope.metroDependencyGraph().userPresenter } return userPresenter.present(Unit) } ``` @@ -258,95 +272,169 @@ class SampleClass( It's recommended to inject `CoroutineDispatcher` through the constructor instead of using `Dispatcher.*`. This allows to easily swap them within unit tests to remove concurrency and improve stability. -## Metro +### `@ContributesScoped` + +!!! warning + + Metro uses `@ContributesScoped` for `Scoped` integrations. `kotlin-inject-anvil` achieves a + similar result by repurposing `@ContributesBinding` with a custom code generator. + +The [`Scoped`](scope.md#scoped) interface is used to notify implementations when a `Scope` gets created and destroyed. + +```kotlin +class AndroidLocationProvider : LocationProvider, Scoped { + + override fun onEnterScope(scope: Scope) { + ... + } + + override fun onExitScope() { + ... + } +} +``` +The implementation class `AndroidLocationProvider` needs to be bound to the super type `LocationProvider` and use +multi-bindings for the `Scoped` interface. This is a lot of boilerplate to write that be auto-generated using +`@ContributesScoped` instead. When using `@ContributesScoped`, all bindings are generated and `@ContributesBinding` +doesn't need to be added. A typical implementation looks like this: + +```kotlin hl_lines="3" +@Inject +@SingleIn(AppScope::class) +@ContributesScoped(AppScope::class) +class AndroidLocationProvider : LocationProvider, Scoped +``` + +See the documentation for [`Scoped`](scope.md#scoped) for more details. + +### Missing integrations + +Metro already supports almost all App Platform specific custom extensions that previously existed +for `kotlin-inject-anvil`, including `@ContributesRenderer` and `@ContributesRobot`. The remaining +gap is support for `@ContributesRealImpl` and `@ContributesMockImpl`, which still needs a Metro +equivalent. + +### Migrating to Metro from kotlin-inject-anvil + +Metro and `kotlin-inject-anvil` are conceptually very similar. Since Metro is the recommended +default, migrating existing `kotlin-inject-anvil` code is usually mostly mechanical. Errors will be +reported at compile time and not runtime. + +Steps could like this. [PR/173](https://github.com/amzn/app-platform/pull/173) highlights this migration for the +`:sample` application. + +* It's strongly recommended to use the latest Kotlin and Metro version. Metro is a compiler plugin and tied to the compiler to a certain degree. +* Enable Metro in the Gradle DSL: +```groovy +appPlatform { + enableMetro true +} +``` +* Change kotlin-inject specific imports to Metro: +``` +me.tatarka.inject.annotations.IntoSet -> dev.zacsweers.metro.IntoSet +me.tatarka.inject.annotations.Provides -> dev.zacsweers.metro.Provides +software.amazon.lastmile.kotlin.inject.anvil.AppScope -> dev.zacsweers.metro.AppScope +software.amazon.lastmile.kotlin.inject.anvil.ContributesTo -> dev.zacsweers.metro.ContributesTo +software.amazon.lastmile.kotlin.inject.anvil.ForScope -> dev.zacsweers.metro.ForScope +software.amazon.lastmile.kotlin.inject.anvil.SingleIn -> dev.zacsweers.metro.SingleIn +``` +* Update the final kotlin-inject components to Metro. The Metro docs explain the API very well. E.g. this component had to adopt a factory: +```kotlin +// Old: +@Component +@MergeComponent(AppScope::class) +@SingleIn(AppScope::class) +abstract class DesktopAppComponent(@get:Provides val rootScopeProvider: RootScopeProvider) : + DesktopAppComponentMerged + +// New: +@DependencyGraph(AppScope::class) +interface DesktopAppComponent { + @DependencyGraph.Factory + fun interface Factory { + fun create(@Provides rootScopeProvider: RootScopeProvider): DesktopAppComponent + } +} +``` +* Change usages of `addKotlinInjectComponent()` to `addMetroDependencyGraph()` and usages of `kotlinInjectComponent()` to `metroDependencyGraph()`. + +## kotlin-inject-anvil !!! note - Metro is an opt-in feature through the Gradle DSL. The default value is `false`. + This section documents the supported alternative path. Prefer the Metro section above for new + App Platform code. + + `kotlin-inject-anvil` is an opt-in feature through the Gradle DSL. The default value is `false`. ```groovy appPlatform { - enableMetro true + enableKotlinInject true } ``` -!!! bug - - There are several bugs and issues related to Metro and the integration is considered experimental until these - problems are resolved and Metro itself becomes stable. More details are listed in the [bugs section](di.md#bugs). - -### Dependency graph +### Component -Dependency graphs are added as a service to the `Scope` class and can be obtained using the `metroDependencyGraph()` -extension function: +Components are added as a service to the `Scope` class and can be obtained using the `kotlinInjectComponent()` extension +function: ```kotlin -scope.metroDependencyGraph() +scope.kotlinInjectComponent() ``` -In modularized projects, final graphs are defined in the `:app` modules, because the object graph has to -know about all features of the app. It is strongly recommended to create an object graph in each platform specific +In modularized projects, final components are defined in the `:app` modules, because the object graph has to +know about all features of the app. It is strongly recommended to create a component in each platform specific folder to provide platform specific types. === "Android" ```kotlin title="androidMain" - @DependencyGraph(AppScope::class) - interface AndroidAppGraph { - @DependencyGraph.Factory - fun interface Factory { - fun create( - @Provides application: Application, - @Provides rootScopeProvider: RootScopeProvider, - ): AndroidAppGraph - } - } + @SingleIn(AppScope::class) + @MergeComponent(AppScope::class) + abstract class AndroidAppComponent( + @get:Provides val application: Application, + @get:Provides val rootScopeProvider: RootScopeProvider, + ) ``` === "iOS" ```kotlin title="iosMain" - @DependencyGraph(AppScope::class) - interface IosAppGraph { - @DependencyGraph.Factory - fun interface Factory { - fun create( - @Provides uiApplication: UIApplication, - @Provides rootScopeProvider: RootScopeProvider, - ): IosAppGraph - } - } + @SingleIn(AppScope::class) + @MergeComponent(AppScope::class) + abstract class IosAppComponent( + @get:Provides val uiApplication: UIApplication, + @get:Provides val rootScopeProvider: RootScopeProvider, + ) ``` === "Desktop" ```kotlin title="desktopMain" - @DependencyGraph(AppScope::class) - interface DesktopAppGraph { - @DependencyGraph.Factory - fun interface Factory { - fun create(@Provides rootScopeProvider: RootScopeProvider): DesktopAppGraph - } - } + @SingleIn(AppScope::class) + @MergeComponent(AppScope::class) + abstract class DesktopAppComponent( + @get:Provides val rootScopeProvider: RootScopeProvider + ) ``` === "WasmJs" ```kotlin title="wasmJsMain" - @DependencyGraph(AppScope::class) - interface WasmJsAppGraph { - @DependencyGraph.Factory - fun interface Factory { - fun create(@Provides rootScopeProvider: RootScopeProvider): WasmJsAppGraph - } - } + @MergeComponent(AppScope::class) + @SingleIn(AppScope::class) + abstract class WasmJsAppComponent( + @get:Provides val rootScopeProvider: RootScopeProvider + ) ``` + ### Platform implementations -Metro makes it simple to provide platform specific implementations for abstract APIs without needing -to use `expect / actual` declarations or any specific wiring. Since the final object graphs live in the platform -specific source folders, all contributions for a platform are automatically picked up. Platform specific -implementations can use and inject types from the platform. +`kotlin-inject-anvil` makes it simple to provide platform specific implementations for abstract APIs without needing +to use `expect / actual` declarations or any specific wiring. Since the final components live in the platform specific +source folders, all contributions for a platform are automatically picked up. Platform specific implementations can +use and inject types from the platform. ```kotlin title="commonMain" interface LocationProvider @@ -401,41 +489,41 @@ Other common code within `commonMain` can safely inject and use `LocationProvide ### Injecting dependencies It's recommended to rely on constructor injection as much as possible, because it removes boilerplate and makes -testing easier. But it some cases it's required to get a dependency from an object graph where constructor injection -is not possible, e.g. in a static context or types created by the platform. In this case a contributed object graph +testing easier. But it some cases it's required to get a dependency from a component where constructor injection +is not possible, e.g. in a static context or types created by the platform. In this case a contributed component interface with access to the `Scope` help: ```kotlin title="androidMain" class MainActivityViewModel(application: Application) : AndroidViewModel(application) { - private val graph = (application as RootScopeProvider).rootScope.metroDependencyGraph() - private val templateProvider = graph.templateProviderFactory.createTemplateProvider() + private val component = (application as RootScopeProvider).rootScope.kotlinInjectComponent() + private val templateProvider = component.templateProviderFactory.createTemplateProvider() @ContributesTo(AppScope::class) - interface Graph { + interface Component { val templateProviderFactory: TemplateProvider.Factory } } ``` This sample shows an Android `ViewModel` that doesn't use constructor injection. Instead, the `Scope` is retrieved -from the `Application` class and the Metro object graph is found through the `metroDependencyGraph()` function. +from the `Application` class and the `kotlin-inject-anvil` component is found through the `kotlinInjectComponent()` function. ??? example "Sample" The `ViewModel` example comes from the [sample app](https://github.com/amzn/app-platform/blob/main/sample/app/src/androidMain/kotlin/software/amazon/app/platform/sample/MainActivityViewModel.kt). - `ViewModels` can use constructor injection, but this requires more setup. This approach of using a graph + `ViewModels` can use constructor injection, but this requires more setup. This approach of using a component interface was simpler and faster. Another example where this approach is handy is in [`NavigationPresenterImpl`](https://github.com/amzn/app-platform/blob/main/sample/navigation/impl/src/commonMain/kotlin/software/amazon/app/platform/sample/navigation/NavigationPresenterImpl.kt). This class waits for the user scope to be available and then optionally retrieves the `Presenter` that is part - of the user graph. Constructor injection cannot be used, because `NavigationPresenterImpl` is part of the app + of the user component. Constructor injection cannot be used, because `NavigationPresenterImpl` is part of the app scope and cannot inject dependencies from the user scope, which is a child scope of app scope. This would violate dependency inversion rules. ```kotlin hl_lines="17" @ContributesTo(UserScope::class) - interface UserGraph { + interface UserComponent { val userPresenter: UserPagePresenter } @@ -448,9 +536,9 @@ from the `Application` class and the Metro object graph is found through the `me return presenter.present(Unit) } - // A user is logged in. Use the user graph to get an instance of UserPagePresenter, which is only + // A user is logged in. Use the user component to get an instance of UserPagePresenter, which is only // part of the user scope. - val userPresenter = remember(scope) { scope.metroDependencyGraph().userPresenter } + val userPresenter = remember(scope) { scope.kotlinInjectComponent().userPresenter } return userPresenter.present(Unit) } ``` @@ -510,120 +598,11 @@ class SampleClass( It's recommended to inject `CoroutineDispatcher` through the constructor instead of using `Dispatcher.*`. This allows to easily swap them within unit tests to remove concurrency and improve stability. -### `@ContriubtesScoped` - -!!! warning - - This is different between `kotlin-inject-anvil` and Metro. In `kotlin-inject-anvil` we repurpose the - `@ContributesBinding` annotation to make it understand the semantics for the `Scoped` interface and generate custom - code using a custom code generator. Metro doesn't support this kind of integration and therefore we had to - introduce `@ContributesScoped` for a similar usage. - -The [`Scoped`](scope.md#scoped) interface is used to notify implementations when a `Scope` gets created and destroyed. - -```kotlin -class AndroidLocationProvider : LocationProvider, Scoped { - - override fun onEnterScope(scope: Scope) { - ... - } - - override fun onExitScope() { - ... - } -} -``` -The implementation class `AndroidLocationProvider` needs to be bound to the super type `LocationProvider` and use -multi-bindings for the `Scoped` interface. This is a lot of boilerplate to write that be auto-generated using -`@ContributesScoped` instead. When using `@ContributesScoped`, all bindings are generated and `@ContributesBinding` -doesn't need to be added. A typical implementation looks like this: - -```kotlin hl_lines="3" -@Inject -@SingleIn(AppScope::class) -@ContributesBinding(AppScope::class) -class AndroidLocationProvider : LocationProvider, Scoped -``` - -See the documentation for [`Scoped`](scope.md#scoped) for more details. - -### Bugs - -Metro is in an early stage and there are several bugs blocking a full roll out. - - -#### Missing integrations - -Almost all App Platform specific custom extensions for `kotlin-inject-anvil` were migrated to Metro, including -`@ContributesRenderer` and `@ContributesRobot`. However the integration for `@ContributesRealImpl` and -`@ContributesMockImpl` is missing and still needs to be ported. - -### Solved bugs in Kotlin 2.3.20 & Metro 0.10.0. Older versions won't receive the fix. - -#### No full KMP support - -Metro is ready to support KMP, but targets other than JVM/Android fail to merge types contributed with -`@ContributesTo` and `@ContributesBinding`. App Platform makes heavy use of them. This is called out in the -[Metro documentation](https://zacsweers.github.io/metro/0.7.7/multiplatform/). There is a chance -this will be fixed in Kotlin 2.3. - -> There is one issue in the repo right now where the compiler appears to have a bug with generated FIR declarations where it doesn’t deserialize them correctly on non-JVM targets. Waiting for feedback from JB. - -The ticket [metro/460](https://github.com/ZacSweers/metro/issues/460) and related in Kotlin [KT-58886](https://youtrack.jetbrains.com/issue/KT-58886) are closed. - -#### Incremental compilation issues - -While testing Metro in App Platform, we encountered incremental compilation issues that impacted merging components -and generated wrong code. This ticket is [metro/997](https://github.com/ZacSweers/metro/issues/997) (closed). - -Other IC issues are reported under [KT-75865](https://youtrack.jetbrains.com/issue/KT-75865) (closed). - - -### Migration - -Metro and `kotlin-inject-anvil` are conceptionally very similar. A migration is mostly mechanical. Errors will be -reported at compile time and not runtime. - -Steps could like this. [PR/129](https://github.com/amzn/app-platform/pull/129) highlights this migration for the -`:sample` application. - -* It's strongly recommended to use the latest Kotlin and Metro version. Metro is a compiler plugin and tied to the compiler to a certain degree. -* Enable Metro in the Gradle DSL: -```groovy -appPlatform { - enableMetro true -} -``` -* Change kotlin-inject specific imports to Metro: -``` -me.tatarka.inject.annotations.IntoSet -> dev.zacsweers.metro.IntoSet -me.tatarka.inject.annotations.Provides -> dev.zacsweers.metro.Provides -software.amazon.lastmile.kotlin.inject.anvil.AppScope -> dev.zacsweers.metro.AppScope -software.amazon.lastmile.kotlin.inject.anvil.ContributesTo -> dev.zacsweers.metro.ContributesTo -software.amazon.lastmile.kotlin.inject.anvil.ForScope -> dev.zacsweers.metro.ForScope -software.amazon.lastmile.kotlin.inject.anvil.SingleIn -> dev.zacsweers.metro.SingleIn -``` -* Update the final kotlin-inject components to Metro. The Metro docs explain the API very well. E.g. this component had to adopt a factory: -```kotlin -// Old: -@Component -@MergeComponent(AppScope::class) -@SingleIn(AppScope::class) -abstract class DesktopAppComponent(@get:Provides val rootScopeProvider: RootScopeProvider) : - DesktopAppComponentMerged - -// New: -@DependencyGraph(AppScope::class) -interface DesktopAppComponent { - @DependencyGraph.Factory - fun interface Factory { - fun create(@Provides rootScopeProvider: RootScopeProvider): DesktopAppComponent - } -} -``` -* Change usages of `addKotlinInjectComponent()` to `addMetroDependencyGraph()` and usages of `kotlinInjectComponent()` to `metroDependencyGraph()`. -## `kotlin-inject-anvil` or Metro +## Metro vs `kotlin-inject-anvil` -Given the [issues highlighted](di.md#bugs) with Metro, it is strongly advised to use `kotlin-inject-anvil` for -production and Metro only for experiments. +Metro supports all features of `kotlin-inject-anvil` and `kotlin-inject`, produces more efficient code, provides +better error messages and compiles much faster. Metro is the recommended default for new projects, +while `kotlin-inject-anvil` remains the supported alternative when you need compatibility with +existing code. We strongly recommend using Metro for new projects and migrating existing projects +soon. diff --git a/docs/faq.md b/docs/faq.md index 8298e384..98a4b8d5 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -9,22 +9,25 @@ for an incremental adoption. Apps can leverage the concepts and the framework wi once. For example, instead of going all in on the unidirectional dataflow, Android apps can start adopting `Presenters` and -`Renderers` on an Activity by Activity or Fragment by Fragment basis. Our Android app initially used -[Dagger 2](https://dagger.dev/) and [Anvil](https://github.com/square/anvil) as dependency injection framework and -made it interop with `kotlin-inject-anvil` before switching fully. +`Renderers` on an Activity by Activity or Fragment by Fragment basis. Today we recommend starting +new App Platform code with Metro. Earlier, our Android app initially used +[Dagger 2](https://dagger.dev/) and [Anvil](https://github.com/square/anvil) as dependency +injection framework and later made it interop with `kotlin-inject-anvil` before switching fully. #### Can I use [Dagger 2](https://dagger.dev/) or any other DI framework? -It depends, but likely yes. We've chosen [kotlin-inject-anvil](https://github.com/amzn/kotlin-inject-anvil) because -it supports Kotlin Multiplatform and verifies the dependency graph at compile time. +It depends, but likely yes. App Platform recommends [Metro](di.md) as the default DI framework because +it supports Kotlin Multiplatform, verifies the dependency graph at compile time, and is the direction +the framework docs and examples assume. -App Platform provides support for [Metro](di.md) out of the box, but there are still rough edges around the KMP -support. Long term we may consider moving App Platform to Metro alone. +[kotlin-inject-anvil](https://github.com/amzn/kotlin-inject-anvil) remains supported as the +alternative, especially for existing codebases or when you need compatibility with older App Platform +examples. -Dagger 2 is more challenging, because it only supports Android and JVM application. That said, App Platform started on -Android we used to use Dagger 2. We bridged the Dagger 2 components with the `kotlin-inject-anvil` components for -interop and this served us well for a long time until we fully migrated to `kotlin-inject-anvil`. +Dagger 2 is more challenging, because it only supports Android and JVM application. Metro is the +recommended default today, though App Platform started on Android with Dagger 2 and we first +bridged those Dagger 2 components with `kotlin-inject-anvil` for interop. #### How does App Platform compare to [Circuit](https://slackhq.github.io/circuit/)? diff --git a/docs/index.md b/docs/index.md index d32061e6..dcfdaf7c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -51,13 +51,15 @@ usage of the module structure are implemented in the Gradle plugin. ### Dependency Injection -App Platform by default provides support for [kotlin-inject-anvil](di.md#kotlin-inject-anvil) and -[Metro](di.md#metro) as dependency injection solution. But these frameworks aren't enforced and you can -bring your own (1). +App Platform provides first-class support for [Metro](di.md#metro) and +[kotlin-inject-anvil](di.md#kotlin-inject-anvil) as dependency injection solutions. Metro is the +recommended default, but these frameworks aren't enforced and you can bring your own (1). { .annotate } -1. In the very first versions of App Platform, we at Amazon used [Dagger 2](https://dagger.dev/) and -[Anvil](https://github.com/square/anvil). Later we migrated to [kotlin-inject-anvil](https://github.com/amzn/kotlin-inject-anvil). +1. Today App Platform recommends [Metro](https://zacsweers.github.io/metro) for new work. + Historically, the very first versions at Amazon used [Dagger 2](https://dagger.dev/) and + [Anvil](https://github.com/square/anvil), and later migrated to + [kotlin-inject-anvil](https://github.com/amzn/kotlin-inject-anvil). ### Scopes @@ -91,7 +93,7 @@ and Desktop. The [Gradle plugin](setup.md) comes with a convenient DSL to take care of many necessary configurations, e.g. it sets up the *Compose* compiler for *Molecule* and *Compose Multiplatform*. It configures KSP and integrates -*kotlin-inject-anvil* or *Metro* for each platform. It sets the Android namespace and artifact ID when the module +*Metro* or *kotlin-inject-anvil* for each platform. It sets the Android namespace and artifact ID when the module structure is enabled. ## Getting Started diff --git a/docs/presenter.md b/docs/presenter.md index b3da6b76..c2c5e6a4 100644 --- a/docs/presenter.md +++ b/docs/presenter.md @@ -124,7 +124,7 @@ class AmazonLoginPresenter : LoginPresenter { !!! note - `MoleculePresenters` are never singletons. While they use `kotlin-inject-anvil` or Metro for constructor injection and + `MoleculePresenters` are never singletons. While they use Metro or `kotlin-inject-anvil` for constructor injection and automatically bind the concrete implementation to an API using `@ContributesBinding`, they don't use the `@SingleIn` annotation. `MoleculePresenters` manage their state in the `@Composable` function with the Compose runtime. Therefore, it's strongly discouraged to have any class properties. diff --git a/docs/renderer.md b/docs/renderer.md index bda3b2d1..1c082031 100644 --- a/docs/renderer.md +++ b/docs/renderer.md @@ -146,11 +146,12 @@ different implementations: ### `@ContributesRenderer` -All factory implementations rely on the dependency injection framework `kotlin-inject-anvil` or Metro to discover and -initialize renderers. When the factory is created, it builds the `RendererComponent`, which parent is the app component. -The `RendererComponent` lazily provides all renderers using the multibindings feature. To participate in the lookup, -renderers must tell `kotlin-inject-anvil` or Metro which models they can render. This is done through a component -interface, which automatically gets generated and added to the renderer scope by using the +All factory implementations rely on Metro or `kotlin-inject-anvil` to discover and initialize +renderers. When the factory is created, it builds the generated renderer graph or component, whose +parent is the app graph or component. That generated type lazily provides all renderers using the +multibindings feature. To participate in the lookup, renderers must tell Metro or +`kotlin-inject-anvil` which models they can render. This is done through a generated graph or +component interface, which is automatically added to the renderer scope by using the [`@ContributesRenderer` annotation](https://github.com/amzn/app-platform/blob/main/kotlin-inject-extensions/contribute/public/src/commonMain/kotlin/software/amazon/app/platform/inject/ContributesRenderer.kt). Which `Model` type is used for the binding is determined based on the super type. In the following example @@ -163,47 +164,47 @@ class LoginRenderer : ComposeRenderer() ??? info "Generated code" - === "kotlin-inject-anvil" + === "Metro" The `@ContributesRenderer` annotation generates following code. ```kotlin @ContributesTo(RendererScope::class) - interface LoginRendererComponent { + interface LoginRendererGraph { @Provides public fun provideSoftwareAmazonAppPlatformSampleLoginLoginRenderer(): LoginRenderer = LoginRenderer() @Provides @IntoMap - public fun provideSoftwareAmazonAppPlatformSampleLoginLoginRendererLoginPresenterModel(renderer: () -> LoginRenderer): Pair, () -> Renderer<*>> = LoginPresenter.Model::class to renderer + @RendererKey(LoginPresenter.Model::class) + public fun provideSoftwareAmazonAppPlatformSampleLoginLoginRendererLoginPresenterModel(renderer: Provider): Renderer<*> = renderer() @Provides @IntoMap @ForScope(scope = RendererScope::class) - public fun provideSoftwareAmazonAppPlatformSampleLoginLoginRendererLoginPresenterModelKey(): Pair, KClass>> = LoginPresenter.Model::class to LoginRenderer::class + @RendererKey(LoginPresenter.Model::class) + public fun provideSoftwareAmazonAppPlatformSampleLoginLoginRendererLoginPresenterModelKey(): KClass> = LoginRenderer::class } ``` - - === "Metro" - + + === "kotlin-inject-anvil" + The `@ContributesRenderer` annotation generates following code. - + ```kotlin @ContributesTo(RendererScope::class) - interface LoginRendererGraph { + interface LoginRendererComponent { @Provides public fun provideSoftwareAmazonAppPlatformSampleLoginLoginRenderer(): LoginRenderer = LoginRenderer() - + @Provides @IntoMap - @RendererKey(LoginPresenter.Model::class) - public fun provideSoftwareAmazonAppPlatformSampleLoginLoginRendererLoginPresenterModel(renderer: Provider): Renderer<*> = renderer() - + public fun provideSoftwareAmazonAppPlatformSampleLoginLoginRendererLoginPresenterModel(renderer: () -> LoginRenderer): Pair, () -> Renderer<*>> = LoginPresenter.Model::class to renderer + @Provides @IntoMap @ForScope(scope = RendererScope::class) - @RendererKey(LoginPresenter.Model::class) - public fun provideSoftwareAmazonAppPlatformSampleLoginLoginRendererLoginPresenterModelKey(): KClass> = LoginRenderer::class + public fun provideSoftwareAmazonAppPlatformSampleLoginLoginRendererLoginPresenterModelKey(): Pair, KClass>> = LoginPresenter.Model::class to LoginRenderer::class } ``` diff --git a/docs/scope.md b/docs/scope.md index 66dda338..316c4dda 100644 --- a/docs/scope.md +++ b/docs/scope.md @@ -175,20 +175,21 @@ fun Scope.myService(): MyService { ``` The App Platform comes with a coroutine scope service and an integration for -[kotlin-inject-anvil](https://github.com/amzn/kotlin-inject-anvil) and [Metro](https://zacsweers.github.io/metro) -as dependency injection frameworks. +[Metro](https://zacsweers.github.io/metro) and +[kotlin-inject-anvil](https://github.com/amzn/kotlin-inject-anvil) as dependency injection +frameworks. Metro is the recommended default. ```kotlin val rootScope = Scope.buildRootScope { addCoroutineScopeScoped(coroutineScope) - addKotlinInjectComponent(kotlinInjectComponent) addMetroDependencyGraph(metroDependencyGraph) + addKotlinInjectComponent(kotlinInjectComponent) } // Obtain service. rootScope.coroutineScope() -rootScope.kotlinInjectComponent() rootScope.metroDependencyGraph() +rootScope.kotlinInjectComponent() ``` !!! warning @@ -211,20 +212,19 @@ It's strongly recommended to add a `CoroutineScope` to each `Scope`. App Platfor It is important to register this `CoroutineScope` in the created app `Scope` instance in order to cancel the `CoroutineScope` in case the `AppScope` ever gets destroyed. The same applies to any child scope. -=== "kotlin-inject-anvil" +=== "Metro" ```kotlin - @SingleIn(AppScope::class) - @MergeComponent(AppScope::class) - interface AppComponent { + @DependencyGraph(AppScope::class) + interface AppGraph { /** The coroutine scope that runs as long as the app scope is alive. */ @ForScope(AppScope::class) val appScopeCoroutineScopeScoped: CoroutineScopeScoped // (1)! } - fun createAppScope(appComponent: AppComponent): Scope { + fun createAppScope(appGraph: AppGraph): Scope { return Scope.buildRootScope { - addKotlinInjectComponent(appComponent) - addCoroutineScopeScoped(appComponent.appScopeCoroutineScopeScoped) + addMetroDependencyGraph(appGraph) + addCoroutineScopeScoped(appGraph.appScopeCoroutineScopeScoped) } } ``` @@ -232,19 +232,20 @@ It is important to register this `CoroutineScope` in the created app `Scope` ins 1. `CoroutineScopeScoped` wraps a `CoroutineScope` in a `Scoped` instance. In `onExitScope()` of this instance the `CoroutineScope` will be canceled. -=== "Metro" +=== "kotlin-inject-anvil" ```kotlin - @DependencyGraph(AppScope::class) - interface AppGraph { + @SingleIn(AppScope::class) + @MergeComponent(AppScope::class) + interface AppComponent { /** The coroutine scope that runs as long as the app scope is alive. */ @ForScope(AppScope::class) val appScopeCoroutineScopeScoped: CoroutineScopeScoped // (1)! } - fun createAppScope(appGraph: AppGraph): Scope { + fun createAppScope(appComponent: AppComponent): Scope { return Scope.buildRootScope { - addMetroDependencyGraph(appGraph) - addCoroutineScopeScoped(appGraph.appScopeCoroutineScopeScoped) + addKotlinInjectComponent(appComponent) + addCoroutineScopeScoped(appComponent.appScopeCoroutineScopeScoped) } } ``` @@ -269,7 +270,7 @@ override fun onEnterScope(scope: Scope) { 1. `scope.launch` is a convenience function for `scope.coroutineScope().launch`. -Since the `CoroutineScope` is part of the `kotlin-inject-anvil` or Metro object graph, the `CoroutineScope` can be +Since the `CoroutineScope` is part of the Metro or `kotlin-inject-anvil` object graph, the `CoroutineScope` can be injected in the constructor as well: ```kotlin @@ -348,14 +349,14 @@ class AndroidLocationProvider( not `LocationProvider`. Being lifecycle aware is an implementation detail. How the `Scoped` object is instantiated depends on the dependency injection framework and which scope to use. -With `kotlin-inject-anvil` and Metro for the app scope it would be: +With Metro, or alternatively `kotlin-inject-anvil`, for the app scope it would be: -=== "kotlin-inject-anvil" +=== "Metro" ```kotlin @Inject // (1)! @SingleIn(AppScope::class) // (2)! - @ContributesBinding(AppScope::class) //(3)! + @ContributesScoped(AppScope::class) //(3)! class AndroidLocationProvider( ... ) : LocationProvider, Scoped { @@ -367,28 +368,26 @@ With `kotlin-inject-anvil` and Metro for the app scope it would be: 2. This annotation ensures that there is only ever a single instance of `AndroidLocationProvider` in the `AppScope`. 3. This annotation ensures that when somebody injects `LocationProvider`, then they get the singleton instance of `AndroidLocationProvider`. - ??? note "`@ContributesBinding` will generate and contribute bindings" - - The `@ContributesBinding` annotation will generate a component interface with bindings for `LocationProvider` + ??? note "`@ContributesScoped` will generate and contribute bindings" + + The `@ContributesScoped` annotation will generate a graph interface with bindings for `LocationProvider` and `Scoped`. The generated interface will be added automatically to the `AppScope`. No further manual step is needed. - + ```kotlin - @Provides - public fun provideAndroidLocationProvider(androidLocationProvider: AndroidLocationProvider): LocationProvider = androidLocationProvider - - @Provides - @IntoSet - @ForScope(AppScope::class) - fun provideAndroidLocationProviderScoped(androidLocationProvider: AndroidLocationProvider): Scoped = androidLocationProvider + @Binds + val AndroidLocationProvider.binds: LocationProvider + + @Binds @IntoSet @ForScope(AppScope::class) + val AndroidLocationProvider.bindsScoped: Scoped ``` -=== "Metro" +=== "kotlin-inject-anvil" ```kotlin @Inject // (1)! @SingleIn(AppScope::class) // (2)! - @ContributesScoped(AppScope::class) //(3)! + @ContributesBinding(AppScope::class) //(3)! class AndroidLocationProvider( ... ) : LocationProvider, Scoped { @@ -400,18 +399,20 @@ With `kotlin-inject-anvil` and Metro for the app scope it would be: 2. This annotation ensures that there is only ever a single instance of `AndroidLocationProvider` in the `AppScope`. 3. This annotation ensures that when somebody injects `LocationProvider`, then they get the singleton instance of `AndroidLocationProvider`. - ??? note "`@ContributesScoped` will generate and contribute bindings" - - The `@ContributesScoped` annotation will generate a graph interface with bindings for `LocationProvider` + ??? note "`@ContributesBinding` will generate and contribute bindings" + + The `@ContributesBinding` annotation will generate a component interface with bindings for `LocationProvider` and `Scoped`. The generated interface will be added automatically to the `AppScope`. No further manual step is needed. - + ```kotlin - @Binds - val AndroidLocationProvider.binds: LocationProvider - - @Binds @IntoSet @ForScope(AppScope::class) - val AndroidLocationProvider.bindsScoped: Scoped + @Provides + public fun provideAndroidLocationProvider(androidLocationProvider: AndroidLocationProvider): LocationProvider = androidLocationProvider + + @Provides + @IntoSet + @ForScope(AppScope::class) + fun provideAndroidLocationProviderScoped(androidLocationProvider: AndroidLocationProvider): Scoped = androidLocationProvider ``` @@ -424,7 +425,7 @@ With `kotlin-inject-anvil` and Metro for the app scope it would be: ```kotlin @Inject @SingleIn(UserScope::class) - @ContributesBinding(UserScope::class) // Use @ContributesScoped with Metro. + @ContributesScoped(UserScope::class) // Use @ContributesBinding with kotlin-inject-anvil. class SessionTimeout(...) : Scoped { override fun onEnterScope(scope: Scope) { @@ -445,63 +446,62 @@ With `kotlin-inject-anvil` and Metro for the app scope it would be: ### Registering `Scoped` -The dependency injection frameworks like `kotlin-inject-anvil` and Metro are only responsible for creating `Scoped` +The dependency injection frameworks like Metro and `kotlin-inject-anvil` are only responsible for creating `Scoped` instances, but don't automatically register them in the `Scope`. This has to be done whenever the `Scope` is created: -=== "kotlin-inject-anvil" +=== "Metro" - ```kotlin hl_lines="5 16" - @SingleIn(AppScope::class) - @MergeComponent(AppScope::class) - interface AppComponent { + ```kotlin hl_lines="4 16" + @DependencyGraph(AppScope::class) + interface AppGraph { /** All [Scoped] instances part of the app scope. */ @ForScope(AppScope::class) val appScopedInstances: Set } - fun createAppScope(appComponent: AppComponent): Scope { + fun createAppScope(appGraph: AppGraph): Scope { val rootScope = Scope.buildRootScope { - addKotlinInjectComponent(appComponent) + addMetroDependencyGraph(appGraph) - addCoroutineScopeScoped(appComponent.appScopeCoroutineScopeScoped) + addCoroutineScopeScoped(appGraph.appScopeCoroutineScopeScoped) } - rootScope.register(appComponent.appScopedInstances) + rootScope.register(appGraph.appScopedInstances) return rootScope } ``` - By calling `appComponent.appScopedInstances` the DI framework instantiates all `Scoped` instances part of the + By calling `appGraph.appScopedInstances` the DI framework instantiates all `Scoped` instances part of the `AppScope`. The `rootScope.register(...)` call will register all of the `Scoped` instances and invoke `onEnterScope(scope)`. When calling `rootScope.destroy()` later at some point, then `onExitScope()` will be called for all `Scoped` instances. +=== "kotlin-inject-anvil" -=== "Metro" - - ```kotlin hl_lines="4 16" - @DependencyGraph(AppScope::class) - interface AppGraph { + ```kotlin hl_lines="5 16" + @SingleIn(AppScope::class) + @MergeComponent(AppScope::class) + interface AppComponent { /** All [Scoped] instances part of the app scope. */ @ForScope(AppScope::class) val appScopedInstances: Set } - fun createAppScope(appGraph: AppGraph): Scope { + fun createAppScope(appComponent: AppComponent): Scope { val rootScope = Scope.buildRootScope { - addMetroDependencyGraph(appGraph) + addKotlinInjectComponent(appComponent) - addCoroutineScopeScoped(appGraph.appScopeCoroutineScopeScoped) + addCoroutineScopeScoped(appComponent.appScopeCoroutineScopeScoped) } - rootScope.register(appGraph.appScopedInstances) + rootScope.register(appComponent.appScopedInstances) return rootScope } ``` - By calling `appGraph.appScopedInstances` the DI framework instantiates all `Scoped` instances part of the + By calling `appComponent.appScopedInstances` the DI framework instantiates all `Scoped` instances part of the `AppScope`. The `rootScope.register(...)` call will register all of the `Scoped` instances and invoke `onEnterScope(scope)`. When calling `rootScope.destroy()` later at some point, then `onExitScope()` will be called for all `Scoped` instances. @@ -519,12 +519,12 @@ The convenience function `onExit` is handy when you want to create objects lazil not create a property in the class itself. This callback notifies you when the `Scope` is destroyed similar to `onExitScope()`. -=== "kotlin-inject-anvil" +=== "Metro" ```kotlin @Inject @SingleIn(AppScope::class) - @ContributesBinding(AppScope::class) + @ContributesScoped(AppScope::class) class MyClass(private val application: Application) : Scoped { override fun onEnterScope(scope: Scope) { @@ -540,12 +540,12 @@ not create a property in the class itself. This callback notifies you when the ` } ``` -=== "Metro" +=== "kotlin-inject-anvil" ```kotlin @Inject @SingleIn(AppScope::class) - @ContributesScoped(AppScope::class) + @ContributesBinding(AppScope::class) class MyClass(private val application: Application) : Scoped { override fun onEnterScope(scope: Scope) { diff --git a/docs/setup.md b/docs/setup.md index 5033f48d..1851e041 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -19,12 +19,12 @@ are explained in more detail in many of the following sections. // false by default. Helpful for final application modules that must consume concrete implementations and not only APIs. addImplModuleDependencies true - // false by default. Configures KSP and adds the kotlin-inject-anvil library as dependency. - enableKotlinInject true - - // false by default. Configures Metro and adds App Platform specific extensions as dependency. + // false by default. Recommended DI option. Configures Metro and adds App Platform specific extensions as dependency. enableMetro true + // false by default. Alternative DI option. Configures KSP and adds the kotlin-inject-anvil library as dependency. + enableKotlinInject true + // false by default. Configures Molecule and provides access to the MoleculePresenter API. enableMoleculePresenters true @@ -51,12 +51,12 @@ are explained in more detail in many of the following sections. // false by default. Helpful for final application modules that must consume concrete implementations and not only APIs. addImplModuleDependencies(true) - // false by default. Configures KSP and adds the kotlin-inject-anvil library as dependency. - enableKotlinInject(true) - - // false by default. Configures Metro and adds App Platform specific extensions as dependency. + // false by default. Recommended DI option. Configures Metro and adds App Platform specific extensions as dependency. enableMetro(true) + // false by default. Alternative DI option. Configures KSP and adds the kotlin-inject-anvil library as dependency. + enableKotlinInject(true) + // false by default. Configures Molecule and provides access to the MoleculePresenter API. enableMoleculePresenters(true) @@ -72,7 +72,8 @@ are explained in more detail in many of the following sections. !!! note All settings of App Platform are optional and opt-in, e.g. you can use Molecule Presenters without enabling - the opinionated module structure. Compose UI can be enabled without using `kotlin-inject-anvil` or `Metro`. + the opinionated module structure. Compose UI can be enabled without using `Metro` or + `kotlin-inject-anvil`. When you do want DI, Metro is the recommended default. ## Snapshot diff --git a/docs/testing.md b/docs/testing.md index 64a41e82..bb3309ea 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -16,14 +16,15 @@ are functioning and tests don’t need to be repeated. The sample application implements instrumented tests for two screens and navigates between the tests. The [tests for Desktop](https://github.com/amzn/app-platform/blob/main/sample/app/src/desktopTest/kotlin/software/amazon/app/platform/sample/LoginUiTest.kt) - highlight how templates are rendered and robots are used for verification. It also sets up a `kotlin-inject-anvil` - [`TestAppComponent`](https://github.com/amzn/app-platform/blob/main/sample/app/src/desktopTest/kotlin/software/amazon/app/platform/sample/TestDesktopAppComponent.kt), - which replaces the main `AppComponent`. + highlight how templates are rendered and robots are used for verification. They also set up a Metro + [`TestDesktopAppGraph`](https://github.com/amzn/app-platform/blob/main/sample/app/src/desktopTest/kotlin/software/amazon/app/platform/sample/TestDesktopAppGraph.kt), + which replaces the main desktop graph. The same UI test is [implemented for Android](https://github.com/amzn/app-platform/blob/main/sample/app/src/androidInstrumentedTest/kotlin/software/amazon/app/platform/sample/AndroidLoginUiTest.kt). The Android tests reuse the same robots for verification and set up a - [`TestAppComponent`](https://github.com/amzn/app-platform/blob/main/sample/app/src/androidInstrumentedTest/kotlin/software/amazon/app/platform/sample/TestAndroidAppComponent.kt) - in a similar way. + [`TestAndroidAppGraph`](https://github.com/amzn/app-platform/blob/main/sample/app/src/androidInstrumentedTest/kotlin/software/amazon/app/platform/sample/TestAndroidAppGraph.kt) + in a similar way. The sample now uses Metro throughout, while `kotlin-inject-anvil` remains + available as the alternative path. ## Fakes @@ -296,41 +297,41 @@ class ConnectionRobot : Robot { ``` `Robots` must be annotated with `@ContributesRobot` in order to find them during tests when using the `robot()` -or `composeRobot()` function. The annotation makes sure that the robots are added to the `kotlin-inject-anvil` -or Metro dependency graph. +or `composeRobot()` function. The annotation makes sure that the robots are added to the Metro +or `kotlin-inject-anvil` dependency graph. ??? info "Generated code" The `@ContributesRobot` annotation generates following code. - - === "kotlin-inject-anvil" + + === "Metro" ```kotlin @ContributesTo(AppScope::class) - public interface LoginRobotComponent { + public interface LoginRobotGraph { @Provides public fun provideLoginRobot(): LoginRobot = LoginRobot() @Provides @IntoMap + @RobotKey(LoginRobot::class) public fun provideLoginRobotIntoMap( - robot: () -> LoginRobot - ): Pair, () -> Robot> = LoginRobot::class to robot + robot: Provider + ): Robot = robot() } ``` - - === "Metro" - + + === "kotlin-inject-anvil" + ```kotlin @ContributesTo(AppScope::class) - public interface LoginRobotGraph { + public interface LoginRobotComponent { @Provides public fun provideLoginRobot(): LoginRobot = LoginRobot() - + @Provides @IntoMap - @RobotKey(LoginRobot::class) public fun provideLoginRobotIntoMap( - robot: Provider - ): Robot = robot() + robot: () -> LoginRobot + ): Pair, () -> Robot> = LoginRobot::class to robot } ```