Skip to content

Interop

Annotations

Metro supports user-defined annotations for common annotations. This means that a user doesn’t necessarily have to use Metro’s annotations if they’re introducing it to an existing codebase. Support varies depending on the annotation’s use case.

Compile-only annotations are mostly supported. This includes the following:

  • @AssistedFactory
  • @AssistedInject
  • @Assisted
  • Includes Dagger’s @Assisted.value custom identifiers. While Metro’s native @Assisted uses parameter names for matching, Dagger’s explicit value identifiers are fully supported when using Dagger’s annotations.
  • @BindsInstance
  • @Binds
  • @ContributesBinding
  • @ContributesTo
  • @DependencyGraph.Factory
  • @DependencyGraph
  • @ElementsIntoSet
  • @Inject
  • @IntoMap
  • @IntoSet
  • @MapKey
  • @Module
  • @Multibinds
  • @Provides
  • @Qualifier
  • @Scope

These are configurable via Metro’s Gradle extension.

metro {
  interop {
    assisted.add("dagger/assisted/Assisted")
  }
}

For Dagger and KI specifically, there are convenience helper functions.

metro {
  interop {
    // Dagger
    includeDagger()
    includeAnvilForDagger()

    // Hilt
    includeHilt()

    // kotlin-inject
    includeKotlinInject()
    includeAnvilForKotlinInject()

    // Guice
    includeGuice()
  }
}

Warning

Dagger’s special-case @Reusable and @LazyClassKey annotations are not supported in Metro.

@DependencyGraph is replaceable but your mileage may vary if you use Anvil or modules, since Metro’s annotation unifies Anvil’s @MergeComponent functionality and doesn’t support modules.

Anvil’s @ContributesMultibinding is supported as @ContributesInto* in Metro and interpreted accordingly for Set/Map scenarios.

kotlin-inject-anvil’s @ContributesBinding(multibinding = true) is supported and automatically routed through as a @ContributesIntoSet contribution.

binding in Metro uses a more flexible mechanism to support generics, but interop with Anvil’s boundType: KClass<*> property is supported.

Components

Metro graphs can interop with components generated by Dagger and Kotlin-Inject. These work exclusively through their public accessors and can be depended on like any other graph dependency.

@DependencyGraph
interface MetroGraph {
  val message: String

  @DependencyGraph.Factory
  fun interface Factory {
    fun create(
      @Includes daggerComponent: DaggerComponent
    ): MetroGraph
  }
}

@dagger.Component
interface DaggerComponent {
  val message: String

  @dagger.Component.Factory
  fun interface Factory {
    fun create(@Provides message: String): DaggerComponent
  }
}

Conversely, kotlin-inject and Dagger components can also depend on Metro graphs.

@DependencyGraph
interface MessageGraph {
  val message: String

  // ...
}

// Dagger
@Component(dependencies = [MessageGraph::class])
interface DaggerComponent {
  val message: String

  @Component.Factory
  fun interface Factory {
    fun create(messageGraph: MessageGraph): DaggerComponent
  }
}

// kotlin-inject
@Component
abstract class KotlinInjectComponent(
  @Component val messageGraph: MessageGraph
) {
  abstract val message: String
}

Runtime

Dagger

Enabling dagger interop also enables more advanced runtime interop with Dagger/Javax/Jakarta’s Provider/Lazy types.

metro {
  interop {
    includeDagger()
  }
}

This specifically enables three features.

  1. Interop with Dagger/Javax/Jakarta’s Provider and Lazy runtime intrinsics.
  2. Interop with generated Dagger factories for constructor-injected classes, assisted-injected classes, member-injected classes, and Dagger modules. This means that Metro can natively reuse an upstream class or module that was processed with the dagger compiler (or Anvil, if using its factory generation) and has a generated factory/injector class.
  3. Interop with Dagger’s @BindsOptionalOf annotation.

Note the companion Gradle plugin automatically adds an extra dev.zacsweers.metro:interop-dagger runtime dependency to support this interop. If you only want annotation interop, just replace the annotations only.

Class/KClass map key interop

A special opt-in form of interop exists for java.lang.Class and kotlin.reflect.KClass on JVM/android compilations. While these types are not intrinsics of each other in regular code, they are in annotations and are often used in Map multibindings. Metro can support these if you enable the enableKClassToClassMapKeyInterop option. When enabled, java.lang.Class and kotlin.reflect.KClass are treated as interchangeable in map key types, matching Kotlin’s own annotation compilation behavior. This only applies to map keys because these are the only scenario where annotation arguments are materialized into non-annotation code (i.e. @ClassKey(Foo::class) -> Map<Class<*>, V>).

This is disabled by default (even if other framework interops like includeDagger are enabled) because this is purely for annotations interop and potentially comes at some runtime overhead cost to interop since KClass types are still used under the hood and must be mapped in some cases. It’s recommended to migrate these to KClass and call .java where necessary if possible.

Guice

Enabling Guice interop enables annotation interop with the following Guice annotations:

  • @Inject (but not @Inject.optional)
  • @Provides
  • @Assisted
  • Includes Guice’s @Assisted.value custom identifiers. While Metro’s native @Assisted uses parameter names for matching, Guice’s explicit value identifiers are fully supported when using Guice’s annotations.
  • @AssistedInject
  • @BindingAnnotation
  • @ScopeAnnotation
  • @MapKey
  • @ProvidesIntoMap
  • @ProvidesIntoSet

Enabling this also enables runtime interop with:

  • Guice’s Provider type. This means that you can use Guice’s Provider type interchangeably with Metro’s Provider type.
  • Jakarta’s Provider type.
  • Guice modules
    • Only @Provides declarations are supported. configure implementations (including anything that would go in them) are not.
metro {
  interop {
    includeGuice()
  }
}

Note the companion Gradle plugin automatically adds an extra dev.zacsweers.metro:interop-guice runtime dependency to support this interop.

Why not javax.inject?

Guice dropped support for javax.inject in 7.0.0.

Hilt

Hilt interop lets a Metro @DependencyGraph merge Hilt modules and entry points during a migration. Enable it with:

metro {
  interop {
    includeHilt()
  }
}

Tip

includeHilt() implicitly chains includeDagger()

Metro treats Hilt’s component target as the graph scope. A @DependencyGraph(<scope>) will merge:

  • @InstallIn(<component>::class) @Module classes as binding containers. Their @Provides declarations enter the graph.
  • @InstallIn(<component>::class) @EntryPoint interfaces as graph supertypes. Their accessors are implemented by the graph.

This works for Hilt declarations compiled in the same module as the graph, and for declarations coming from upstream modules. Upstream modules are discovered either through Metro’s contribution hints, when they also apply includeHilt(), or through Hilt’s generated aggregation metadata when Hilt’s KSP or KAPT processor ran upstream.

Built-in component-to-scope mapping

Standard Hilt components map to their canonical scopes automatically:

Hilt component Scope
SingletonComponent @Singleton
ActivityRetainedComponent @ActivityRetainedScoped
ActivityComponent @ActivityScoped
ViewModelComponent @ViewModelScoped
FragmentComponent @FragmentScoped
ServiceComponent @ServiceScoped
ViewComponent @ViewScoped
ViewWithFragmentComponent @ViewScoped

For example, @DependencyGraph(Singleton::class) pulls @InstallIn(SingletonComponent::class) sites, and @DependencyGraph(ActivityScoped::class) pulls @InstallIn(ActivityComponent::class) sites.

Custom @DefineComponent

Custom @DefineComponent interfaces work without extra registration. Metro resolves the component’s Metro scope from the scope annotation declared on the component itself:

@Scope @Retention(AnnotationRetention.RUNTIME) annotation class FeatureScoped

@FeatureScoped
@DefineComponent(parent = SingletonComponent::class)
interface FeatureComponent

@Module
@InstallIn(FeatureComponent::class)
class FeatureModule {
  @FeatureScoped
  @Provides fun provideTag(): String = "feature"
}

@FeatureScoped
@DependencyGraph(FeatureScoped::class)
interface FeatureGraph {
  val tag: String
}

The @DependencyGraph(FeatureScoped::class) argument is the aggregation key. If contributed Hilt
bindings are also scoped with @FeatureScoped, put @FeatureScoped on the graph too so Metro’s
scope compatibility check sees the concrete scope annotation.

Limitations

  • @TestInstallIn and @CustomTestApplication are not supported.
  • @DefineComponent.parent is not used to derive Metro @GraphExtension parent/child relationships. Component hierarchy must be expressed through Metro’s own graph extension APIs.
  • No EntryPointAccessors-style runtime helper is shipped. Cast the graph to the entry-point interface directly, or write a small helper that does so for your codebase.
  • The Hilt-internal componentEntryPoints field on @AggregatedDeps is ignored.
  • A custom @DefineComponent must have a scope annotation on the component interface. If Metro cannot find one, matching @InstallIn sites are skipped without a diagnostic.
  • Hilt component generation from @HiltAndroidApp is not consumed. Metro merges contributions itself.

See samples/interop/customAnnotations-hilt for a complete two-module example.

Diagnostics

When interoping with annotations that are written in Kotlin and have parameters, it may be unsafe to rely on positional arguments. Metro’s own annotations often have the same indices, but not always! If you want to be super safe, you can enable the interopAnnotationsNamedArgSeverity to WARN or ERROR to report diagnostics for positional arguments in any custom annotations that Metro is configured to look at.

Why only Kotlin annotations?

This is because the Kotlin compiler doesn’t support positional arguments for annotations that are written in Java.