
Compile-time dependency injection compositor offering annotation-driven DI, graph validation at compile time, generated entry points, singleton support, and fast incremental builds for safe, efficient dependency wiring.
Spike is a Kotlin Multiplatform dependency injection compositor that ensures compile-time safety and fast, incremental compilation through KSP. It is designed to be straightforward to use and universally applicable across all Kotlin platforms.
Apply the plugin next to your Kotlin plugin. It applies KSP and adds the Spike runtime and compiler to every target, including each target of a Kotlin Multiplatform project:
plugins {
kotlin("multiplatform") // or kotlin("jvm"), or the Android Kotlin plugin
id("io.github.diareuse.spike") version "<see 'tag' above>"
}Integrations such as spike-androidx are still added as regular dependencies. The manual setup below does the
same thing by hand.
[versions]
spike = "<see 'tag' above>"
[libraries]
spike = { module = "io.github.diareuse:spike", version.ref = "spike" }
spike-ktor = { module = "io.github.diareuse:spike-ktor", version.ref = "spike" }
spike-compiler = { module = "io.github.diareuse:spike-compiler", version.ref = "spike" }
spike-androidx = { module = "io.github.diareuse:spike-androidx", version.ref = "spike" }
spike-androidx-compose = { module = "io.github.diareuse:spike-androidx-compose", version.ref = "spike" }--- or ---
dependencies {
val spike = "see 'tag' above"
ksp("io.github.diareuse:spike-compiler:$spike")
implementation("io.github.diareuse:spike:$spike")
implementation("io.github.diareuse:spike-ktor:$spike")
implementation("io.github.diareuse:spike-androidx:$spike")
implementation("io.github.diareuse:spike-androidx-compose:$spike")
}val spike = "see 'tag' above"
kotlin {
// …
sourceSets {
androidMain.dependencies {
// if not using jetpack compose
implementation("io.github.diareuse:spike-androidx:$spike")
}
commonMain.dependencies {
implementation("io.github.diareuse:spike:$spike")
// if using jetpack compose
implementation("io.github.diareuse:spike-androidx-compose:$spike")
}
}
}
dependencies {
add("kspAndroid", "io.github.diareuse:spike-compiler:$spike")
add("kspJs", "io.github.diareuse:spike-compiler:$spike")
add("kspWasmJs", "io.github.diareuse:spike-compiler:$spike")
add("kspJvm", "io.github.diareuse:spike-compiler:$spike")
// and all other targets…
}Spike offers specific integrations to simplify its usage across various frameworks. Check out their respective documentation for more details:
spike-androidx) - Support for Multiplatform AndroidX
ViewModels.spike-androidx-compose) - Jetpack
Compose integration for Spike ViewModels.spike-ktor) - Seamless dependency injection for Ktor
applications.spike-compiler-dagger, spike-dagger) - Convert
Dagger modules, multibindings and components to Spike while migrating.spike-compiler-kotlin-inject) - Convert
kotlin-inject declarations and components to Spike while migrating.spike-compiler-metro, spike-metro) - Convert Metro
declarations and dependency graphs to Spike while migrating.spike-compiler-koin, spike-koin) - Share dependencies
between Spike and Koin in both directions.spike-compiler-jsr, spike-jsr) - Use classes with
javax or jakarta @Inject constructors in a Spike graph without editing them.Spike uses annotations to define your dependency graph. The core annotations are:
@Include: Includes a class, a provider function or a provider property in the dependency graph. Functions and properties must be top level or inside an object, such as a companion object.@Include.Constructor: Marks the constructor Spike uses when an @Include class has more than one.@Singleton: Marks a class as a singleton (only instantiated once per entry point instance; two entry point instances never share a singleton).@EntryPoint: Defines an entry point interface for the dependency graph.Here's an example of how to define your components:
interface Heater
@Include
class ElectricHeater : Heater
// Bind a class to an interface with an @Include extension property or function
@Include
val ElectricHeater.asHeater: Heater get() = this
// Spike can inject standard `Lazy<T>` or `spike.Provider<T>` automatically; they also break dependency cycles
@Include
class CoffeeMaker(
private val heater: Heater,
private val pumpProvider: spike.Provider<Pump>,
private val logger: Lazy<CoffeeLogger>
) {
fun brew() {
val pump = pumpProvider.get()
// ...
}
}
// You can also use @Include on functions to provide external or third-party classes!
@Include
fun provideCoffeeLogger(): CoffeeLogger = ConsoleCoffeeLogger()
@Include
class Pump {
// If a class has multiple constructors, annotate one with @Include.Constructor
@Include.Constructor
constructor()
constructor(capacity: Int)
}
// Mark a class with @Singleton to share the same instance within an entry point
@Singleton
@Include
class ShopInventory
// Depend on a function type to pass some values at runtime: Spike builds Order with the
// call's arguments and takes everything else from the graph
@Include
class Order(val table: Int, val inventory: ShopInventory)
@Include
class Waiter(private val order: (Int) -> Order) {
fun serve(table: Int) = order(table)
}A nullable dependency such as Analytics? uses a binding of Analytics?, then a binding of Analytics, and is
null when neither exists; the build warns about every such null.
Each argument type of a function-type dependency must be distinct and must not also be bound in the graph.
A @Singleton, Lazy or Provider cannot depend on an argument, because it would outlive the call.
To access your dependency graph, you need to create an interface and annotate it with @EntryPoint.
A companion object is required so Spike can generate an extension function on it.
@EntryPoint
interface CoffeeShop {
val coffeeMaker: CoffeeMaker
val inventory: ShopInventory
companion object
}After building your project, Spike generates a factory extension on your entry point's companion object. You can use it to create your graph and access the dependencies:
fun main() {
val coffeeShop = CoffeeShop() // invokes generated extension on CoffeeShop.Companion
val coffeeMaker = coffeeShop.coffeeMaker
coffeeMaker.brew()
}Add spike-test to your test dependencies and run the compiler on your tests too:
dependencies {
testImplementation("io.github.diareuse:spike-test:$spike")
kspTest("io.github.diareuse:spike-compiler:$spike")
}A test entry point that extends a production entry point gets the production graph, and @Replaces
swaps a production binding for a fake:
@Include
@Replaces(Repository::class)
class FakeRepository : Repository
@EntryPoint
interface TestCoffeeShop : CoffeeShop {
companion object
}
val shop = TestCoffeeShop() // every Repository is now FakeRepositoryThe replacement must be a Repository, and the production graph must bind Repository; otherwise the
build fails. Production entry points are unaffected.
Spike is licensed under the Mozilla Public License 2.0. Changes to Spike's own files must be shared under the same license; the code you write with Spike can be under any license, open or closed.
Code that the Spike compiler generates into your project (for example *_Factory, *_Program0,
*_EntryPoint) is not covered by the MPL-2.0. It belongs to you and you may license it however you like.
Releases up to and including 0.0.15 were published under GPL-v3.
Spike is a Kotlin Multiplatform dependency injection compositor that ensures compile-time safety and fast, incremental compilation through KSP. It is designed to be straightforward to use and universally applicable across all Kotlin platforms.
Apply the plugin next to your Kotlin plugin. It applies KSP and adds the Spike runtime and compiler to every target, including each target of a Kotlin Multiplatform project:
plugins {
kotlin("multiplatform") // or kotlin("jvm"), or the Android Kotlin plugin
id("io.github.diareuse.spike") version "<see 'tag' above>"
}Integrations such as spike-androidx are still added as regular dependencies. The manual setup below does the
same thing by hand.
[versions]
spike = "<see 'tag' above>"
[libraries]
spike = { module = "io.github.diareuse:spike", version.ref = "spike" }
spike-ktor = { module = "io.github.diareuse:spike-ktor", version.ref = "spike" }
spike-compiler = { module = "io.github.diareuse:spike-compiler", version.ref = "spike" }
spike-androidx = { module = "io.github.diareuse:spike-androidx", version.ref = "spike" }
spike-androidx-compose = { module = "io.github.diareuse:spike-androidx-compose", version.ref = "spike" }--- or ---
dependencies {
val spike = "see 'tag' above"
ksp("io.github.diareuse:spike-compiler:$spike")
implementation("io.github.diareuse:spike:$spike")
implementation("io.github.diareuse:spike-ktor:$spike")
implementation("io.github.diareuse:spike-androidx:$spike")
implementation("io.github.diareuse:spike-androidx-compose:$spike")
}val spike = "see 'tag' above"
kotlin {
// …
sourceSets {
androidMain.dependencies {
// if not using jetpack compose
implementation("io.github.diareuse:spike-androidx:$spike")
}
commonMain.dependencies {
implementation("io.github.diareuse:spike:$spike")
// if using jetpack compose
implementation("io.github.diareuse:spike-androidx-compose:$spike")
}
}
}
dependencies {
add("kspAndroid", "io.github.diareuse:spike-compiler:$spike")
add("kspJs", "io.github.diareuse:spike-compiler:$spike")
add("kspWasmJs", "io.github.diareuse:spike-compiler:$spike")
add("kspJvm", "io.github.diareuse:spike-compiler:$spike")
// and all other targets…
}Spike offers specific integrations to simplify its usage across various frameworks. Check out their respective documentation for more details:
spike-androidx) - Support for Multiplatform AndroidX
ViewModels.spike-androidx-compose) - Jetpack
Compose integration for Spike ViewModels.spike-ktor) - Seamless dependency injection for Ktor
applications.spike-compiler-dagger, spike-dagger) - Convert
Dagger modules, multibindings and components to Spike while migrating.spike-compiler-kotlin-inject) - Convert
kotlin-inject declarations and components to Spike while migrating.spike-compiler-metro, spike-metro) - Convert Metro
declarations and dependency graphs to Spike while migrating.spike-compiler-koin, spike-koin) - Share dependencies
between Spike and Koin in both directions.spike-compiler-jsr, spike-jsr) - Use classes with
javax or jakarta @Inject constructors in a Spike graph without editing them.Spike uses annotations to define your dependency graph. The core annotations are:
@Include: Includes a class, a provider function or a provider property in the dependency graph. Functions and properties must be top level or inside an object, such as a companion object.@Include.Constructor: Marks the constructor Spike uses when an @Include class has more than one.@Singleton: Marks a class as a singleton (only instantiated once per entry point instance; two entry point instances never share a singleton).@EntryPoint: Defines an entry point interface for the dependency graph.Here's an example of how to define your components:
interface Heater
@Include
class ElectricHeater : Heater
// Bind a class to an interface with an @Include extension property or function
@Include
val ElectricHeater.asHeater: Heater get() = this
// Spike can inject standard `Lazy<T>` or `spike.Provider<T>` automatically; they also break dependency cycles
@Include
class CoffeeMaker(
private val heater: Heater,
private val pumpProvider: spike.Provider<Pump>,
private val logger: Lazy<CoffeeLogger>
) {
fun brew() {
val pump = pumpProvider.get()
// ...
}
}
// You can also use @Include on functions to provide external or third-party classes!
@Include
fun provideCoffeeLogger(): CoffeeLogger = ConsoleCoffeeLogger()
@Include
class Pump {
// If a class has multiple constructors, annotate one with @Include.Constructor
@Include.Constructor
constructor()
constructor(capacity: Int)
}
// Mark a class with @Singleton to share the same instance within an entry point
@Singleton
@Include
class ShopInventory
// Depend on a function type to pass some values at runtime: Spike builds Order with the
// call's arguments and takes everything else from the graph
@Include
class Order(val table: Int, val inventory: ShopInventory)
@Include
class Waiter(private val order: (Int) -> Order) {
fun serve(table: Int) = order(table)
}A nullable dependency such as Analytics? uses a binding of Analytics?, then a binding of Analytics, and is
null when neither exists; the build warns about every such null.
Each argument type of a function-type dependency must be distinct and must not also be bound in the graph.
A @Singleton, Lazy or Provider cannot depend on an argument, because it would outlive the call.
To access your dependency graph, you need to create an interface and annotate it with @EntryPoint.
A companion object is required so Spike can generate an extension function on it.
@EntryPoint
interface CoffeeShop {
val coffeeMaker: CoffeeMaker
val inventory: ShopInventory
companion object
}After building your project, Spike generates a factory extension on your entry point's companion object. You can use it to create your graph and access the dependencies:
fun main() {
val coffeeShop = CoffeeShop() // invokes generated extension on CoffeeShop.Companion
val coffeeMaker = coffeeShop.coffeeMaker
coffeeMaker.brew()
}Add spike-test to your test dependencies and run the compiler on your tests too:
dependencies {
testImplementation("io.github.diareuse:spike-test:$spike")
kspTest("io.github.diareuse:spike-compiler:$spike")
}A test entry point that extends a production entry point gets the production graph, and @Replaces
swaps a production binding for a fake:
@Include
@Replaces(Repository::class)
class FakeRepository : Repository
@EntryPoint
interface TestCoffeeShop : CoffeeShop {
companion object
}
val shop = TestCoffeeShop() // every Repository is now FakeRepositoryThe replacement must be a Repository, and the production graph must bind Repository; otherwise the
build fails. Production entry points are unaffected.
Spike is licensed under the Mozilla Public License 2.0. Changes to Spike's own files must be shared under the same license; the code you write with Spike can be under any license, open or closed.
Code that the Spike compiler generates into your project (for example *_Factory, *_Program0,
*_EntryPoint) is not covered by the MPL-2.0. It belongs to you and you may license it however you like.
Releases up to and including 0.0.15 were published under GPL-v3.