What is Metro?
main@ContributesTo), and kotlin-inject (Kotlin-first API with top-level function injection).repository·main·Indexed 23 days ago
https://github.com/zacsweers/metroA compile-time dependency injection framework for Kotlin Multiplatform that uses a Kotlin compiler plugin to provide fast, validated dependency graphs without KAPT or KSP. Includes a compiler compatibility layer to abstract evolving Kotlin compiler APIs and a comprehensive testing suite for diagnostics, IR/FIR dumps, and IDE integration.
@ContributesTo), and kotlin-inject (Kotlin-first API with top-level function injection).Metro is a dependency injection (DI) framework built as a Kotlin compiler plugin. It provides familiar DI semantics including:
Provider and Lazy types.@Provides declarations, injection of private member properties/functions, and the reuse of default value expressions for optional dependencies.@ContributesTo and @ContributesBinding (similar to Anvil).Metro is designed for Kotlin Multiplatform (KMP) projects:
runtime artifact): Supports all common JVM, JS, and native targets.2.3.20-Beta1 or later (though they will still function within the same compilation).When implementing runtime tracing in an Android application, follow these architectural requirements to ensure the AndroidX tracer is available before generated binding code runs:
AppGraph.Factory) accepts a @Provides tracer: Tracer input.MetroApp) should own the TraceDriver and TraceSink. These should be passed into the generated graph factory via driver.tracer.TraceDriver as your application, you must remove the default AndroidX profiler tracing initializer in your AndroidManifest.xml.Metro performs dependency graph validation at the per-graph level in the compiler IR backend. Errors are reported as structured diagnostics containing:
[Metro/MissingBinding]).help: (actionable fixes), note: (context), and docs: (reference links).Common error types include:
similar bindings: if a near-miss exists (e.g., different qualifier, nullability, or sub/supertype).() -> T or Lazy<T>.Metro graphs can depend on, and be depended upon by, components generated by Dagger and Kotlin-Inject. This is achieved through their public accessors.
Pass the Dagger component into a Metro @DependencyGraph.Factory:
@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
}
}Include the Metro graph as a dependency in your Dagger or Kotlin-Inject component:
@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
}Test behavior is modified using directives, which are specified as line comments at the top of the .kt test file.
Metro provides its own directives in MetroDirectives, and you can also use standard Kotlin compiler directives. Directives can be simple boolean flags or complex instructions (e.g., disabling specific diagnostics).
Common patterns include:
// RENDER_DIAGNOSTICS_FULL_TEXT// DIAGNOSTICS: -PROVIDES_OR_BINDS_SHOULD_BE_PRIVATE// RUN_PIPELINE_TILL: FIR2IRScoped suspend bindings (using @SingleIn or @DependencyGraph(scope = ...)) work like standard scoped bindings. The first successful result is cached and shared.
Cache Semantics:
Note: Scoped suspend bindings require dev.zacsweers.metro:runtime-coroutines to be available at compile time and runtime.
@DependencyGraph(scope = AppScope::class)
interface AppGraph {
suspend fun database(): Database
@Provides
@SingleIn(AppScope::class)
suspend fun provideDatabase(): Database = openDatabase()
}The SuspendDoubleCheck mechanism manages concurrent access to scoped values:
kotlinx-coroutines.Mutex. The first caller holds the mutex while invoking the delegate; subsequent callers suspend until the first attempt finishes.null) is cached. If the delegate fails or is cancelled, the cache remains empty so future callers can retry.suspendLazy(SYNCHRONIZED): Uses SuspendDoubleCheck (mutex-based).PUBLICATION: Runs several initializers at once and publishes the first result. Uses compare-and-set on JVM/Native; checks for overlap at suspension points on JS/Wasm.NONE: Caches the result only if calls do not overlap; does not coordinate concurrent callers.A @BindingContainer is a class, object, or interface used to group @Provides or @Binds declarations. They are not complete dependency graphs themselves, but rather reusable, composable units intended to be included in a complete @DependencyGraph.
Unlike full graphs, the public accessors of a @BindingContainer are not read; only the @Binds and @Provides declarations within them are processed by Metro.
@BindingContainer
class NetworkBindings(private val baseUrl: String) {
@Provides fun provideHttpClient(): HttpClient = HttpClient(baseUrl)
}EntryPointAccessors. To access dependencies from a Hilt entry point within your Metro graph, you must manually cast the graph to the specific entry point interface.Metro enforces strict rules regarding scope matching to ensure graph integrity. You will encounter errors in the following scenarios:
@DependencyGraph cannot access a class or provider that is marked with a scope.AppGraph scoped to AppScope cannot access a class scoped to UserScope.Multiple Scopes: Like Dagger, a single graph can support multiple scopes. You can annotate a graph with multiple scope annotations to allow it to satisfy dependencies from different scope levels.
@Scope annotation class Singleton
@Singleton
@SingleIn(AppScope::class)
@DependencyGraph
interface AppGraph {
// This is ok
val exampleClass: ExampleClass
}
@SingleIn(AppScope::class)
@Inject
class ExampleClass