KOtlin DEpendency INjection - A straightforward and yet very useful dependency retrieval container for Kotlin Multiplatform
—
Advanced dependency injection patterns including constructor injection, external sources, binding search, sub-DI creation, and specialized utilities for complex dependency management scenarios.
Automatic constructor parameter injection for creating instances with dependency resolution without explicit binding definitions.
/**
* Create instance with no-parameter constructor injection
* @param constructor Constructor function reference
* @return New instance with all dependencies automatically injected
*/
fun <T> DirectDIAware.new(constructor: () -> T): T
/**
* Create instance with 1-parameter constructor injection
* @param constructor Constructor function reference
* @return New instance with P1 automatically injected via instance<P1>()
*/
fun <P1, T> DirectDIAware.new(constructor: (P1) -> T): T
/**
* Create instance with 2-parameter constructor injection
* @param constructor Constructor function reference
* @return New instance with P1, P2 automatically injected
*/
fun <P1, P2, T> DirectDIAware.new(constructor: (P1, P2) -> T): T
/**
* Create instance with 3-parameter constructor injection
* @param constructor Constructor function reference
* @return New instance with P1, P2, P3 automatically injected
*/
fun <P1, P2, P3, T> DirectDIAware.new(constructor: (P1, P2, P3) -> T): T
// ... continues up to 22 parameter overloads
/**
* Exception thrown when a constructor parameter is not used in injection
* This helps detect unused parameter instances in new operator calls
*/
class DI.UnusedParameterException(message: String, cause: Exception? = null) : RuntimeException(message, cause)Fallback mechanism for dependency resolution when bindings are not found in the container, enabling integration with other DI frameworks or dynamic resolution.
/**
* External source interface for fallback dependency resolution
*/
interface ExternalSource {
/**
* Attempt to provide a factory for the given key when not found in container
* @param key The binding key that was not found
* @param context The resolution context
* @return Factory function or null if this source cannot provide the dependency
*/
fun <C : Any, A, T : Any> getFactory(
key: DI.Key<C, A, T>,
context: C
): ((A) -> T)?
}
/**
* External sources list for registering fallback resolution mechanisms
* External sources are consulted in order when bindings are not found
*/
val DI.MainBuilder.externalSources: MutableList<ExternalSource>Creation of child DI containers that inherit from parent containers while allowing additional bindings and overrides.
/**
* Create a sub-DI container with additional bindings
* @param allowSilentOverride Whether to allow implicit binding overrides
* @param copy Copy strategy for inheriting bindings from parent
* @param init Configuration block for additional bindings
* @return New DI container extending the current one
*/
fun DirectDIAware.subDI(
allowSilentOverride: Boolean = false,
copy: Copy = Copy.NonCached,
init: DI.MainBuilder.() -> Unit
): DI
/**
* Copy strategies for sub-DI creation
*/
sealed class Copy {
/** Copy no bindings from parent */
object None : Copy()
/** Copy all bindings from parent */
object All : Copy()
/** Copy only non-cached bindings (providers, factories) */
object NonCached : Copy()
}Utilities for converting factory functions into provider functions by pre-supplying arguments.
/**
* Convert a factory function to a provider by currying with an argument
* @param arg Function that provides the argument for the factory
* @return Provider function that calls the factory with the supplied argument
*/
fun <A, T> ((A) -> T).toProvider(arg: () -> A): () -> TAdvanced binding search capabilities for finding and inspecting bindings within the DI container.
/**
* Specifications for searching bindings in the container
*/
data class SearchSpecs(
val contextType: TypeToken<*>? = null,
val argType: TypeToken<*>? = null,
val type: TypeToken<*>? = null,
val tag: Any? = null
)
/**
* DSL for building search specifications
*/
class SearchDSL {
/**
* Search for bindings with specific context type
*/
fun <C : Any> context(type: TypeToken<C>)
/**
* Search for bindings with specific argument type
*/
fun <A> argument(type: TypeToken<A>)
/**
* Search for bindings with specific return type
*/
fun <T> type(type: TypeToken<T>)
/**
* Search for bindings with specific tag
*/
fun tag(tag: Any)
}
/**
* DSL for finding bindings in the container
*/
class FindDSL : SearchDSL() {
// Additional methods for finding specific binding patterns
}
/**
* Find all bindings matching the search criteria
* @param f DSL block for specifying search criteria
* @return List of matching bindings with their keys and context matches
*/
fun DITree.findAllBindings(
f: FindDSL.() -> Unit
): List<Triple<DI.Key<*, *, *>, DIBinding<*, *, *>, ContextMatch>>
/**
* Context match information for search results
*/
enum class ContextMatch {
EXACT, // Exact context type match
SUPER, // Context is a supertype
SUB // Context is a subtype
}Patterns and utilities for delayed DI initialization in scenarios where immediate container creation is not possible.
/**
* Late-initialized DI container for manual setup scenarios
*/
class LateInitDI : DI {
/**
* Base DI instance that must be set before use
* @throws UninitializedPropertyAccessException if accessed before initialization
*/
lateinit var baseDI: DI
// All DI operations delegate to baseDI after initialization
}
/**
* Property delegate for late-initialized DI-aware properties
*/
class LateinitDIProperty<T>(
private val getDI: () -> DI,
private val creator: DirectDI.() -> T
) : LazyDelegate<T>Advanced binding patterns for functions and factories that require multiple arguments beyond single-parameter factories.
/**
* Multi-argument factory binding support
* Allows binding factories that take multiple typed arguments
*/
interface MultiArgFactory<T> {
fun with(vararg args: Any): T
}
/**
* Multi-argument binding DSL support
*/
fun <T : Any> DI.Builder.multiArgFactory(
creator: DirectDI.(Array<Any>) -> T
): MultiArgFactory<T>Enhanced error reporting with detailed container information and binding diagnostics.
/**
* Enhanced exception with detailed binding information
*/
class DI.NotFoundException(
val key: Key<*, *, *>,
message: String
) : RuntimeException(message) {
/** The binding key that was not found */
val key: Key<*, *, *>
}
/**
* Exception for dependency cycles with detailed loop information
*/
class DI.DependencyLoopException(message: String) : RuntimeException(message)
/**
* Exception for binding override conflicts
*/
class DI.OverridingException(message: String) : RuntimeException(message)
/**
* Exception for search operations that return no results
*/
class DI.NoResultException(
val search: SearchSpecs,
message: String
) : RuntimeException(message)
// Error configuration for detailed diagnostics
var DI.MainBuilder.fullDescriptionOnError: Boolean
var DI.MainBuilder.fullContainerTreeOnError: BooleanAdvanced type handling and generic type preservation for complex dependency scenarios.
/**
* Typed wrapper for values with their type information
* @param A Type of the wrapped value
*/
interface Typed<A> {
/** TypeToken representing the type of the value */
val type: TypeToken<A>
/** The actual typed value */
val value: A
}
/**
* Create a typed wrapper with immediate value
* @param type TypeToken for the value type
* @param value The value to wrap
* @return Typed wrapper containing the value and its type
*/
fun <A> Typed(type: TypeToken<A>, value: A): Typed<A>
/**
* Create a typed wrapper with lazy value evaluation
* @param type TypeToken for the value type
* @param func Function that provides the value when needed
* @return Typed wrapper with lazy value evaluation
*/
fun <A> Typed(type: TypeToken<A>, func: () -> A): Typed<A>Utilities for inspecting and debugging DI container state and binding resolution.
/**
* Container tree interface for inspecting binding structure
*/
interface DITree {
/**
* Find bindings matching search criteria
*/
fun find(search: SearchSpecs): List<DI.Key<*, *, *>>
/**
* Get all registered bindings
*/
fun allBindings(): Map<DI.Key<*, *, *>, DIBinding<*, *, *>>
/**
* Get container description for debugging
*/
fun containerDescription(): String
}
/**
* Binding information for diagnostics
*/
data class BindingInfo(
val key: DI.Key<*, *, *>,
val binding: DIBinding<*, *, *>,
val scope: String?,
val description: String
)Usage Examples:
// Constructor injection with new operator
class OrderService : DirectDIAware {
override val directDI: DirectDI = appDirectDI
fun processOrder(order: Order) {
// Automatic constructor parameter injection
val validator = new(::OrderValidator) // Injects dependencies automatically
val processor = new(::PaymentProcessor) // Finds and injects required services
val emailer = new(::OrderEmailService) // Injects email service and templates
validator.validate(order)
val payment = processor.process(order.paymentInfo)
emailer.sendConfirmation(order, payment)
}
// Multi-parameter constructor injection
fun createComplexService() {
val service = new { db: Database, cache: Cache, logger: Logger, config: Config ->
ComplexService(db, cache, logger, config) // All 4 params auto-injected
}
}
}
// External sources for fallback resolution
class SpringIntegrationSource : ExternalSource {
private val springContext: ApplicationContext = getSpringContext()
override fun <C : Any, A, T : Any> getFactory(
key: DI.Key<C, A, T>,
context: C
): ((A) -> T)? {
return try {
val bean = springContext.getBean(key.type.jvmType as Class<T>)
{ _ -> bean } // Return factory that ignores argument and returns Spring bean
} catch (e: NoSuchBeanDefinitionException) {
null // Let DI continue looking
}
}
}
val integratedDI = DI {
// Regular Kodein-DI bindings
bind<UserService>() with singleton { UserServiceImpl() }
// Add Spring as fallback
externalSources.add(SpringIntegrationSource())
// Now can inject Spring beans even if not bound in Kodein-DI
}
// Sub-DI for testing and isolation
class TestingService : DirectDIAware {
override val directDI = productionDI.direct
fun runTestScenario() {
// Create test-specific DI with mocks
val testDI = subDI(copy = Copy.NonCached) {
bind<EmailService>(overrides = true) with singleton { MockEmailService() }
bind<PaymentProcessor>(overrides = true) with singleton { TestPaymentProcessor() }
// Additional test-only services
bind<TestDataProvider>() with singleton { TestDataProviderImpl() }
}
// Use test DI for this scenario
val testDirectDI = testDI.direct
val service = testDirectDI.new(::OrderService) // Uses mocked dependencies
service.processTestOrder()
}
}
// Factory currying for partial application
class DataProcessingService : DirectDIAware {
override val directDI = appDirectDI
fun setupProcessors() {
// Get factory and curry it with configuration
val processorFactory = directDI.factory<ProcessingConfig, DataProcessor>()
val standardConfig = ProcessingConfig(threads = 4, batchSize = 100)
val standardProcessor = processorFactory.toProvider { standardConfig }
val highPerfConfig = ProcessingConfig(threads = 8, batchSize = 200)
val highPerfProcessor = processorFactory.toProvider { highPerfConfig }
// Now have two providers with different configurations
processStandardData(standardProcessor())
processHighVolumeData(highPerfProcessor())
}
}
// Binding search and discovery
class DIInspector(private val di: DI) {
fun inspectBindings() {
val tree = di.container.tree
// Find all singleton bindings
val singletons = tree.findAllBindings {
// Search by binding type patterns
}.filter { (_, binding, _) ->
binding.javaClass.simpleName.contains("Singleton")
}
println("Found ${singletons.size} singleton bindings:")
singletons.forEach { (key, binding, match) ->
println(" ${key.description} -> ${binding.description}")
}
// Find all bindings for a specific type
val userServiceBindings = tree.find(SearchSpecs(
type = generic<UserService>(),
tag = null
))
println("UserService bindings: $userServiceBindings")
}
fun diagnosticReport(): String {
return try {
di.container.tree.containerDescription()
} catch (e: Exception) {
"Error generating diagnostic report: ${e.message}"
}
}
}
// Late initialization patterns
class ApplicationContext {
private val lateInitDI = LateInitDI()
// Services that depend on DI
private val userService: UserService by lateInitDI.instance()
private val configService: ConfigService by lateInitDI.instance()
fun initialize(config: AppConfig) {
// Set up DI after configuration is available
lateInitDI.baseDI = DI {
bind<AppConfig>() with instance(config)
bind<UserService>() with singleton { UserServiceImpl(instance()) }
bind<ConfigService>() with singleton { ConfigServiceImpl(instance()) }
}
// Now property delegates can resolve
configService.loadConfiguration()
userService.initializeUserData()
}
}
// Advanced error handling and diagnostics
class RobustDIService : DIAware {
override val di = DI {
fullDescriptionOnError = true // Include full type names in errors
fullContainerTreeOnError = true // Include all bindings in NotFoundException
bind<PrimaryService>() with singleton { PrimaryServiceImpl() }
bind<FallbackService>() with singleton { FallbackServiceImpl() }
}
fun robustOperation() {
try {
val service: PrimaryService by instance()
service.execute()
} catch (e: DI.NotFoundException) {
println("Binding not found: ${e.key.fullDescription}")
println("Available bindings:")
println(e.message) // Contains full container tree due to config
// Try fallback
val fallback: FallbackService by instance()
fallback.execute()
} catch (e: DI.DependencyLoopException) {
println("Dependency loop detected: ${e.message}")
// Handle circular dependency
}
}
}
// Multi-argument and complex binding scenarios
val advancedDI = DI {
// Multi-argument factory using array
bind<ReportGenerator>() with factory { args: Array<Any> ->
val format = args[0] as ReportFormat
val filters = args[1] as List<DataFilter>
val options = args[2] as ReportOptions
ReportGeneratorImpl(format, filters, options)
}
// Typed wrapper for preserving generic information
bind<Typed<List<User>>>() with singleton {
Typed(generic<List<User>>()) {
userRepository.getAllUsers()
}
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-kodein-di--kodein-di