This document describes the custom annotations, what each one generates, and example usages. These use cases are implementation-agnostic -- they can be fulfilled by KSP processors, a compiler plugin feature, or any other code generation mechanism.
Contributes a FeatureFlag object into Set<FeatureFlag> via @IntoSet. Always scoped
to AppScope since all feature flags live in the app scope.
The description, removeBy, and fakeModeValues parameters are metadata only -- used by
linters and tooling, not by the generated binding.
annotation class ContributesFeatureFlag(
val description: String,
val removeBy: Date,
val fakeModeValues: Array<FlagValue> = [],
)object classes implementing FeatureFlag.
@ContributesFeatureFlag(
description = "Enables new checkout flow",
removeBy = Date(Month.June, 1, 2026),
)
object NewCheckoutFlag : FeatureFlag { ... }@ContributesTo(AppScope::class)
@BindingContainer
object NewCheckoutFlagBindingContainer {
@Provides @IntoSet
fun contributeFlag(): FeatureFlag = NewCheckoutFlag
}Identical to @ContributesFeatureFlag but for permanent flags that should not be removed.
Separate annotation for lint/policy enforcement.
annotation class ContributesDynamicConfigurationFlag(
val description: String,
val fakeModeValues: Array<FlagValue> = [],
)object classes implementing FeatureFlag.
@ContributesDynamicConfigurationFlag(
description = "Controls max retry count for sync operations",
)
object SyncRetryConfig : FeatureFlag { ... }@ContributesTo(AppScope::class)
@BindingContainer
object SyncRetryConfigBindingContainer {
@Provides @IntoSet
fun contributeFlag(): FeatureFlag = SyncRetryConfig
}Generates a contributed interface with an accessor method that exposes the robot
class on the dependency graph. If the robot is not already injectable with
@Inject, the generated interface also includes a @Provides method that
constructs the robot. The target must extend ScreenRobot or
ComposeScreenRobot.
annotation class ContributesRobot(
val scope: KClass<*>,
)Classes extending ScreenRobot or ComposeScreenRobot. The robot constructor
does not need @Inject; when @Inject is absent, constructor parameters are
injected through the generated @Provides function. If a robot declares multiple
constructors, annotate the robot class or the constructor Metro should use with
@Inject.
@ContributesRobot(AppScope::class)
class LoginScreenRobot(
private val authRobot: AuthRobot,
) : ComposeScreenRobot<LoginScreenRobot>() {
fun tapSignIn() { clickView(R.id.sign_in) }
fun seeWelcomeMessage() { seeView(R.id.welcome) }
}@ContributesTo(AppScope::class)
interface LoginScreenRobotComponent {
fun getcom_squareup_example_LoginScreenRobotComponent(): LoginScreenRobot
@Provides
fun provideLoginScreenRobotComponent(
authRobot: AuthRobot,
): LoginScreenRobot = LoginScreenRobot(authRobot)
}This exposes the robot as an accessor on the merged graph, making it injectable at
the test site. The generated provider supplies the robot binding only when Metro
cannot already use @Inject. Each robot gets its own contributed interface with a
package-qualified accessor method name derived from the robot fqcn, avoiding
collisions when different packages contribute robots with the same class name.
Contributes a Retrofit service binding to a scope. Handles two distinct paths: real services (interface declarations) and fake services (classes that replace a real service in debug builds).
The target must have exactly one @Qualifier annotation (e.g., @RetrofitAuthenticated,
@RetrofitUnauthenticated) which determines which ServiceCreator is injected.
annotation class ContributesService(
val scope: KClass<*>,
val replaces: Array<KClass<*>> = [],
)- Real services: Retrofit service interfaces with a qualifier annotation.
- Fake services: Classes in debug source sets that implement a real service interface, using
replacesto specify which service they replace.@Injectis optional; if it is absent, the generated binding container includes a@Providesconstructor provider. Fake services with multiple constructors must annotate the service class or the constructor Metro should use with@Inject.
@ContributesService(AppScope::class)
@RetrofitAuthenticated
interface RemoteDeviceApiService {
@POST("/v2/devices")
fun updateDevice(@Body request: UpdateDeviceRequest): Response<UpdateDeviceResponse>
}In release builds:
@ContributesTo(AppScope::class)
@BindingContainer
object ServiceContribution {
@Provides @SingleIn(AppScope::class)
fun provideRemoteDeviceApiService(
@RetrofitAuthenticated serviceCreator: ServiceCreator,
): RemoteDeviceApiService {
return serviceCreator.create(RemoteDeviceApiService::class.java)
}
}In debug builds, an additional @FakeMode safety check is generated to catch cases
where fake mode is enabled but no fake service was provided:
@ContributesTo(AppScope::class)
@BindingContainer
object ServiceContribution {
@Provides @SingleIn(AppScope::class)
fun provideRemoteDeviceApiService(
@RetrofitAuthenticated serviceCreator: ServiceCreator,
@FakeMode isFakeMode: Boolean,
): RemoteDeviceApiService {
check(!isFakeMode) { "No fake service provided for RemoteDeviceApiService." }
return serviceCreator.create(RemoteDeviceApiService::class.java)
}
}// In src/debug
@SingleIn(AppScope::class)
@ContributesService(AppScope::class, replaces = [RemoteDeviceApiService::class])
class FakeRemoteDeviceApiService(
factory: MockServiceHelper.Factory,
) : RemoteDeviceApiService {
private val mockHelper = factory.create<RemoteDeviceApiService>()
override fun updateDevice(request: UpdateDeviceRequest) =
mockHelper.mockResponse { UpdateDeviceResponse() }.updateDevice(request)
// ...
}The generated fake service binding container replaces the real service contribution. It re-creates
the real service binding under a @RealService qualifier, then adds a switcher that picks real or
fake based on the @FakeMode boolean. If the fake service is not already injectable with @Inject,
the container also provides the fake service by calling its constructor:
@ContributesTo(
scope = AppScope::class,
replaces = [
RemoteDeviceApiService::class,
RemoteDeviceApiService.ServiceContribution::class,
],
)
@BindingContainer
object ServiceContribution {
// Real service still available under @RealService qualifier
@Provides @SingleIn(AppScope::class) @RealService
fun provideRemoteDeviceApiService(
@RetrofitAuthenticated serviceCreator: ServiceCreator,
): RemoteDeviceApiService {
return serviceCreator.create(RemoteDeviceApiService::class.java)
}
// Switcher: returns fake or real based on runtime @FakeMode flag
@Provides
fun provideFakeOrRealRemoteDeviceApiService(
@RealService realService: Provider<RemoteDeviceApiService>,
fakeService: Provider<FakeRemoteDeviceApiService>,
@FakeMode isFakeMode: Boolean,
): RemoteDeviceApiService {
return if (isFakeMode) fakeService.get() else realService.get()
}
@Provides
fun provideContributedServiceReplacement(
factory: MockServiceHelper.Factory,
): FakeRemoteDeviceApiService {
return FakeRemoteDeviceApiService(factory)
}
}The qualifier (e.g., @RetrofitAuthenticated) is read from the replaced service
interface's annotations. The fake class must extend all replaced service types. Both
real and fake must use the same scope.
Contributes a class implementing Scoped into a Set<Scoped> qualified with @ForScope. The
scope value is used for both the contribution target and the @ForScope qualifier, since they are
always the same. If the class is not already injectable with @Inject, the plugin also generates a
@Provides function that calls the constructor.
annotation class ContributesMultibindingScoped(
val scope: KClass<*>,
)Classes implementing Scoped.
@ContributesMultibindingScoped(AppScope::class)
class AppLifecycleLogger(
private val logger: Logger,
) : Scoped {
override fun onEnterScope(scope: MortarScope) { logger.log("entered") }
override fun onExitScope() { logger.log("exited") }
}@ContributesTo(AppScope::class)
@BindingContainer
interface MultibindingScopedContribution {
companion object {
@Provides
fun provideContributedMultibindingScoped(logger: Logger): AppLifecycleLogger {
return AppLifecycleLogger(logger)
}
}
@Binds @IntoSet @ForScope(AppScope::class)
fun bindsAppLifecycleLogger(target: AppLifecycleLogger): Scoped
}Note: the scope from the annotation (AppScope) is used in two places -- as the
@ContributesTo scope and as the @ForScope qualifier value. If the class or selected constructor
is annotated with @Inject, the constructor provider is not generated. Classes with multiple
constructors must mark the class or selected constructor with @Inject.
Generates a complete Metro @DependencyGraph component for development/demo apps.
Eliminates the need to manually define the application component interface, its factory,
and its factory provider. The scope is always AppScope.
The annotated class must extend DevelopmentApplication, which provides a reflection-based
provideGraphFactory() method to locate the generated factory at runtime.
annotation class DevelopmentAppComponent(
val generateLoggedInComponent: Boolean = true,
val featureScope: KClass<*> = Unit::class,
val featureComponent: KClass<*> = Unit::class,
) {
interface Factory {
fun create(application: Application): Any
}
}Classes extending DevelopmentApplication.
@DevelopmentAppComponent
class MyDemoApp : DevelopmentApplication()Inside the annotated class, two nested types are generated:
@SingleIn(AppScope::class)
@DependencyGraph(AppScope::class)
interface MetroComponent {
@DependencyGraph.Factory
interface Factory : DevelopmentAppComponent.Factory {
override fun create(@Provides application: Application): MetroComponent
}
}Metro processes the generated @DependencyGraph interface and creates the implementation
class. All @ContributesTo(AppScope::class) contributions are automatically merged into
the graph. The application parameter is provided as a binding via @Provides.
At runtime, DevelopmentApplication.provideGraphFactory() uses reflection to find the
Metro-generated Factory.Impl singleton and returns it as a DevelopmentAppComponent.Factory.
When generateLoggedInComponent is set to false, the @DependencyGraph annotation
includes excludes to remove the logged-in scope infrastructure:
@DependencyGraph(
scope = AppScope::class,
excludes = [LoginScreenModule::class, DevelopmentLoggedInComponent::class],
)
interface MetroComponent { ... }- The annotated class must extend
DevelopmentApplication. - If
featureComponentis specified,featureScopemust also be specified (and vice versa).