Inara
Compile-time dependency injection for Kotlin Multiplatform: your modules are just interfaces.
Inara turns a plain Kotlin interface into a dependency-injection module.
You declare what a module offers as ordinary interface members, and you decide how each member is produced by supplying a Binding when you build the module.
A KSP processor generates the implementation.
@DIModule
interface NetworkModule {
val baseUrl: String
fun httpClient(): HttpClient
}
val network = NetworkModule( (1)
baseUrl = Binding.value("https://api.example.com"),
httpClient = Binding.single { HttpClient(baseUrl) }, (2)
)
network.httpClient() (3)
| 1 | A builder function generated by Inara, named after the interface. |
| 2 | A binding body sees the module as its receiver, so it can read any other member. |
| 3 | Consumers call a normal method on a normal interface. |
No reflection. No service locator. No runtime graph. Just an interface, a generated implementation, and a compiler that tells you when a dependency is missing.
Why Inara
- Your module is its own API
-
A
@DIModuleinterface is the contract consumers depend on. There is no separate "component" type, no qualifier annotation, and noget<T>()lookup. - Opinionated, by design
-
There is no global context and no mutable context. Business code stays independent from the dependency-injection layer: it has no notion that Inara even exists.
@DIFactorytakes that further still, building classes that do not import a single Inara type. - Wiring is a value, not a configuration language
-
A module instance is built by calling a generated function with one
Bindingper member. Bindings are ordinary values that you can pass around, store, and swap. - Every dependency is checked by the compiler
-
Forget a binding, and the builder call does not compile. There is no runtime "unresolved dependency" failure.
suspendmembers are native-
Mark a member
suspendand it is bound with aSuspendBindingwhose bodies can suspend. Asynchronous construction propagates the way it should, instead of hiding behindrunBlocking. - Overriding is first-class
-
Every module gets a generated
copy(…)function, so building a test variant means overriding one member and keeping the rest. - Multiplatform, without surprises
-
Inara behaves exactly the same on every platform it supports. The only exception is JS and Wasm, where thread synchronization is a no-op, since there is no thread to synchronize.
How to read this documentation
The documentation starts simple and builds up:
-
Getting started gets a first module running in a few minutes.
-
The Modules section explains
@DIModulefrom the ground up: declaring a module, choosing bindings, then inheritance and composition. -
The Factories section explains
@DIFactory, which builds your own classes from a module. -
The Guides section shows Inara in context: testing, scopes, multi-module projects, and concurrency.