Kotlin
In this tutorial you build a Kotlin console app that loads a list of users and prints a greeting. You learn how to declare a Koin module, bind implementations to interfaces, and start Koin. It takes about 10 minutes and needs a JDK 17 and Gradle.
This tutorial uses the Compiler Plugin DSL. For the same app with annotations, see Kotlin & Annotations. To compare the styles, see Choosing your style.
Get the code
The full sample is in koin-getting-started/kotlin. Clone the repository and run it from the kotlin folder:
./gradlew run
Gradle setup
The sample applies the Koin Compiler Plugin and adds koin-core. Versions live in a version catalog:
# gradle/libs.versions.toml
[versions]
kotlin = "<KOTLIN_VERSION>"
koin = "<KOIN_VERSION>"
koin-plugin = "<KOIN_PLUGIN_VERSION>"
[libraries]
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" }
koin-test-junit4 = { module = "io.insert-koin:koin-test-junit4", version.ref = "koin" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
koin-compiler = { id = "io.insert-koin.compiler.plugin", version.ref = "koin-plugin" }
// app/build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.koin.compiler)
application
}
dependencies {
implementation(libs.koin.core)
testImplementation(libs.koin.test)
testImplementation(libs.koin.test.junit4)
}
The Compiler Plugin needs Kotlin 2.3.20 or later. For current versions and the BOM, see Gradle setup. For plugin options, see Compiler Plugin setup.
The application
The app chains three components. UserApplication asks UserService for a user, and UserService reads from UserRepository:
User -> UserRepository -> UserService -> UserApplication
Let's write the user data
Start with the data class and a default list of users:
data class User(val name: String, val email: String)
val DEFAULT_USER = User("Alice", "alice@example.com")
val DEFAULT_USERS = listOf(
DEFAULT_USER,
User("Bob", "bob@example.com"),
User("Charlie", "charlie@example.com")
)
These users are the data that the repository stores.
Let's write the repository
The repository keeps users in memory and finds one by name:
interface UserRepository {
fun findUserOrNull(name: String): User?
fun addUsers(users: List<User>)
}
class UserRepositoryImpl : UserRepository {
private val _users = arrayListOf<User>()
override fun findUserOrNull(name: String): User? {
return _users.firstOrNull { it.name == name }
}
override fun addUsers(users: List<User>) {
_users.addAll(users)
}
}
UserRepositoryImpl has no dependency, so Koin can create it with no extra information.
Let's declare the Koin module
A module is where you declare the components Koin creates (see Core Concepts). Declare UserRepositoryImpl as a singleton, available as UserRepository:
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koin.plugin.module.dsl.single
val appModule = module {
single<UserRepositoryImpl>() bind UserRepository::class
}
Any component that asks for a UserRepository now gets the same UserRepositoryImpl instance. single and bind are described in Definitions.
Let's write the service
UserService loads the users and prepares the greeting:
interface UserService {
fun getUserOrNull(name: String = DEFAULT_USER.name): User?
fun loadUsers()
fun prepareHelloMessage(user: User?): String
}
class UserServiceImpl(
private val userRepository: UserRepository
) : UserService {
override fun getUserOrNull(name: String): User? = userRepository.findUserOrNull(name)
override fun loadUsers() {
userRepository.addUsers(DEFAULT_USERS)
}
override fun prepareHelloMessage(user: User?): String {
return user?.let { "Hello '${user.name}' (${user.email})!" } ?: "User not found"
}
}
Add it to the module:
val appModule = module {
single<UserRepositoryImpl>() bind UserRepository::class
single<UserServiceImpl>() bind UserService::class
}
UserServiceImpl takes a UserRepository in its constructor. The Compiler Plugin reads that constructor and passes the UserRepositoryImpl instance for you.
Let's write the application class
UserApplication receives UserService through its constructor, loads the users and prints the greeting:
class UserApplication(
private val userService: UserService
) {
init {
userService.loadUsers()
}
fun sayHello(name: String) {
val user = userService.getUserOrNull(name)
val message = userService.prepareHelloMessage(user)
println(message)
}
}
Declare it in the module too:
val appModule = module {
single<UserApplication>()
single<UserRepositoryImpl>() bind UserRepository::class
single<UserServiceImpl>() bind UserService::class
}
All three components are now in the module. The plugin checks at build time that each constructor parameter has a definition.
Let's start Koin
Start Koin in main() with your module, then get UserApplication from the container:
import org.koin.core.context.startKoin
import org.koin.mp.KoinPlatform
fun main() {
startKoin {
modules(appModule)
}
val userApplication = KoinPlatform.getKoin().get<UserApplication>()
userApplication.sayHello("Alice")
}
Run ./gradlew run and the app prints Hello 'Alice' (alice@example.com)!. startKoin and its options are covered in Starting Koin.
Next steps
- Core Concepts: the mental model behind modules, definitions and injection.
- Definitions: every definition type and binding option.
- JUnit Tests: test this same app with Koin Test.
- Compile-Time Safety: what the Compiler Plugin checks in your graph.
- Tooling: IDE plugin and monitoring tools for Koin.