Kodein Open Source Initiative documentation
The Kodein Open Source Initiative is a suite of libraries that aims to help you to develop Kotlin Multiplatform applications.
Libraries
Inara: Dependency Injection
Inara turns a plain Kotlin interface into a dependency-injection module. You declare what a module offers as ordinary interface members; you decide how each member is produced by supplying a Binding at construction time.
Inara plugs and validates everything at compile time, then gets out of your way.
There is no separate "component" type, no qualifier annotations, no get<T>() lookups.
Consumers call module.httpClient() — a normal method on a normal interface.
-
Opinionated, by design. No global context, no mutable context, and business code is always independent from the DI layer — it has no notion that Inara even exists.
@DIFactorytakes that further still, generating a class with zero imports from Inara at all. -
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 you can pass around, store, and swap. -
Every dependency is checked by the compiler. Forget a binding and the builder call doesn’t compile. There is no runtime "unresolved dependency" failure mode.
-
Bodies see the whole module. A
Binding's lambda runs with the module as its receiver, so any member can reach any sibling member — including inherited ones — without you wiring anything up:Binding.single { HttpClient(this.baseUrl) } // `baseUrl` is this module's own member -
suspendmembers are native. Mark a membersuspendand it is bound with aSuspendBinding, whose every factory takes a suspending body. The generated override is itselfsuspend, so asynchronous construction propagates the way it should instead of hiding behindrunBlockingor a`Deferred` you have to unwrap at every call site.suspend fun token(): Token // bound with SuspendBinding.single { authClient.fetchToken() } -
Overriding is first-class. Every module gets generated
copy(…)overloads, so building a test variant means overriding one member and inheriting the rest. -
Multiplatform, with no platform-specific surprises. Inara targets every platform it supports and behaves exactly the same on each of them — the sole exception is the JS/Wasm targets, where thread synchronization is a no-op, since there is no thread to synchronize.
Nomi: REST API
Nomi turns plain Kotlin interfaces into a working REST API.
You declare resources — nested interfaces that are your routes — and services — interfaces whose suspend functions are your endpoints.
A KSP processor generates a ktor server implementation from them; another generates a typed ktor client.
There is no reflection, no runtime route table and no hand-written URL on either side.
-
One declaration, both sides. The resource and service interfaces in
commonMainare the only place the API is written down. The server’s routes and the client’s implementation are both generated from them, so they can never disagree about a path, a method, or a type. -
A route is a type, not a string. Nesting
@Endpoint.Resourceinterfaces is the path structure, and a path parameter is avalon the interface that owns it. -
Business failures are in the return type. Every endpoint returns
Either<V, E>: the server answers a failure withEither.error(…), and the client receives it back as the very same, typedE. No exception crosses the wire for a business outcome. -
Everything is checked before anything is generated. A processor validates every declaration it reads, and generates nothing at all if anything is invalid. There is no partial output and no runtime "route not found".
-
The API documents itself. Set
withOpenApiSpecsand ktor serves an OpenAPI document straight from the routing tree. -
Serialization stays yours. Nomi defines the shape of a call and hands the body to ktor’s
ContentNegotiation: JSON, CBOR, ProtoBuf, or anything else ktor can negotiate. -
Multiplatform client, JVM server. The generated client runs on every target; the generated server runs on ktor-server, which is JVM-only.
VIE: Model-View-Intent
VIE brings Model-View-Intent to Compose Multiplatform: one state flow down, intents up, signals sideways.
A screen has three types: a Model that the view renders, Intents the view posts (usually user events), and Signals, which are one-off things the view reacts to but does not render, such as a navigation or a snackbar.
You declare all three in an interface, a VIEViewModel implements it, and a VIEView renders it.
No navigation framework, no event bus, no manual collectAsState.
-
The contract is an interface. The view depends on the
VIEinterface, not on the ViewModel that implements it, so you can render it in a preview or a test by giving it a different receiver. -
The view never holds the ViewModel. Inside
VIEView, the view sees aVIEViewScopewith only two members:post(intent)andsignalFlow. It cannot read the model flow, callupdate, or reachviewModelScope. -
Events and Effects are different delivery channels. An Event is broadcast to every view that is listening. An Effect reaches exactly one view, so a "navigate back" fires once even when two views observe the same ViewModel.
-
Model updates are serialized.
update { }hands its block to a single consumer and suspends until it is applied, so concurrent updates never lose one another. -
Built on
ViewModel, not beside it.VIEViewModelextendsandroidx.lifecycle.ViewModel, soviewModelScope,viewModel { }and Navigation3 entry scoping work unchanged. -
Middlewares wrap the flows. A
VIEMiddlewareintercepts the model, intent and signal flows as plainFlowtransformations (logging, filtering, inspecting) without the ViewModel or the view knowing.
MocKMP: Mocking for tests
MocKMP turns a plain interface or abstract class into a mock or a fake for your tests.
You ask for what a test needs with an annotation — @Mock, @Fake, @UsesMocks, @UsesFakes — and a KSP processor generates the implementation, ready to inject.
-
Compile-time, not reflection. A KSP processor generates a real class implementing the interface or abstract class you asked to mock or fake, so it behaves identically on every target — including the ones with no runtime reflection at all, like iOS, JS and Wasm.
-
Mocking and faking answer different questions. A
mock's behaviour is defined per test, and every call on it can be verified; afake's data is inert and zero-valued, and nothing about it is checked. Reach for a fake when a collaborator only needs to exist, and a mock when the test needs to configure or verify it. -
Nothing is generated until you ask for it.
@UsesMocks/@UsesFakes— or a@Mock/@Fakeproperty — requests exactly the types a given test needs, so an interface used across a large codebase doesn’t grow generated code everywhere it’s declared, only where it’s actually mocked or faked. -
Injection kills the constructor boilerplate. Annotate properties
@Mockor@Fakeand call`mocker.injectMocks(this)`; inherited properties are injected too, so a shared base test class works exactly as if its mocks had been declared locally. -
The
TestsWithMockshelper collapses the ceremony further. Inherit it and get aMocker, an automatic reset before every test,withMocksproperty delegates rebuilt fresh each time, andevery/everySuspending/verify/verifyWithSuspendcallable without themocker.prefix. -
Constraints read the same in a definition and in a verification.
isAny,isEqual,isNotNull,isInstanceOf, and any custom constraint you write withisValid, work identically whether they appear insideeveryor insideverify— one matcher vocabulary, not two. -
Multiplatform, with no platform-specific surprises.
mockmp-runtimetargets every platform MocKMP supports and behaves the same on each of them.