Adoption Strategies¶
If adopting Metro into an existing codebase, you can use a few different strategies.
- First, add the Metro Gradle plugin and runtime deps. The plugin id is
dev.zacsweers.metro, runtime isdev.zacsweers.metro:runtime. The Gradle Plugin should add the runtime automatically, but it’s there just in case! - Apply the Gradle plugin to your relevant project(s).
Precursor steps¶
Compiler options you should enable in Dagger
Dagger has some compiler options you should enable and get working first to make it easier to move to Metro.
- useBindingGraphFix
- The issue it fixes is something that Metro catches as well.
- ignoreProvisionKeyWildcards
K2 Migration
If you are migrating from square/anvil, you likely are also going to have to migrate to Kotlin K2 as a part of this. If you want to split that effort up, you can consider migrating to anvil-ksp first. This would move fully over to KSP and K2 first, then you can resume here.
Option 1: Interop at the component/graph level¶
This option is good if you only want to use Metro for new code. Metro graphs can depend on Dagger components (as @Includes parameters) and vice versa. Here is an example project that does this.
This option is also good if you just want to do a simple, isolated introduction of Metro in one part of your codebase, such as a smaller modularized feature or library.
Option 2: Migrate existing usages + reuse your existing annotations¶
If you want the least amount of splash as possible, you can tell Metro to reuse your annotations from Dagger/Anvil. Here is an example for enabling that in Gradle.
- Remove the dagger-compiler/anvil plugin (but keep their runtime deps).
- Enable interop with the Metro gradle plugin
metro {
interop {
includeDagger()
includeAnvil() // If using Anvil
}
}
Most things will Just Work™, but you will still possibly need to do some manual migrations.
- If you use
KClassandClassinterchangeably in your graph, Metro distinguishes between these by default. You can temporarily enable interop viaenableKClassToClassMapKeyInterop(with some runtime overhead caveats, see the docs), or you’ll need to move fully over toKClass. - If you use
@MergeComponentwith@Component, it’ll be easier if you just migrate those interfaces to@DependencyGraphsince they’re combined in there now. - Not necessary if coming from anvil-ksp.
- Migrate
@BindsInstanceto@Provides. Metro consolidated these to just one annotation. - Update references to generated
Dagger*Componentcalls to use metro’screateGraph/createGraphFactoryAPIs.
You can also remove any @JvmSuppressWildcard annotations, these are ignored in Metro.
Option 3: Full migration¶
- Remove the Dagger and anvil runtimes.
- Replace all Dagger/anvil annotations with Metro equivalents.
- Update references to generated
Dagger*Componentcalls to use metro’screateGraph/createGraphFactoryAPIs. - Migrate from javax/jakarta
Provideranddagger.LazyAPIs to the function-syntax() -> Tprovider form (or Metro’sProvider<T>) and the stdlib’sLazyAPIs. See MigratingProvider<T>to function syntax below.
Keep enableFunctionProviders enabled
kotlin-inject’s idiomatic way to request a provider is () -> T, so any non-trivial kotlin-inject codebase has these all over the graph. Metro’s enableFunctionProviders option (on by default) is what makes Metro resolve () -> T as a provider of T, which is exactly the semantics kotlin-inject users expect. Disabling it will break almost every migrated class, so leave it at its default during and after the migration.
Precursor steps¶
- Remove the kotlin-inject(-anvil) dependencies (but keep their runtime deps if you use option 1 below!).
- Migrate to
@AssistedFactoryif you haven’t already.
Option 1: Interop at the component/graph level¶
This option is good if you only want to use Metro for new code. Metro graphs can depend on kotlin-inject components (as @Includes parameters) and vice versa. Here is an example project that does this.
This option is also good if you want to do a simple, isolated introduction of Metro in one part of your codebase, such as a smaller modularized feature or library.
Option 2: Migrate existing usages + reuse your existing annotations¶
If you want the least amount of splash as possible, you can tell Metro to reuse your annotations from kotlin-inject/kotlin-inject-anvil. Here is an example for enabling that in Gradle.
- Remove the kotlin-inject and kotlin-inject-anvil KSP processors (but keep their runtime deps).
- Enable interop with the Metro Gradle plugin
metro {
interop {
includeKotlinInject()
includeAnvil() // If using kotlin-inject-anvil
}
}
You will still possibly need to do some manual migrations, namely providers.
- Any map multibindings need to migrate to use map keys.
- Any higher order function injection is directly compatible with Metro’s function-syntax providers (
() -> T), which are enabled by default. See MigratingProvider<T>to function syntax below for details. - Any higher order assisted function injection will need to switch to using
@AssistedFactory-annotated factories. - If you use
@MergeComponent+@Component, it’ll be easier if you just migrate those interfaces to@DependencyGraphsince they’re combined in there now. - If you use
@Componentparameters for graph extensions, you’ll have to switch to Graph extensions. This will primarily entail annotating the parameter with@Nestedand marking the parent graph as extendable. - Update calls to generated
SomeComponent::class.create(...)functions to use metro’screateGraph/createGraphFactoryAPIs.
Option 3: Full migration¶
- Any map multibindings need to migrate to use map keys.
- Any higher order function injection is directly compatible with Metro’s function-syntax providers (
() -> T), which are enabled by default. - Any higher order assisted function injection will need to switch to using
@AssistedFactory-annotated factories. - Remove the kotlin-inject and kotlin-inject-anvil runtimes.
- Replace all kotlin-inject/kotlin-inject-anvil annotations with Metro equivalents.
- If you use
@Componentparameters for graph extensions, you’ll have to switch to Graph extensions. This will primarily entail annotating the parameter with@Nestedand marking the parent graph as extendable. - Update calls to generated
SomeComponent::class.create(...)functions to use metro’screateGraph/createGraphFactoryAPIs.
Migrating Provider<T> to function syntax¶
Metro supports two equivalent provider forms:
- Function syntax (preferred, default):
() -> T - Desugared:
Provider<T>
The function-syntax form is enabled by default (enableFunctionProviders = true) and is the recommended way to declare provider dependencies going forward. The desugared Provider<T> form still works and remains useful for some interop scenarios, but Metro will surface a compiler diagnostic when it encounters it so you can incrementally migrate.
Controlling the diagnostic severity¶
Use the desugaredProviderSeverity compiler option to control how Metro reports Provider<T> usages:
metro {
// `WARN` by default. Set to `NONE` while migrating a large codebase, or
// `ERROR` to enforce function syntax for new code.
desugaredProviderSeverity.set(DiagnosticSeverity.NONE)
}
Values:
NONE— no diagnostic (also forced whenenableFunctionProvidersis disabled).WARN(default) — emit a compiler warning.ERROR— fail the compilation.
If a specific declaration intentionally uses Provider<T> (e.g., to interop with an external API), suppress the warning locally:
@Suppress("DESUGARED_PROVIDER_WARNING")
val metroProvider: Provider<Foo>
Automated migration¶
For large codebases, a helper script is available at scripts/migrate-metro-provider.sh that mechanically rewrites Provider<T> → () -> T across all .kt files in a directory:
scripts/migrate-metro-provider.sh path/to/your/code
What it does:
- Only touches files that import
dev.zacsweers.metro.Provider(explicit or via adev.zacsweers.metro.*wildcard). - Skips files that also import a non-Metro
Provider(javax.inject,jakarta.inject,com.google.inject,dagger.internal) to avoid ambiguity. - Handles nested generics (
Provider<Lazy<Foo>>,Map<K, Provider<V>>, etc.) via a recursive regex that loops until stable. - Preserves
Provider<*>(since() -> *is not valid Kotlin) andProvider { ... }factory calls. - Drops the now-unused
dev.zacsweers.metro.Providerimport when noProvideridentifier remains.
Review the diff afterwards — the script is deliberately conservative about what it rewrites, but manual spot-checking is always recommended.
If you previously used function types as binding keys¶
With enableFunctionProviders enabled (the default), any () -> T (i.e., Function0<T>) on the graph is interpreted as a provider of T. If your existing code uses a bare function type as the binding itself — e.g., a navigation callback or a side-effecting action injected as () -> Unit — Metro will now resolve it as “provide me a Unit” instead of “provide me this particular function”.
The fix is to give that function a concrete type so its binding key is distinct from the generic function shape. A fun interface extending the original function type is the lightest-weight option and keeps SAM conversion at call sites:
// Before: ambiguous under function-syntax providers
@Inject class AppShell(val onBackPressed: () -> Unit)
@Provides fun provideOnBackPressed(): () -> Unit = { /* ... */ }
// After: the binding is keyed by OnBackPressed, not by the function shape
fun interface OnBackPressed : () -> Unit
@Inject class AppShell(val onBackPressed: OnBackPressed)
@Provides fun provideOnBackPressed(): OnBackPressed = OnBackPressed { /* ... */ }
Qualifiers (@Named, custom @Qualifiers) do not disambiguate here — the provider semantics apply to () -> T regardless of qualifier — so a named type is required.
If you prefer to keep raw function types as bindings (and give up the function-syntax provider form for the whole project), set enableFunctionProviders = false in the Gradle metro { } block. That’s the escape hatch, but be aware it’s a project-wide decision and the desugaredProviderSeverity diagnostic will be forced to NONE since there is no alternative form to migrate to.