Aggregation (aka ‘Anvil’)¶
Metro supports Anvil-style aggregation in graphs via @ContributesTo
and @ContributesBinding
annotations. As aggregation is a first-class citizen of Metro’s API, there is no @MergeComponent
annotation like in Anvil. Instead, @DependencyGraph
defines which contribution scope it supports directly.
@DependencyGraph(scope = AppScope::class)
interface AppGraph
When a graph declares a scope
, all contributions to that scope are aggregated into the final graph implementation in code gen.
If a graph supports multiple scopes, use additionalScopes
.
@DependencyGraph(
AppScope::class,
additionalScopes = [LoggedOutScope::class]
)
interface AppGraph
Similar to kotlin-inject-anvil, @DependencyGraph
supports excluding contributions by class. This is useful for cases like tests, where you may want to contribute a test/fake implementation that supersedes the “real” graph.
@DependencyGraph(
scope = AppScope::class,
excludes = [RealNetworkProviders::class]
)
interface TestAppGraph
@ContributesTo(AppScope::class)
interface TestNetworkProviders {
@Provides fun provideHttpClient(): TestHttpClient
}
@ContributesTo¶
This annotation is used to contribute graph interfaces to the target scope to be merged in at graph-processing time to the final merged graph class as another supertype.
@ContributesTo(AppScope::class)
interface NetworkProviders {
@Provides fun provideHttpClient(): HttpClient
}
This annotation is repeatable and can be used to contribute to multiple scopes.
@ContributesTo(AppScope::class)
@ContributesTo(LoggedInScope::class)
interface NetworkProviders {
@Provides fun provideHttpClient(): HttpClient
}
@ContributesBinding¶
This annotation is used to contribute injected classes to a target scope as a given bound type.
The below example will contribute the CacheImpl
class as a Cache
type to AppScope
.
@ContributesBinding(AppScope::class)
@Inject
class CacheImpl(...) : Cache
For simple cases where there is a single typertype, that type is implicitly used as the bound type. If your bound type is qualified, for the implicit case you can put the qualifier on the class.
@Named("cache")
@ContributesBinding(AppScope::class)
@Inject
class CacheImpl(...) : Cache
For classes with multiple supertypes or advanced cases where you want to bind an ancestor type, you can explicitly define this via boundType
parameter.
@ContributesBinding(
scope = AppScope::class,
boundType = BoundType<Cache>()
)
@Inject
class CacheImpl(...) : Cache, AnotherType
Note that the bound type is defined as the type argument to @ContributesBinding
. This allows for the bound type to be generic and is validated in FIR.
Qualifier annotations can be specified on the BoundType
type parameter and will take precedence over any qualifiers on the class itself.
@ContributesBinding(
scope = AppScope::class,
boundType = BoundType<@Named("cache") Cache>()
)
@Inject
class CacheImpl(...) : Cache, AnotherType
This annotation is repeatable and can be used to contribute to multiple scopes.
@ContributesBinding(
scope = AppScope::class,
boundType = BoundType<Cache>()
)
@ContributesBinding(
scope = AppScope::class,
boundType = BoundType<AnotherType>()
)
@Inject
class CacheImpl(...) : Cache, AnotherType
@ContributesIntoSet/@ContributesIntoMap¶
To contribute into a multibinding, use the @ContributesIntoSet
or @ContributesIntoMap
annotations as needed.
@ContributesIntoSet(AppScope::class)
@Inject
class CacheImpl(...) : Cache
Same rules around qualifiers and boundType()
apply in this scenario
To contribute into a Map multibinding, the map key annotation must be specified on the class or BoundType
type argument.
// Will be contributed into a Map multibinding with @StringKey("Networking")
@ContributesIntoMap(AppScope::class)
@StringKey("Networking")
@Inject
class CacheImpl(...) : Cache
// Or if using BoundType
@ContributesIntoMap(
scope = AppScope::class,
boundType = BoundType<@StringKey("Networking") Cache>()
)
@Inject
class CacheImpl(...) : Cache
This annotation is also repeatable and can be used to contribute to multiple scopes, multiple bound types, and multiple map keys.
@GraphExtension
/@ContributesGraphExtension
¶
Not yet implemented. Please share design feedback in #165!
Implementation notes¶
This leans on Kotlin’s ability to put generic type parameters on annotations. That in turn allows for both generic bound types and to contribute map bindings to multiple map keys.
Because it’s a first-party feature, there’s no need for intermediary “merged” components like kotlin-inject-anvil and anvil-ksp do.
Generated contributing interfaces are generated to the metro.hints
package and located during graph supertype generation in FIR downstream. Any contributed bindings are implemented as @Binds
(± IntoSet/IntoMap/etc) annotated properties.