Service Injection
Kontainer supports constructor injection, factory methods, lazy injection, and injecting multiple implementations of an interface.
Constructor injection
The simplest and most common pattern. Declare dependencies in your constructor:
class Counter {
private var count = 0
fun next() = ++count
}
class MyService(val counter: Counter)
val blueprint = kontainer {
singleton(Counter::class)
singleton(MyService::class)
}
val kontainer = blueprint.create()
val service = kontainer.get(MyService::class)
println(service.counter.next()) // 1
println(service.counter.next()) // 2 Factory method injection
When a service needs parameters that Kontainer doesn't know about, use a factory lambda. The lambda's parameters are injected; you handle the rest.
class MyService(private val counter: Counter, private val offset: Int) {
fun next() = counter.next() + offset
}
val blueprint = kontainer {
singleton(Counter::class)
// Kontainer injects Counter; you provide the offset
singleton(MyService::class) { counter: Counter ->
MyService(counter, 100)
}
} Factories support up to 10 injected parameters.
Inject by super type
A service can be injected by any of its supertypes — interface or parent class:
interface CounterInterface {
fun next(): Int
}
class Counter : CounterInterface {
private var count = 0
override fun next() = ++count
}
// MyService depends on the interface, not the concrete class
class MyService(val counter: CounterInterface)
val blueprint = kontainer {
singleton(Counter::class)
singleton(MyService::class)
} If two services implement the same interface, injection becomes ambiguous and Kontainer reports an error. Use List<T> injection instead (see below).
Nullable injection
Mark a dependency as nullable to make it optional. If the service isn't registered, null is injected instead of throwing:
class SomeOptionalService
// Will receive null if SomeOptionalService is not registered
class MyService(val optional: SomeOptionalService?)
val blueprint = kontainer {
singleton(MyService::class)
// SomeOptionalService is NOT registered
}
val kontainer = blueprint.create()
println(kontainer.get(MyService::class).optional) // null Inject all implementations
Inject all services that implement a given type as a List. This is powerful for building extensible, plugin-style architectures:
interface Repository {
val name: String
}
class UserRepository : Repository {
override val name = "users"
}
class OrderRepository : Repository {
override val name = "orders"
}
// Injects ALL Repository implementations
class Database(val repos: List<Repository>)
val blueprint = kontainer {
singleton(Database::class)
singleton(UserRepository::class)
singleton(OrderRepository::class)
}
val kontainer = blueprint.create()
kontainer.get(Database::class).repos.forEach {
println(it.name) // "users", "orders"
} Lazy injection
Wrap a dependency in Lazy<T> to defer its creation until first use:
class ExpensiveService {
init { println("ExpensiveService created!") }
fun doWork() = "done"
}
class MyService(private val lazy: Lazy<ExpensiveService>) {
fun run() = lazy.value.doWork()
}
val blueprint = kontainer {
singleton(ExpensiveService::class)
singleton(MyService::class)
}
val kontainer = blueprint.create()
val service = kontainer.get(MyService::class)
// ExpensiveService is NOT created yet
service.run()
// NOW ExpensiveService is created For a more idiomatic Kotlin style, use by delegation to unwrap the lazy automatically:
class MyService(lazyService: Lazy<ExpensiveService>) {
private val service by lazyService
fun run() = service.doWork()
} Breaking circular dependencies with Lazy
When two services need each other, make one of them lazy to break the cycle:
class ServiceOne(private val two: ServiceTwo) {
val name = "one"
fun greet() = "I know ${two.name}"
}
class ServiceTwo(private val one: Lazy<ServiceOne>) {
val name = "two"
fun greet() = "I know ${one.value.name}"
}
val blueprint = kontainer {
singleton(ServiceOne::class)
singleton(ServiceTwo::class)
}
val kontainer = blueprint.create()
println(kontainer.get(ServiceOne::class).greet()) // "I know two"
println(kontainer.get(ServiceTwo::class).greet()) // "I know one" Lookup injection
For large sets of implementations where you only need specific ones at a time, use Lookup<T>. Unlike List<T>, a Lookup creates services on demand:
import io.peekandpoke.ultra.common.Lookup
class Database(val repos: Lookup<Repository>)
val blueprint = kontainer {
singleton(Database::class)
dynamic(UserRepository::class)
dynamic(OrderRepository::class)
}
val kontainer = blueprint.create()
val db = kontainer.get(Database::class)
// Only UserRepository is created — OrderRepository stays untouched
val users = db.repos.get(UserRepository::class)
println(users.name) // "users" Injection patterns summary
| Pattern | Constructor type | Behavior |
|---|---|---|
| Direct | val x: Service | Injected immediately |
| Nullable | val x: Service? | Null if not registered |
| List | val x: List<Base> | All implementations |
| Lookup | val x: Lookup<Base> | Lazy, on-demand access |
| Lazy | val x: Lazy<Service> | Created on first .value |
| Lazy List | val x: Lazy<List<Base>> | Deferred list of all impls |