Dependency Injection in Kotlin: A Complete Guide

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction to Dependency Injection in Kotlin



Modern Kotlin development requires proper management of dependencies between classes. Dependency Injection (DI) is a design pattern that allows passing an object's dependencies from the outside, rather than creating them internally. This reduces code coupling, simplifies testing, and makes the application more flexible.



In this article, we will explore what dependency injection is, why it is needed, and examine three approaches to implementing it in Kotlin: manual injection, using the Hilt library, and the lightweight Koin framework. The material will be useful for both beginners and experienced developers looking to organize their project architecture.



Basics of Dependency Injection



The Problem Without DI



Imagine a UserService class that directly creates an instance of UserRepository:



class UserService {    private val repository = UserRepository() // tight coupling

fun getUser(id: Int): User { return repository.findById(id) }}


Such code is difficult to test: you cannot replace UserRepository with a mock object. Additionally, if the constructor of UserRepository changes, you will have to modify all places where it is created.



Solution: Constructor Injection



The simplest way to apply DI is to pass the dependency through the constructor:



class UserService(private val repository: UserRepository) {    fun getUser(id: Int): User {        return repository.findById(id)    }}


Now UserService does not know how UserRepository is created. You can pass any implementation — a real database, a fake layer, or a mock for testing.



Inversion of Control (IoC)



DI is closely related to the Inversion of Control (IoC) principle. Instead of the class controlling the creation of its dependencies, control is handed over to an external container or factory. This makes the system loosely coupled and easily extensible.



Manual Dependency Injection (Manual DI)



For small projects or educational purposes, you can manage without frameworks. Let's create a simple dependency container manually.



Example: Dependency Container



// Define the repository interfaceinterface UserRepository {    fun findById(id: Int): User}

// Implementationclass UserRepositoryImpl : UserRepository { override fun findById(id: Int): User { // logic for retrieving user from the database return User(id, "User$id") }}

// Container that assembles all dependenciesclass AppContainer { val userRepository: UserRepository = UserRepositoryImpl() val userService: UserService by lazy { UserService(userRepository) }}

// Usagefun main() { val container = AppContainer() val service = container.userService println(service.getUser(1))}


Pros: full control, no external dependencies, easy to understand.

Cons: as the project grows, the container expands, making it difficult to manage object lifecycles and scopes.



Dependency Injection with Hilt



Hilt is Google's official library for DI in Android applications using Kotlin. It is built on top of Dagger but is significantly easier to configure.



Setting up Hilt



Add to build.gradle.kts (project level):



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


In the app module's build.gradle.kts:



plugins {    id("com.google.dagger.hilt.android")}

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

Blogs

Book Recommendations