metro-di-mobile
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMetro DI for Kotlin Multiplatform
适用于Kotlin Multiplatform的Metro DI
Compile-time dependency injection framework for KMP. Production-proven at Cash App.
适用于KMP的编译时依赖注入框架,已在Cash App的生产环境中验证。
Setup
配置步骤
build.gradle.kts
build.gradle.kts
kotlin
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.metro)
}
// Apply Metro plugin to modules that need DIkotlin
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.metro)
}
// 对需要DI的模块应用Metro插件libs.versions.toml
libs.versions.toml
toml
[versions]
metro = "0.1.1"
[plugins]
metro = { id = "dev.zacsweers.metro", version.ref = "metro" }toml
[versions]
metro = "0.1.1"
[plugins]
metro = { id = "dev.zacsweers.metro", version.ref = "metro" }Core Concepts
核心概念
@DependencyGraph
@DependencyGraph
Root container for dependencies. One per application entry point.
kotlin
// 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依赖项的根容器,每个应用入口点对应一个。
kotlin
// composeApp/src/commonMain/kotlin/di/AppGraph.kt
@DependencyGraph
interface AppGraph {
// 暴露依赖项
val authRepository: AuthRepository
val homeComponent: HomeComponent
// 用于运行时参数的工厂方法
fun createHomeComponent(context: ComponentContext): HomeComponent
}
// 创建实例
val graph = createGraph<AppGraph>()
val authRepo = graph.authRepository@Provides
@Provides
Define how to create instances.
kotlin
@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)
}定义实例的创建方式。
kotlin
@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
@Inject
Constructor injection for classes.
kotlin
@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
}类的构造函数注入。
kotlin
@Inject
class AuthRepositoryImpl(
private val api: ApiService,
private val tokenStorage: TokenStorage
) : AuthRepository {
override suspend fun login(email: String, password: String): AppResult<User> {
// 实现逻辑
}
}
// 在依赖图中使用
@DependencyGraph
interface AppGraph {
val authRepository: AuthRepository // Metro会自动创建AuthRepositoryImpl实例
}@BindingContainer
@BindingContainer
Group related providers into modules.
kotlin
// 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)) }
)
}将相关的提供者分组到模块中。
kotlin
// 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
平台特定依赖图
kotlin
// 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
}kotlin
// 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
多模块DI模式
Feature Module Bindings
功能模块绑定
kotlin
// 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)
}kotlin
// 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
在应用依赖图中组装
kotlin
// 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
}kotlin
// composeApp/src/androidMain/kotlin/di/AndroidAppGraph.kt
@DependencyGraph(
bindingContainers = [
// 核心模块
CommonNetworkModule::class,
CommonDataModule::class,
AndroidPlatformModule::class,
// 功能模块
AuthModule::class,
HomeModule::class
]
)
interface AndroidAppGraph {
// 核心依赖项
val httpClient: HttpClient
// 功能依赖项
val authRepository: AuthRepository
val homeRepository: HomeRepository
// 组件工厂
fun createRootComponent(context: ComponentContext): RootComponent
}Advanced Features
高级特性
Scopes
作用域
kotlin
@DependencyGraph(
scope = "app",
additionalScopes = ["activity"]
)
interface AppGraph {
@Provides
@Scope("app")
fun provideAppDatabase(): AppDatabase = AppDatabase()
@Provides
@Scope("activity")
fun provideNavigator(): Navigator = Navigator()
}kotlin
@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.
kotlin
// 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)用于需要运行时参数的依赖项。
kotlin
// 需要运行时参数的组件
@Inject
class HomeComponent(
private val repository: HomeRepository,
@Assisted val componentContext: ComponentContext
) : ComponentContext by componentContext {
// 组件逻辑
}
// 工厂接口
@AssistedFactory
interface HomeComponentFactory {
fun create(componentContext: ComponentContext): HomeComponent
}
// 在依赖图中
@DependencyGraph
interface AppGraph {
val homeComponentFactory: HomeComponentFactory
}
// 使用方式
val graph = createGraph<AppGraph>()
val homeComponent = graph.homeComponentFactory.create(componentContext)Lazy and Provider
Lazy与Provider
kotlin
@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
}
}kotlin
@Inject
class SomeService(
private val lazyDatabase: Lazy<AppDatabase>, // 首次访问时初始化
private val userProvider: Provider<User> // 每次调用返回新实例
) {
fun doWork() {
val db = lazyDatabase.value // 在此处初始化
val user1 = userProvider.get()
val user2 = userProvider.get() // 不同的实例
}
}Multibindings
多绑定
kotlin
@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) { /* ... */ }
}kotlin
@DependencyGraph
interface AppGraph {
@Multibinds
val interceptors: Set<Interceptor>
@Multibinds
val handlers: Map<String, Handler>
}
// 向集合中贡献实例
@ContributesIntoSet(AppGraph::class)
class LoggingInterceptor : Interceptor {
override fun intercept(chain: Chain) { /* ... */ }
}
// 向映射中贡献实例
@ContributesIntoMap(AppGraph::class, key = "auth")
class AuthHandler : Handler {
override fun handle(request: Request) { /* ... */ }
}Decompose Integration
与Decompose集成
Component with DI
带DI的组件
kotlin
// 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()
}kotlin
// 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) {
// 导航或处理逻辑
}
@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
根组件工厂
kotlin
// 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
}
}kotlin
// 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
包含组件的应用依赖图
kotlin
@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)
}
}
}
}kotlin
@DependencyGraph(
bindingContainers = [
NetworkModule::class,
DataModule::class,
AuthModule::class,
HomeModule::class
]
)
interface AndroidAppGraph {
val rootComponentFactory: DefaultRootComponent.Factory
}
// 在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
测试模块
kotlin
@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)
}
}kotlin
@BindingContainer
class TestNetworkModule {
@Provides
fun provideFakeApiService(): ApiService = FakeApiService()
}
@DependencyGraph(
bindingContainers = [
TestNetworkModule::class,
DataModule::class
]
)
interface TestAppGraph {
val authRepository: AuthRepository
}
// 在测试中使用
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 per platform entry point
@DependencyGraph - Use to organize providers by feature/layer
@BindingContainer - Use for runtime parameters (ComponentContext, IDs)
@Assisted - Prefer constructor injection () over
@Inject@Provides - Keep binding containers in the same module as implementations
- Use for expensive dependencies
Lazy<T>
- 每个平台入口点对应一个
@DependencyGraph - 使用按功能/层组织提供者
@BindingContainer - 对运行时参数(ComponentContext、ID等)使用
@Assisted - 优先使用构造函数注入()而非
@Inject@Provides - 将绑定容器与实现放在同一模块中
- 对开销大的依赖项使用
Lazy<T>
Don'ts
不建议
- Don't create multiple graphs for the same platform
- Don't put platform-specific code in common binding containers
- Don't use when
@Provideson class is sufficient@Inject - Don't expose implementation types from graphs (use interfaces)
- Don't put Android Context in common modules
- 不为同一平台创建多个依赖图
- 不要在通用绑定容器中放置平台特定代码
- 当类上的足够时,不要使用
@Inject@Provides - 不要从依赖图中暴露实现类型(使用接口)
- 不要在通用模块中放置Android Context
Comparison with Koin
与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 |
| 特性 | Metro | Koin |
|---|---|---|
| 类型安全 | 编译时 | 运行时 |
| 错误检测 | 构建时 | 运行时崩溃 |
| 性能 | 无反射 | 部分反射 |
| KMP支持 | 完全支持 | 完全支持 |
| 学习曲线 | 中等(类似Dagger) | 低 |
| 构建速度 | 比KAPT快47-56% | 无代码生成 |