metro-di-mobile
Metro DI for Kotlin Multiplatform
Compile-time dependency injection framework for KMP. Production-proven at Cash App.
Setup
build.gradle.kts
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.metro)
}
// Apply Metro plugin to modules that need DI
libs.versions.toml
[versions]
metro = "0.1.1"
[plugins]
metro = { id = "dev.zacsweers.metro", version.ref = "metro" }
Core Concepts
@DependencyGraph
Root container for dependencies. One per application entry point.
// composeApp/src/commonMain/kotlin/di/AppGraph.kt
@DependencyGraph
interface AppGraph {
// Expose dependencies
val authRepository: AuthRepository
val homeComponent: HomeComponent
// Factory methods for runtime parameters
fun createHomeComponent(context: ComponentContext): HomeComponent
}
// Create instance
val graph = createGraph<AppGraph>()
val authRepo = graph.authRepository
@Provides
Define how to create instances.
@DependencyGraph
interface AppGraph {
@Provides
fun provideHttpClient(): HttpClient = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
install(HttpTimeout) {
requestTimeoutMillis = 30_000
}
}
@Provides
fun provideApiService(httpClient: HttpClient): ApiService =
ApiServiceImpl(httpClient, "https://api.your-project.com")
@Provides
fun provideAuthRepository(api: ApiService, tokenStorage: TokenStorage): AuthRepository =
AuthRepositoryImpl(api, tokenStorage)
}
@Inject
Constructor injection for classes.
@Inject
class AuthRepositoryImpl(
private val api: ApiService,
private val tokenStorage: TokenStorage
) : AuthRepository {
override suspend fun login(email: String, password: String): AppResult<User> {
// Implementation
}
}
// Used in graph
@DependencyGraph
interface AppGraph {
val authRepository: AuthRepository // Metro knows to create AuthRepositoryImpl
}
@BindingContainer
Group related providers into modules.
// core/network/src/commonMain/kotlin/di/NetworkModule.kt
@BindingContainer
class NetworkModule {
@Provides
fun provideHttpClient(): HttpClient = HttpClient(CIO) {
install(ContentNegotiation) { json() }
}
@Provides
fun provideApiService(httpClient: HttpClient): ApiService =
ApiServiceImpl(httpClient)
}
// core/data/src/commonMain/kotlin/di/DataModule.kt
@BindingContainer
class DataModule {
@Provides
fun provideTokenStorage(): TokenStorage = TokenStorageImpl()
@Provides
fun providePreferencesDataStore(context: PlatformContext): DataStore<Preferences> =
PreferenceDataStoreFactory.createWithPath(
produceFile = { Path(createDataStorePath(context)) }
)
}
Platform-Specific Graphs
// composeApp/src/commonMain/kotlin/di/CommonModules.kt
@BindingContainer
class CommonNetworkModule {
@Provides
fun provideHttpClient(): HttpClient = HttpClient(CIO) {
install(ContentNegotiation) { json() }
}
}
@BindingContainer
class CommonDataModule {
@Provides
fun provideAuthRepository(api: ApiService, storage: TokenStorage): AuthRepository =
AuthRepositoryImpl(api, storage)
}
// composeApp/src/androidMain/kotlin/di/AndroidAppGraph.kt
@BindingContainer
class AndroidPlatformModule {
@Provides
fun providePlatformContext(context: Context): PlatformContext = context
@Provides
fun provideTokenStorage(context: Context): TokenStorage =
AndroidTokenStorage(context)
}
@DependencyGraph(
bindingContainers = [
CommonNetworkModule::class,
CommonDataModule::class,
AndroidPlatformModule::class
]
)
interface AndroidAppGraph {
val authRepository: AuthRepository
fun createRootComponent(context: ComponentContext): RootComponent
}
// composeApp/src/iosMain/kotlin/di/IosAppGraph.kt
@BindingContainer
class IosPlatformModule {
@Provides
fun providePlatformContext(): PlatformContext = PlatformContext()
@Provides
fun provideTokenStorage(): TokenStorage = IosTokenStorage()
}
@DependencyGraph(
bindingContainers = [
CommonNetworkModule::class,
CommonDataModule::class,
IosPlatformModule::class
]
)
interface IosAppGraph {
val authRepository: AuthRepository
fun createRootComponent(context: ComponentContext): RootComponent
}
Multi-Module DI Pattern
Feature Module Bindings
// feature/auth/impl/src/commonMain/kotlin/di/AuthModule.kt
@BindingContainer
class AuthModule {
@Provides
fun provideAuthRepository(
api: ApiService,
tokenStorage: TokenStorage
): AuthRepository = AuthRepositoryImpl(api, tokenStorage)
@Provides
fun provideLoginUseCase(
authRepository: AuthRepository
): LoginUseCase = LoginUseCase(authRepository)
}
// feature/home/impl/src/commonMain/kotlin/di/HomeModule.kt
@BindingContainer
class HomeModule {
@Provides
fun provideHomeRepository(
api: ApiService,
database: AppDatabase
): HomeRepository = HomeRepositoryImpl(api, database)
}
Assembly in App Graph
// composeApp/src/androidMain/kotlin/di/AndroidAppGraph.kt
@DependencyGraph(
bindingContainers = [
// Core
CommonNetworkModule::class,
CommonDataModule::class,
AndroidPlatformModule::class,
// Features
AuthModule::class,
HomeModule::class
]
)
interface AndroidAppGraph {
// Core
val httpClient: HttpClient
// Features
val authRepository: AuthRepository
val homeRepository: HomeRepository
// Component factories
fun createRootComponent(context: ComponentContext): RootComponent
}
Advanced Features
Scopes
@DependencyGraph(
scope = "app",
additionalScopes = ["activity"]
)
interface AppGraph {
@Provides
@Scope("app")
fun provideAppDatabase(): AppDatabase = AppDatabase()
@Provides
@Scope("activity")
fun provideNavigator(): Navigator = Navigator()
}
Assisted Injection
For dependencies that need runtime parameters.
// Component that needs runtime parameters
@Inject
class HomeComponent(
private val repository: HomeRepository,
@Assisted val componentContext: ComponentContext
) : ComponentContext by componentContext {
// Component logic
}
// Factory interface
@AssistedFactory
interface HomeComponentFactory {
fun create(componentContext: ComponentContext): HomeComponent
}
// In graph
@DependencyGraph
interface AppGraph {
val homeComponentFactory: HomeComponentFactory
}
// Usage
val graph = createGraph<AppGraph>()
val homeComponent = graph.homeComponentFactory.create(componentContext)
Lazy and Provider
@Inject
class SomeService(
private val lazyDatabase: Lazy<AppDatabase>, // Initialized on first access
private val userProvider: Provider<User> // New instance each call
) {
fun doWork() {
val db = lazyDatabase.value // Initialized here
val user1 = userProvider.get()
val user2 = userProvider.get() // Different instance
}
}
Multibindings
@DependencyGraph
interface AppGraph {
@Multibinds
val interceptors: Set<Interceptor>
@Multibinds
val handlers: Map<String, Handler>
}
// Contributing to set
@ContributesIntoSet(AppGraph::class)
class LoggingInterceptor : Interceptor {
override fun intercept(chain: Chain) { /* ... */ }
}
// Contributing to map
@ContributesIntoMap(AppGraph::class, key = "auth")
class AuthHandler : Handler {
override fun handle(request: Request) { /* ... */ }
}
Decompose Integration
Component with DI
// feature/home/impl/src/commonMain/kotlin/HomeComponent.kt
interface HomeComponent {
val state: Value<HomeState>
fun onItemClick(item: HomeItem)
}
@Inject
class DefaultHomeComponent(
private val repository: HomeRepository,
@Assisted componentContext: ComponentContext
) : HomeComponent, ComponentContext by componentContext {
private val _state = MutableValue<HomeState>(HomeState.Loading)
override val state: Value<HomeState> = _state
init {
loadData()
}
private fun loadData() {
componentScope.launch {
repository.getItems()
.onSuccess { _state.value = HomeState.Success(it) }
.onError { msg, _ -> _state.value = HomeState.Error(msg) }
}
}
override fun onItemClick(item: HomeItem) {
// Navigate or handle
}
@AssistedFactory
interface Factory {
fun create(componentContext: ComponentContext): DefaultHomeComponent
}
}
sealed class HomeState {
data object Loading : HomeState()
data class Success(val items: List<HomeItem>) : HomeState()
data class Error(val message: String) : HomeState()
}
Root Component Factory
// composeApp/src/commonMain/kotlin/RootComponent.kt
interface RootComponent {
val childStack: Value<ChildStack<Config, Child>>
sealed class Child {
data class Auth(val component: AuthComponent) : Child()
data class Home(val component: HomeComponent) : Child()
}
@Serializable
sealed class Config {
@Serializable data object Auth : Config()
@Serializable data object Home : Config()
}
}
@Inject
class DefaultRootComponent(
private val authComponentFactory: AuthComponent.Factory,
private val homeComponentFactory: HomeComponent.Factory,
@Assisted componentContext: ComponentContext
) : RootComponent, ComponentContext by componentContext {
private val navigation = StackNavigation<RootComponent.Config>()
override val childStack: Value<ChildStack<RootComponent.Config, RootComponent.Child>> =
childStack(
source = navigation,
serializer = RootComponent.Config.serializer(),
initialConfiguration = RootComponent.Config.Auth,
childFactory = ::createChild
)
private fun createChild(
config: RootComponent.Config,
context: ComponentContext
): RootComponent.Child = when (config) {
RootComponent.Config.Auth -> RootComponent.Child.Auth(
authComponentFactory.create(context) { navigateToHome() }
)
RootComponent.Config.Home -> RootComponent.Child.Home(
homeComponentFactory.create(context)
)
}
private fun navigateToHome() {
navigation.replaceAll(RootComponent.Config.Home)
}
@AssistedFactory
interface Factory {
fun create(componentContext: ComponentContext): DefaultRootComponent
}
}
App Graph with Components
@DependencyGraph(
bindingContainers = [
NetworkModule::class,
DataModule::class,
AuthModule::class,
HomeModule::class
]
)
interface AndroidAppGraph {
val rootComponentFactory: DefaultRootComponent.Factory
}
// Usage in MainActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val graph = createGraph<AndroidAppGraph>()
val rootComponent = graph.rootComponentFactory.create(
defaultComponentContext()
)
setContent {
AppTheme {
RootContent(component = rootComponent)
}
}
}
}
Testing
Test Modules
@BindingContainer
class TestNetworkModule {
@Provides
fun provideFakeApiService(): ApiService = FakeApiService()
}
@DependencyGraph(
bindingContainers = [
TestNetworkModule::class,
DataModule::class
]
)
interface TestAppGraph {
val authRepository: AuthRepository
}
// In tests
class AuthRepositoryTest {
private val graph = createGraph<TestAppGraph>()
@Test
fun `login returns success`() = runTest {
val result = graph.authRepository.login("test@test.com", "password")
assertTrue(result is AppResult.Success)
}
}
Best Practices
Do's
- One
@DependencyGraphper platform entry point - Use
@BindingContainerto organize providers by feature/layer - Use
@Assistedfor runtime parameters (ComponentContext, IDs) - Prefer constructor injection (
@Inject) over@Provides - Keep binding containers in the same module as implementations
- Use
Lazy<T>for expensive dependencies
Don'ts
- Don't create multiple graphs for the same platform
- Don't put platform-specific code in common binding containers
- Don't use
@Provideswhen@Injecton class is sufficient - Don't expose implementation types from graphs (use interfaces)
- Don't put Android Context in common modules
Comparison with Koin
| Feature | Metro | Koin |
|---|---|---|
| Type safety | Compile-time | Runtime |
| Error detection | Build time | Runtime crash |
| Performance | No reflection | Some reflection |
| KMP support | Full | Full |
| Learning curve | Medium (Dagger-like) | Low |
| Build speed | 47-56% faster than KAPT | No code gen |
Resources
More from andvl1/claude-plugin
kmp
Kotlin Multiplatform fundamentals - use for project setup, expect/actual patterns, source sets, and platform-specific code
40workmanager
Android WorkManager for guaranteed background execution - use for deferred tasks, periodic syncs, file uploads, notifications, and task chains. Covers CoroutineWorker, constraints, chaining, testing, and troubleshooting. Use when implementing background work that needs reliable execution across app restarts and doze mode.
16decompose
Decompose navigation and components - use for KMP component architecture, navigation, lifecycle, and state management
15koog
JetBrains Koog AI Agent framework (Kotlin) - use for building AI agents with tool calling, LLM integration via OpenRouter/OpenAI/Anthropic/Google/DeepSeek, streaming, GOAP planning, MCP integration, and AI-powered workflows. Use when implementing AI agents, LLM calls, tool-calling patterns, or integrating LLM providers in Kotlin projects.
11compose
Compose Multiplatform UI patterns - use for shared UI components, theming, resources, and platform-specific adaptations
10compose-arch
Compose Multiplatform Architecture Framework - strict Screen/View/Component layering, use cases, repositories, and feature slice patterns
8