HuxleyMc

android-dependency-injection

1
0
# Install this skill:
npx skills add HuxleyMc/Android-Skills --skill "android-dependency-injection"

Install specific skill from multi-skill repository

# Description

Implements dependency injection with Hilt, Koin, and Metro. Use when setting up DI, migrating between frameworks, organizing modules, or testing with injected dependencies.

# SKILL.md


name: android-dependency-injection
description: Implements dependency injection with Hilt, Koin, and Metro. Use when setting up DI, migrating between frameworks, organizing modules, or testing with injected dependencies.
tags: ["android", "dependency-injection", "hilt", "koin", "metro", "dagger", "ksp", "testing"]
difficulty: intermediate
category: architecture
version: "1.0.0"
last_updated: "2025-01-29"


Android Dependency Injection

Quick Start

Choose your DI framework:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Feature   β”‚     Hilt        β”‚     Koin        β”‚     Metro       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Type        β”‚ Compile-time    β”‚ Runtime         β”‚ Compile-time    β”‚
β”‚ Build Tool  β”‚ KAPT/KSP        β”‚ None            β”‚ KSP             β”‚
β”‚ Startup     β”‚ Slower          β”‚ Faster          β”‚ Fast            β”‚
β”‚ Error Catch β”‚ Build time      β”‚ Runtime         β”‚ Build time      β”‚
β”‚ Jetpack     β”‚ Excellent       β”‚ Good            β”‚ Good            β”‚
β”‚ Learning    β”‚ Steeper         β”‚ Easy            β”‚ Moderate        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Hilt

Setup

// build.gradle (project)
plugins {
    id("com.google.dagger.hilt.android") version "2.48" apply false
}

// build.gradle (app)
plugins {
    id("com.google.dagger.hilt.android")
    id("com.google.devtools.ksp")
}

dependencies {
    implementation("com.google.dagger:hilt-android:2.48")
    ksp("com.google.dagger:hilt-compiler:2.48")

    // Testing
    testImplementation("com.google.dagger:hilt-android-testing:2.48")
    kspTest("com.google.dagger:hilt-compiler:2.48")
}

Application Setup

@HiltAndroidApp
class MyApplication : Application()
<!-- AndroidManifest.xml -->
<application
    android:name=".MyApplication"
    android:label="@string/app_name">
</application>

Basic Injection

// Constructor injection (preferred)
class UserRepository @Inject constructor(
    private val api: UserApi,
    private val dao: UserDao
) {
    suspend fun getUser(id: String): User {
        return dao.getUser(id) ?: api.getUser(id).also {
            dao.insert(it)
        }
    }
}

// Field injection (Android components)
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var viewModelFactory: ViewModelProvider.Factory
}

@AndroidEntryPoint
class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()
}

@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {
    // ...
}

Module Definition

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BuildConfig.BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(HttpLoggingInterceptor())
            .build()
    }

    @Provides
    fun provideUserApi(retrofit: Retrofit): UserApi {
        return retrofit.create(UserApi::class.java)
    }
}

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

    @Binds
    @Singleton
    abstract fun bindUserRepository(
        impl: UserRepositoryImpl
    ): UserRepository
}

Scoping

@Singleton              // Application scope
@ActivityScoped         // Activity scope (survives config change)
@FragmentScoped         // Fragment scope
@ActivityRetainedScoped // Survives config change, shared across fragments
@ViewModelScoped        // ViewModel scope
@ServiceScoped         // Service scope

Qualifiers

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ApiKey

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BaseUrl

@Module
@InstallIn(SingletonComponent::class)
object ConfigModule {

    @Provides
    @ApiKey
    fun provideApiKey(): String = BuildConfig.API_KEY

    @Provides
    @BaseUrl
    fun provideBaseUrl(): String = BuildConfig.BASE_URL
}

class ApiClient @Inject constructor(
    @ApiKey private val apiKey: String,
    @BaseUrl private val baseUrl: String
)

Koin

Setup

// build.gradle (app)
dependencies {
    implementation("io.insert-koin:koin-android:3.5.0")
    implementation("io.insert-koin:koin-androidx-compose:3.5.0")

    // Testing
    testImplementation("io.insert-koin:koin-test:3.5.0")
    testImplementation("io.insert-koin:koin-test-junit4:3.5.0")
}

Application Setup

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidLogger()
            androidContext(this@MyApplication)
            modules(appModule, networkModule, dataModule)
        }
    }
}

Module Definition

val networkModule = module {
    single { 
        OkHttpClient.Builder()
            .addInterceptor(HttpLoggingInterceptor())
            .build() 
    }

    single {
        Retrofit.Builder()
            .baseUrl(BuildConfig.BASE_URL)
            .client(get())
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
    }

    single { get<Retrofit>().create(UserApi::class.java) }
}

val repositoryModule = module {
    single<UserRepository> { UserRepositoryImpl(get(), get()) }
    single { UserRepositoryImpl(get(), get()) } bind UserRepository::class
}

val viewModelModule = module {
    viewModel { UserViewModel(get()) }
    viewModel { (userId: String) -> DetailViewModel(userId, get()) }
}

Injection

// Constructor injection
class UserRepository(
    private val api: UserApi,
    private val dao: UserDao
)

// Field injection in Android
class MainActivity : AppCompatActivity() {
    private val viewModel: UserViewModel by viewModel()
    private val sharedViewModel: SharedViewModel by sharedViewModel()
}

// Compose injection
@Composable
fun UserScreen() {
    val viewModel: UserViewModel = koinViewModel()
    val userId = "123"
    val detailViewModel: DetailViewModel = koinViewModel { parametersOf(userId) }
}

// Direct injection
val repository: UserRepository by inject()
val api: UserApi by inject()

Scoping

module {
    single { }           // Singleton (application scope)
    factory { }          // New instance each time
    scoped { }           // Scope-specific (activity, fragment)
    viewModel { }        // ViewModel scope
}

// Custom scope
val activityScope = scope<MainActivity> {
    scoped { ActivityDependency() }
}

Qualifiers

val appModule = module {
    single(named("api_key")) { BuildConfig.API_KEY }
    single(named("base_url")) { BuildConfig.BASE_URL }

    single { ApiClient(get(named("api_key")), get(named("base_url"))) }
}

// Usage
val apiKey: String by inject(named("api_key"))

Metro (Kotlin Inject)

Setup

// build.gradle (app)
plugins {
    id("com.google.devtools.ksp") version "1.9.20-1.0.14"
}

dependencies {
    implementation("dev.zacsweers.metro:runtime:0.1.0")
    ksp("dev.zacsweers.metro:compiler:0.1.0")
}

Component Definition

// Metro is KSP-based and generates code at compile time
@DependencyGraph
interface AppGraph {
    val userRepository: UserRepository
    val userViewModel: UserViewModel

    @DependencyGraph.Factory
    fun interface Factory {
        fun create(@ApplicationContext context: Context): AppGraph
    }
}

// Generated usage
val graph = AppGraph.Factory.create(context)
val repository = graph.userRepository

Provider Functions

@DependencyGraph
interface AppGraph {
    // Metro generates implementations based on these providers
}

@Provides
fun provideOkHttpClient(): OkHttpClient {
    return OkHttpClient.Builder()
        .addInterceptor(HttpLoggingInterceptor())
        .build()
}

@Provides
fun provideRetrofit(client: OkHttpClient): Retrofit {
    return Retrofit.Builder()
        .baseUrl(BuildConfig.BASE_URL)
        .client(client)
        .build()
}

@Provides
@Singleton
fun provideUserApi(retrofit: Retrofit): UserApi {
    return retrofit.create(UserApi::class.java)
}

Assisted Injection

// Factory for runtime parameters
@AssistedFactory
interface UserViewModelFactory {
    fun create(userId: String): UserViewModel
}

class UserViewModel @AssistedInject constructor(
    @Assisted private val userId: String,
    private val repository: UserRepository
) : ViewModel()

Testing

Hilt Testing

@HiltAndroidTest
class UserRepositoryTest {

    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    @Inject
    lateinit var repository: UserRepository

    @Before
    fun init() {
        hiltRule.inject()
    }

    @Test
    fun testGetUser() = runTest {
        val user = repository.getUser("1")
        assertNotNull(user)
    }
}

// With mocks
@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [NetworkModule::class]
)
object TestNetworkModule {
    @Provides
    fun provideMockApi(): UserApi {
        return mockk {
            coEvery { getUser(any()) } returns User("1", "Test")
        }
    }
}

Koin Testing

class UserRepositoryTest : KoinTest {

    private val repository: UserRepository by inject()

    @get:Rule
    val koinTestRule = KoinTestRule.create {
        modules(testModule)
    }

    @Test
    fun testGetUser() = runTest {
        val user = repository.getUser("1")
        assertNotNull(user)
    }
}

// Test module
val testModule = module {
    single { mockk<UserApi>() }
    single<UserRepository> { UserRepositoryImpl(get(), get()) }
}

Multi-Module Projects

Hilt

// :core:network
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit { }
}

// :feature:user (uses network, doesn't need to know implementation)
@Module
@InstallIn(ViewModelComponent::class)
abstract class UserModule {
    @Binds
    abstract fun bindRepository(impl: UserRepositoryImpl): UserRepository
}

Koin

// :core:network
val networkModule = module {
    single { Retrofit.Builder()... }
}

// :feature:user
val userModule = module {
    single<UserRepository> { UserRepositoryImpl(get()) }
    viewModel { UserViewModel(get()) }
}

// :app
startKoin {
    modules(
        networkModule,
        databaseModule,
        userModule,
        settingsModule
    )
}

Migration Guide

Hilt to Koin

// Before (Hilt)
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel()

// After (Koin)
class UserViewModel(
    private val repository: UserRepository
) : ViewModel()

// Module
val viewModelModule = module {
    viewModel { UserViewModel(get()) }
}

Koin to Hilt

// Before (Koin)
val repositoryModule = module {
    single<UserRepository> { UserRepositoryImpl(get(), get()) }
}

// After (Hilt)
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds
    @Singleton
    abstract fun bindRepository(
        impl: UserRepositoryImpl
    ): UserRepository
}

Examples (Input β†’ Output)

Multi-Qualifier Setup

Input: "Set up Hilt with multiple API clients (staging and production)"

Output:

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ProductionApi

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class StagingApi

@Module
@InstallIn(SingletonComponent::class)
object ApiModule {

    @Provides
    @ProductionApi
    @Singleton
    fun provideProductionRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .build()
    }

    @Provides
    @StagingApi
    @Singleton
    fun provideStagingRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://staging-api.example.com/")
            .build()
    }

    @Provides
    @Singleton
    fun provideUserApi(
        @ProductionApi retrofit: Retrofit
    ): UserApi {
        return retrofit.create(UserApi::class.java)
    }
}

Best Practices

  1. Prefer constructor injection over field injection
  2. Use singleton scope for stateless dependencies (API, Repository)
  3. Use factory scope for stateful dependencies (ViewModel)
  4. Create feature modules in multi-module projects
  5. Avoid component dependencies when possible (use subcomponents)
  6. Use qualifiers sparingly - prefer separate types
  7. Test with real DI - don't manually construct dependencies in tests
  8. Document scoping decisions - why singleton vs factory?

Resources

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.