Dependency Injection in Kotlin: A Simple Guide for Beginners

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?

\n

Imagine you are building a house. You need bricks, cement, and tools. Instead of running to the warehouse for them every time, you simply order the delivery of all materials to the construction site. Dependency Injection (DI) is the same principle, but for your code.

\n

Dependency Injection is a design pattern where an object receives its dependencies from the outside, rather than creating them itself. This makes the code more flexible, testable, and easier to maintain. In this article, we will break down DI using Kotlin — a modern programming language that is great for learning this pattern.

\n\n

The Problem: Why Does Code Become Fragile Without DI?

\n

Let's look at a typical example. Suppose we have a class UserService that sends email notifications. Without DI, it looks like this:

\n
class EmailService {\n    fun sendEmail(to: String, message: String) {\n        println("Sending email to $to: $message")\n    }\n}\n\nclass UserService {\n    private val emailService = EmailService() // Tight coupling!\n\n    fun notifyUser(email: String) {\n        emailService.sendEmail(email, "Hello, user!")\n    }\n}
\n

What's the problem? UserService is tightly coupled to a specific implementation of EmailService. If you want to:

\n
    \n
  • Replace EmailService with SmsService;
  • \n
  • Add logging;
  • \n
  • Write unit tests with a mock object,
  • \n
\n

— you will have to change the code of UserService. This violates the Open/Closed principle and complicates testing.

\n\n

The Solution: Manual Dependency Injection in Kotlin

\n

The simplest way to implement DI is to pass the dependency through the constructor. In Kotlin, this is done very concisely:

\n
// 1. Create an interface (abstraction)\ninterface NotificationService {\n    fun send(to: String, message: String)\n}\n\n// 2. Implement concrete services\nclass EmailService : NotificationService {\n    override fun send(to: String, message: String) {\n        println("Email to $to: $message")\n    }\n}\n\nclass SmsService : NotificationService {\n    override fun send(to: String, message: String) {\n        println("SMS to $to: $message")\n    }\n}\n\n// 3. Inject dependency via constructor\nclass UserService(private val notificationService: NotificationService) {\n    fun notifyUser(email: String) {\n        notificationService.send(email, "Hello!")\n    }\n}\n\n// 4. Usage\nfun main() {\n    val emailService = EmailService()\n    val userService = UserService(emailService) // Injection!\n    userService.notifyUser("user@example.com")\n}
\n

Advantages of this approach:

\n
    \n
  • Flexibility: you can easily replace EmailService with SmsService;
  • \n
  • Testability: in tests, you can pass a mock object:
  • \n
\n
class MockNotificationService : NotificationService {\n    var lastMessage: String? = null\n    override fun send(to: String, message: String) {\n        lastMessage = message\n    }\n}\n\nfun test() {\n    val mock = MockNotificationService()\n    val userService = UserService(mock)\n    userService.notifyUser("test@test.com")\n    assert(mock.lastMessage == "Hello!")\n}
\n\n

DI Frameworks: Koin and Dagger for Kotlin

\n

With manual DI, if the project is large, you write a lot of boilerplate code. This is where DI frameworks come to the rescue. In the Kotlin world, two are popular: Koin (simple) and Dagger/Hilt (powerful).

\n\n

Koin: Simplicity and Speed

\n

Koin is a lightweight DI framework written in Kotlin. It does not require code generation and is

Blogs

Book Recommendations