Dependency Injection in Kotlin in Practice: 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: What is Dependency Injection and Why Do You Need It in Kotlin?

Dependency Injection (DI) is a design pattern that makes code more flexible, testable, and maintainable. Instead of an object creating its own dependencies, DI passes (injects) them from the outside. In Kotlin, with its concise syntax and powerful capabilities, DI becomes an especially elegant tool.

Imagine you are writing a pizza ordering application. You have a class OrderService that needs to send notifications. Without DI, it would look like this:

class OrderService {    private val notifier = EmailNotifier() // Tight coupling

fun placeOrder(order: Order) { // Order logic notifier.send("Your order has been accepted!") }}

The problem: you cannot easily replace EmailNotifier with SmsNotifier or test OrderService without actually sending emails. DI solves this problem by injecting the dependency through the constructor:

class OrderService(private val notifier: Notifier) {    fun placeOrder(order: Order) {        notifier.send("Your order has been accepted!")    }}

Now OrderService does not know how the notification is sent. This is the essence of DI: loose coupling and high reusability.



Main Ways to Implement DI in Kotlin

In Kotlin, there are three main approaches to dependency injection: manual DI, using libraries (Koin, Dagger 2), and injection through frameworks (e.g., Spring). Let's look at each one in practice.



1. Manual Dependency Injection (Manual DI)

The simplest way is to pass dependencies through constructors or setters. This does not require additional libraries and is perfect for small projects.

Example: Let's create a simple payment processing service.

// Payment gateway interfaceinterface PaymentGateway {    fun process(amount: Double): Boolean}

// PayPal implementationclass PayPalGateway : PaymentGateway { override fun process(amount: Double): Boolean { println("Processing payment $amount via PayPal") return true }}

// Order service with dependency injectionclass OrderService(private val paymentGateway: PaymentGateway) { fun checkout(total: Double) { if (paymentGateway.process(total)) { println("Order placed!") } }}

// Entry point — "factory" or "container"fun main() { val gateway = PayPalGateway() val service = OrderService(gateway) service.checkout(100.0)}

Pros: Simplicity, full control, no library dependencies.
Cons: As the project grows, manually managing all dependencies becomes complex (you need to create a "container" or "factory" manually).



2. Dependency Injection with Koin

Koin is a lightweight DI library written in Kotlin. It does not use reflection and does not require code generation, making it ideal for small and medium-sized projects.

Installation: Add to build.gradle.kts:

implementation("io.insert-koin:koin-core:3.5.6")

Usage example:

import org.koin.core.context.startKoinimport org.koin.dsl.module

// Dependency moduleval appModule = module { single<PaymentGateway> { PayPalGateway() } factory { OrderService(get()) } // get() automatically retrieves PaymentGateway}

fun main() { startKoin { modules(appModule) }

// Get the service from the container val service: OrderService = org.koin.core.component.get() service.checkout(200.0)}

Key Koin concepts:

  • single — creates one instance (singleton)

Blogs

Book Recommendations