Kotlin & Annotations
In this tutorial you build a Kotlin console app that loads a list of users and prints a greeting. You learn how to annotate classes, scan them into a module, and start Koin from a @KoinApplication object. It takes about 10 minutes and needs a JDK 17 and Gradle.
This tutorial uses annotations. For the same app with the Compiler Plugin DSL, see Kotlin. To compare the styles, see Choosing your style.
Get the code
The full sample is in koin-getting-started/kotlin-annotations. Clone the repository and run it from the kotlin-annotations folder:
./gradlew run
Gradle setup
The sample applies the Koin Compiler Plugin and adds koin-core and koin-annotations. 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-annotations = { module = "io.insert-koin:koin-annotations", 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)
implementation(libs.koin.annotations)
testImplementation(libs.koin.test)
testImplementation(libs.koin.test.junit4)
}
The Compiler Plugin processes the annotations and needs Kotlin 2.3.20 or later. koin-annotations shares the Koin version. For details, see Gradle setup and 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. Annotate the implementation with @Singleton to declare it as a singleton:
import org.koin.core.annotation.Singleton
interface UserRepository {
fun findUserOrNull(name: String): User?
fun addUsers(users: List<User>)
}
@Singleton
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)
}
}
Koin binds UserRepositoryImpl to its supertypes, so a request for UserRepository gets this instance. @Singleton comes from org.koin.core.annotation and is equivalent to @Single (see Definitions).
Let's write the service
UserService loads the users and prepares the greeting. It is a @Singleton too:
interface UserService {
fun getUserOrNull(name: String = DEFAULT_USER.name): User?
fun loadUsers()
fun prepareHelloMessage(user: User?): String
}
@Singleton
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"
}
}
The Compiler Plugin reads the constructor and passes the UserRepository instance for you.
Let's write the application class
UserApplication receives UserService through its constructor, loads the users and prints the greeting:
@Singleton
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)
}
}
All three components are now annotated. Next, gather them in a module.
Let's declare the Koin module
A module groups definitions (see Core Concepts). Declare a module class that scans the sample package:
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Configuration
import org.koin.core.annotation.Module
@Module
@ComponentScan("org.koin.sample")
@Configuration
class AppModule
@ComponentScan registers every annotated class in org.koin.sample and its subpackages. @Configuration makes the module discoverable by a @KoinApplication entry point. Both are described in Modules.
Let's start Koin
Declare a @KoinApplication object and start Koin with the typed startKoin<T>():
import org.koin.core.annotation.KoinApplication
import org.koin.mp.KoinPlatform
import org.koin.plugin.module.dsl.startKoin
@KoinApplication
object KoinUserApplication
fun main() {
startKoin<KoinUserApplication>()
val userApplication = KoinPlatform.getKoin().get<UserApplication>()
userApplication.sayHello("Alice")
}
startKoin<KoinUserApplication>() loads every @Configuration module, so you don't call modules(). The plugin validates the full graph at build time, and ./gradlew run prints Hello 'Alice' (alice@example.com)!. See Starting Koin for the other options.
Next steps
- Core Concepts: the mental model behind modules, definitions and injection.
- Modules:
@Module,@ComponentScan,@Configurationand module includes. - Annotations Reference: every annotation and its parameters.
- Testing: write tests with Koin Test.
- Tooling: IDE plugin and monitoring tools for Koin.