Kotlin Dependency Injection — Hilt and Koin Guide
In this tutorial, you will learn about Kotlin Dependency Injection. We cover key concepts, practical examples, and best practices to help you master this topic.
Kotlin dependency injection uses Hilt for Android applications with compile-time Dagger integration and Koin for lightweight multiplatform DI, enabling constructor injection, scoped dependencies, and testable code architecture.
What You'll Learn
- Add Hilt to an Android project
- Define modules and provide dependencies
- Inject into ViewModels, activities, and fragments
- Use scopes for singletons and per-screen instances
- Set up Koin for multiplatform projects
- Test with mocked dependencies
- Understand DI best practices
Why It Matters
Dependency injection decouples object creation from usage, making code testable, modular, and maintainable. Without DI, classes create their own dependencies, leading to tight coupling and difficult testing. Hilt simplifies Dagger's compile-time DI for Android with annotations. Koin provides a simpler runtime DI for multiplatform projects.
Real-World Use
DodaTech uses Hilt for all Android dependencies: repositories, API clients, database instances, and analytics. Modules provide singleton dependencies with clear scopes. The Koin-based multiplatform module shares business logic dependencies across Android and iOS.
Learning Path
flowchart LR A[Testing] --> B[Dependency Injection\nYou are here] B --> C[Project CLI Tool] style B fill:#f90,color:#fff
Hilt Setup
// build.gradle.kts (project level)
plugins {
id("com.google.dagger.hilt.android") version "2.51.1" apply false
}
// build.gradle.kts (app level)
plugins {
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.51.1")
kapt("com.google.dagger:hilt-android-compiler:2.51.1")
// Hilt ViewModel
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
// For testing
testImplementation("com.google.dagger:hilt-android-testing:2.51.1")
kaptTest("com.google.dagger:hilt-android-compiler:2.51.1")
}
Hilt Application Class
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Application-level initialization
}
}
Every app with Hilt needs an @HiltAndroidApp Application class.
Hilt Modules
Modules tell Hilt how to provide dependencies.
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
}
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
).build()
}
@Provides
fun provideUserDao(database: AppDatabase): UserDao {
return database.userDao()
}
}
Output: Hilt generates the implementation that creates these dependencies once (singletons) and injects them wherever requested.
Injecting into Activities and Fragments
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var apiService: ApiService
@Inject
lateinit var database: AppDatabase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// apiService and database are already injected
}
}
@AndroidEntryPoint
class HomeFragment : Fragment() {
private val viewModel: HomeViewModel by viewModels()
// Field injection
@Inject
lateinit var analyticsTracker: AnalyticsTracker
}
Hilt injects dependencies into the activity or fragment after onCreate.
Hilt ViewModel Injection
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
private val userRepository: UserRepository,
private val apiService: ApiService,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _users = MutableStateFlow<ApiResult<List<User>>>(ApiResult.Loading)
val users: StateFlow<ApiResult<List<User>>> = _users.asStateFlow()
init {
loadUsers()
}
fun loadUsers() {
viewModelScope.launch {
_users.value = ApiResult.Loading
try {
val users = userRepository.getUsers()
_users.value = ApiResult.Success(users)
} catch (e: Exception) {
_users.value = ApiResult.Error(e.message ?: "Unknown error")
}
}
}
}
// In Compose
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel()
) {
// ViewModel is provided by Hilt
val users by viewModel.users.collectAsState()
// ...
}
Output: HiltViewModel creates the ViewModel with all its dependencies injected.
Hilt Scopes
| Scope | Lifetime |
|---|---|
| @Singleton | Application lifetime |
| @ActivityScoped | Per activity |
| @FragmentScoped | Per fragment |
| @ViewModelScoped | Per ViewModel |
| @ViewScoped | Per Android View |
@Module
@InstallIn(ActivityComponent::class)
object ActivityModule {
@Provides
@ActivityScoped
fun provideActivityScopedService(): ActivityScopedService {
return ActivityScopedService()
}
}
@ActivityScoped
class ActivityScopedService @Inject constructor() {
// One instance per activity
}
Qualifiers
Use qualifiers when multiple implementations of the same type exist.
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class RemoteDataSource
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LocalDataSource
@Module
@InstallIn(SingletonComponent::class)
object DataSourceModule {
@RemoteDataSource
@Provides
@Singleton
fun provideRemoteDataSource(api: ApiService): UserDataSource {
return RemoteUserDataSource(api)
}
@LocalDataSource
@Provides
@Singleton
fun provideLocalDataSource(dao: UserDao): UserDataSource {
return LocalUserDataSource(dao)
}
}
class UserRepository @Inject constructor(
@RemoteDataSource private val remote: UserDataSource,
@LocalDataSource private val local: UserDataSource
) {
suspend fun getUsers(): List<User> {
return try {
val users = remote.getUsers()
// Cache locally
users
} catch (e: Exception) {
local.getUsers()
}
}
}
Koin for Multiplatform
Koin is a lightweight DI framework that works on all KMP targets.
// build.gradle.kts
implementation("io.insert-koin:koin-core:3.5.6")
implementation("io.insert-koin:koin-android:3.5.6")
implementation("io.insert-koin:koin-androidx-compose:3.5.6")
// Define modules
val appModule = module {
single { OkHttpClient() }
single { ApiService(get()) }
factory { UserRepository(get()) }
viewModel { UserViewModel(get()) }
}
// Start Koin
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApplication)
modules(appModule)
}
}
}
// Inject in composables
@Composable
fun HomeScreen(
viewModel: UserViewModel = koinViewModel()
) {
// Use viewModel
}
Koin uses single for singletons, Factory for new instances each time, and scoped for scope-bound instances.
Testing with Hilt
@HiltAndroidTest
class UserRepositoryTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Inject
lateinit var repository: UserRepository
@Before
fun setup() {
hiltRule.inject()
}
@Test
fun testGetUsers() = runTest {
val users = repository.getUsers()
assertTrue(users.isNotEmpty())
}
}
Common Mistakes
Not annotating the Application class: Without @HiltAndroidApp, Hilt cannot initialize. Add it to the Application class before using Hilt.
Forgetting to annotate entry points: Activities and fragments need @AndroidEntryPoint. ViewModels need @HiltViewModel.
Mixing field injection and constructor injection: Prefer constructor injection. Field injection hides dependencies and makes testing harder.
Over-scoping dependencies: Using @Singleton for everything prevents per-screen state. Use the narrowest scope needed.
Circular dependencies: Dagger detects circular dependencies at compile time. Refactor to break the cycle if this occurs.
Not providing Retrofit converters: If @Provides functions miss dependencies, Hilt reports missing bindings at compile time.
Practice Questions
- What is the purpose of @InstallIn in Hilt?
Answer: @InstallIn defines the component (scope) where the module is installed. Common options: SingletonComponent (app-wide), ActivityComponent (per activity), FragmentComponent (per fragment).
- How does Hilt ViewModel injection work?
Answer: Annotate the ViewModel with @HiltViewModel and use @Inject constructor. In Compose, use hiltViewModel(). In fragments, use by viewModels(). Hilt provides the ViewModel with its dependencies.
- What is the difference between @Singleton and @ActivityScoped?
Answer: @Singleton creates one instance for the entire application lifetime. @ActivityScoped creates one instance per activity, recreated when the activity is recreated.
- When would you use a @Qualifier?
Answer: When you have multiple implementations of the same interface (e.g., RemoteDataSource and LocalDataSource). Qualifiers distinguish which implementation to inject.
- Challenge: Convert an Android app from manual DI to Hilt. The app has three screens, two repositories, a database, and an API service. Identify all components that need modules and injection points.
Answer: Create AppModule for singletons (database, API), RepositoryModule for repositories (scoped per activity), and ViewModelModule for ViewModels. Annotate Application, activities, fragments, and ViewModels. Remove manual factory code.
Mini Project
Refactor the todo list app to use Hilt. Requirements:
- @HiltAndroidApp Application class
- AppModule with @Singleton API service, database, Repository
- @HiltViewModel for ListViewModel and DetailViewModel
- @AndroidEntryPoint for activities and fragments
- @Qualifier for RemoteRepository and LocalRepository
- Test ViewModel with HiltAndroidTest
- Koin alternative in a separate branch
This project demonstrates DI Migration and both DI frameworks in practice.
FAQ
What's Next
After learning DI, build a CLI tool project to apply your knowledge. You can also explore coroutines for advanced async patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro